Skip to content
Shai Almog edited this page Jan 24, 2016 · 32 revisions

The iOS Screenshot/Splash Screen Process

iOS apps seem to start almost instantly in comparison to Android apps.+ There is a trick to that, iOS applications have a file traditionally called Default.png that includes a 320x480 pixel image of the first screen of the application. This is an "illusion" of the application instantly coming to life and filling up with data, this rather clever but is a source trouble as the platform grows [1].

The screenshot process was a pretty clever workaround but as Apple introduced the retina display 640x960 it required a higher resolution [email protected] file, then it added the iPad, iPad Retina and iPhone 5 [2] iPhone 6 & 6+ all of which required images of their own.

To make matters worse iPad apps (and iPhone 6+ apps) can be launched in landscape mode so that’s two more resolutions for the horizontal orientation iPad. Overall as of this writing (or until Apple adds more resolutions) we need 10 screenshots for a typical iOS app:

Resolution File Name Devices

320x480

Default.png

iPhone 3gs

640x960

[email protected]

iPhone 4x

640x1136

[email protected]

iPhone 5x

1024x768

Default-Portrait.png

Non-retina ipads in portrait mode

768x1024

Default-Landscape.png

Non-retina ipads in landscape mode

2048x1536

[email protected]

Retina ipads in portrait mode

1536x2048

[email protected]

Retina ipads in landscape mode

750x1334

[email protected]

iPhone 6

1242x2208

[email protected]

iPhone 6 Plus Portrait

2208x1242

[email protected]

iPhone 6 Plus Landscape

iOS developers who use this approach literally run their applications 10 times with blank data to grab these screenshots every time they change something in the first form of their application!

With Codename One this will not be feasible since the fonts and behavior might not match the device. Thus Codename One runs the application 10 times in the build server, grabs the right sized screenshots in the simulator and then builds the app!

This means the process of the iPhone splash screen is almost seamless to the developer, however like every abstraction this too leaks.

Size

One of the first things we ran into when building one of our demos was a case where an app that wasn’t very big in terms of functionality took up 30mb!

After inspecting the app we discovered that the iPad retina PNG files were close to 5mb in size…​ Since we had 2 of them (landscape and portrait) this was the main problem. The iPad retina is a 2048x1536 device and with the leather theme the PNG images are almost impossible to compress because of the richness of details within that theme. This produced the huge screenshots that ballooned the application.

Mutable first screen

A very common use case is to have an application that pops up a login dialog on first run. This doesn’t work well since the server takes a picture of the login screen and the login screen will appear briefly for future loads and will never appear again.

Unsupported component

One of the biggest obstacles is with heavyweight components, e.g. if you use a browser or maps on the first screen of the app you will see a partially loaded/distorted MapComponent and the native webkit browser obviously can’t be rendered properly by our servers.

The workaround for such issues is to have a splash screen that doesn’t include any of the above. Its OK to show it for a very brief amount of time since the screenshot process is pretty fast.

Provisioning Profile & Certificates

One of the hardest parts in developing for iOS is the total mess they made with their overly complex certificate/provisioning process. Relatively for the complexity the guys at Apple did a great job of hiding allot of the crude details but its still difficult to figure out where to start.

Start by logging in to the iOS-provisioning portal

Log into IOS provisioning portal
Figure 1. Login for the iOS provisoning portal

In the certificates section you can download your development and distribution certificates.

Download development provisioning profile
Figure 2. Download development provisioning profile
Download distribution provisioning profile
Figure 3. Download distribution provisioning profile

In the devices section add device ids for the development devices you want to support. Notice no more than 100 devices are supported!

Add devices
Figure 4. Add devices

Create an application id; it should match the package identifier of your application perfectly!

Create application id

Create a provisioning profile for development, make sure to select the right app and make sure to add the devices you want to use during debug.

Create provisioning profile 1
Create provisioning profile 2

Refresh the screen to see the profile you just created and press the download button to download your development provisioning profile.

Create provisioning profile 3

Create a distribution provisioning profile; it will be used when uploading to the app store. There is no need to specify devices here.

Create distribution provisioning profile

Download the distribution provisioning profile.

Download distribution provisioning profile

We can now import the cer files into the key chain tool on a Mac by double clicking the file, on Windows the process is slightly more elaborate

Import cer files

We can export the p12 files for the distribution and development profiles through the keychain tool

Export p12 files

In the IDE we enter the project settings, configure our provisioning profile, the password we typed when exporting and the p12 certificates. It is now possible to send the build to the server.

IOS Project Settings

Local Notifications on iOS and Android

Local notifications are similar to push notifications, except that they are initiated locally by the app, rather than remotely. They are useful for communicating information to the user while the app is running in the background, since they manifest themselves as pop-up notifications on supported devices.

Sending Notifications

The process for sending a notification is:

  1. Create a LocalNotification object with the information you want to send in the notification.

  2. Pass the object to Display.scheduleLocalNotification().

Notifications can either be set up as one-time only or as repeating.

Example Sending Notification
LocalNotification n = new LocalNotification();
n.setId("demo-notification");
n.setAlertBody("It's time to take a break and look at me");
n.setAlertTitle("Break Time!");
n.setAlertSound("beep-01a.mp3");

Display.getInstance().scheduleLocalNotification(
        n,
        System.currentTimeMillis() + 10 * 1000, // fire date/time
        LocalNotification.REPEAT_MINUTE  // Whether to repeat and what frequency
);

The resulting notification will look like

f7200840 677e 11e5 8fd7 41eb027f8a6c

The above screenshot was taken on the iOS simulator.

Receiving Notifications

The API for receiving/handling local notifications is also similar to push. Your application’s main lifecycle class needs to implement the com.codename1.notifications.LocalNotificationCallback interface which includes a single method:

public void localNotificationReceived(String notificationId)

The notificationId parameter will match the id value of the notification as set using LocalNotification.setId().

Example Receiving Notification
public class BackgroundLocationDemo implements LocalNotificationCallback {
    //...

    public void init(Object context) {
        //...
    }

    public void start() {
        //...

    }

    public void stop() {
        //...
    }

    public void destroy() {
        //...
    }

    public void localNotificationReceived(String notificationId) {
        System.out.println("Received local notification "+notificationId);
    }
}
Note
localNotificationReceived() is only called when the user responds to the notification by tapping on the alert. If the user doesn’t opt to click on the notification, then this event handler will never be fired.

Cancelling Notifications

Repeating notifications will continue until they are canceled by the app. You can cancel a single notification by calling:

Display.getInstance().cancelLocalNotification(notificationId);

Where notificationId is the string id that was set for the notification using LocalNotification.setId().

Push Notifications (New Servers)

We are starting the complete overhaul of our push implementation that will allow us to deliver improved push related fixes/features and provide more reliability to the push service. When we designed our push offering initially it was focused around the limitations of Google App Engine which we are finally phasing out. The new servers are no longer constrained by this can scale far more easily and efficiently for all requirements.

However, as part of this we decided to separate the push functionality into two very different capabilities: push & device registration.+ Currently only push is supported and so the feature of pushing to all devices is effectively unsupported at the moment. However, once the device registration API is exposed it will allow you to perform many tasks that were often requested such as the ability to push to a cross section of devices (e.g. users in a given city).

The original push API mixed device tracking and the core push functionality in a single API which meant we had scaling issues when dealing with large volumes of push due to database issues. So the new API discards the old device id structure which is just a numeric key into our database. With the new API we have a new device key which includes the native OS push key as part of its structure e.g. cn1-ios-nativedevicekey instead of 999999.

Assuming you store device ids in your server code you can easily convert them to the new device ids, your server code can check if a device id starts with <code>cn1-</code> and if not convert the old numeric id to the new ID using this request (assuming 999999 is the device id):

https://codename-one.appspot.com/token?m=id&i=999999

The response to that will be something like this:

{"key":"cn1-ios-nativedevicecode"}

The response to that will be something or this:

{"error":"Unsupported device type"}

To verify that a push is being sent by your account and associate the push quotas correctly the new API requires a push token. You can see your push token in the developer console at the bottom of the account settings tab. If it doesn’t appear logout and login again.

The new API is roughly identical to the old API with two major exceptions:

We now need to replace usage of Push.getDeviceKey()</code> with <code>Push.getPushKey().
 We thought about keeping the exact same API but eventually decided that creating a separate API will simplify
migration and allow you to conduct it at your own pace.
.All push methods now require the push token as their first argument. The old methods will push to the old push servers
and the new identical methods that accept a token go to the new servers.

To send a push directly to the new servers you can use very similar code to the old Java SE code we provided just changing the URL, adding the token and removing some of the unnecessary arguments. Send the push to the URL https://push.codenameone.com/push/push which accepts the following arguments:

cert - http or https URL containing the push certificate for an iOS push

E.g. we can send push to the new servers using something like this from Java SE/EE:

URLConnection connection = new URL("https://push.codenameone.com/push/push").openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
String query = "token=TOKEN&amp;device=" + deviceId1 +
                    "&amp;device=" + deviceId2 + "&amp;device=" + deviceId3 +
                    "&amp;type=1&amp;auth=GOOGLE_AUTHKEY&amp;certPassword=CERTIFICATE_PASSWORD&amp;" +
                    "cert=LINK_TO_YOUR_P12_FILE&amp;body=" + URLEncoder.encode(MESSAGE_BODY, "UTF-8");
try (OutputStream output = connection.getOutputStream()) {
    output.write(query.getBytes("UTF-8"));
}
int c = connection.getResponseCode();
... read response and parse JSON

Unlike the previous API which was completely asynchronous and decoupled the new API is mostly synchronous so we return JSON that you should inspect for results and to maintain your device list. E.g. if there is an error that isn’t fatal such as quota exceeded etc. you will get an error message like this:

{"error":"Error message"}

A normal response though, will be an array with results:

[
    {"id"="deviceId","status"="error","message"="Invalid Device ID"},
    {"id"="cn1-gcm-nativegcmkey","status"="updateId" newId="cn1-gcm-newgcmkey"},
    {"id"="cn1-gcm-okgcmkey","status"="OK"},
    {"id"="cn1-gcm-errorkey","status"="error" message="Server error message"},
    {"id"="cn1-ios-iphonekey","status"="inactive" },
]
There are several things to notice in the responses above:
If the response contains status=updateId it means that the GCM server wants you to update the device id to

a new device id. You should do that in the database and avoid sending pushes to the old key. .iOS doesn’t acknowledge device receipt but it does send a status=inactive result which you should use to remove the device from the list of devices.

NOTICE: It seems that APNS (Apple’s push service) returns uppercase key results. So you need to query the database in a case insensitive way.

Push Notifications (Legacy)

Warning
We are currently in the process of migrating to a new push notification architecture, see the section above for further details. A lot of the topics mentioned below are still relevant for the new push architecture which is why we are currently leaving this section intact.

Push notification is only enabled for pro accounts in Codename One due to some of the server side infrastructure we need to maintain to support this feature.

Push notification allows you to send a message to a device, usually a simple text message which is sent to the application or prompted to the user appropriately. When supported by the device it can be received even when the application isn’t running and doesn’t require polling which can drain the devices battery. The keyword here is "when supported" unfortunately not all devices support push notification e.g. Android device that don’t have the Google Play application (formerly Android Market) don’t support push and must fall back to polling the server which isn’t ideal.

Currently Codename One supports pushing to Google authorized Android devices: GCM (Google Cloud Messaging), to iOS devices: Push Notification & to blackberry devices.

For other devices we will fallback to polling the server in a given frequency, not an ideal solution by any means so Codename One provides the option to not fallback.

This is how Apple describes push notification (image source Apple):

Apple push notification workflow

The "provider" is the server code that wishes to notify the device. It needs to ask Apple to push to a specific device and a specific client application. There are many complexities not mentioned here such as the need to have a push certificate or how the notification to APNS actually happens but the basic idea is identical in iOS and Android’s GCM.

Codename One hides some but not all of the complexities involved in the push notification process. Instead of working between the differences of APNS/GCM & falling back to polling, we effectively do everything that’s involved.

Push consists of the following stages on the client:

  1. Local Registration - an application needs to register to ask for push. This is done by invoking:

  2. Display.getInstance().registerPush(metaData, fallback);

  3. The fallback flag indicates whether the system should fallback to polling if push isn’t supported.

  4. On iOS this stage prompts the user indicating that the application is interested in receiving push notification messages.

  5. Remote registration - once registration in the client works, the device needs to register to the cloud. This is an important step since push requires a specific device registration key (think of it as a "phone number" for the device). Normally Codename One registers all devices that reach this stage so you can push a notification for everyone, however if you wish to push to a specific device you will need to catch this information! To get push events your main class (important, this must be your main class!) should implement the PushCallback interface. The registeredForPush(String) callback is invoked with the device native push ID (not the id you should use. Once this method is invoked the device is ready to receive push messages.

  6. In case of an error during push registration you will receive the dreaded: pushRegistrationError.

    Push error
  7. This is a very problematic area on iOS, where you must have a package name that matches EXACTLY the options in your provisioning profile, which is setup to support push. It is also critical that you do not use a provisioning profile containing a * character in it.

  8. You will receive a push callback if all goes well.

First there are several prerequisites you will need in order to get started with push:

  • Android - you can find the full instructions from Google at http://developer.android.com/google/gcm/gs.html. You will need a project id that looks something like this: 4815162342. You will also need the server key, which looks something like this: AIzaSyATSw_rGeKnzKWULMGEk7MDfEjRxJ1ybqo.

  • iOS - You will need to create a provisioning profile that doesn’t have the * element within it.

    For that provisioning profile you will need to enable push and download a push certificate. Notice that this push certificate should be converted to a P12 file in the same manner we used in the signing tutorials.

    You will need the password for that P12 file as well.

    You will need a distribution P12 and a testing P12.

Warning! The P12 for push is completely different from the one used to build your application, don’t confuse them! You will need to place the certificate on the web so our push server can access them, we often use dropbox to store our certificates for push. * Blackberry - you need to register with Blackberry for credentials to use their push servers at https://developer.blackberry.com/devzone/develop/platform_services/push_overview.html.

+ Notice that initially you need to register for evaluation and later on move your app to production. This registration will trigger an email which you will receive that will contain all the information you will need later on. Such as your app ID, push URL (which during development is composed from your app ID), special password and client port number.

To start using push (on any platform) you will need to implement the PushCallback interface within your main class. The methods in that interface will be invoked when a push message arrives:

public class PushDemo implements PushCallback {

    private Form current;

    public void init(Object context) {
    }

    public void start() {
        if(current != null){
            current.show();
            return;
        }
        new StateMachine("/theme");
    }

    public void stop() {
        current = Display.getInstance().getCurrent();
    }

    public void destroy() {
    }

    public void push(String value) {
        Dialog.show("Push Received", value, "OK", null);
    }

    public void registeredForPush(String deviceId) {
        Dialog.show("Push Registered", "Device ID: " + deviceId + "\nDevice Key: " + Push.getDeviceKey() , "OK", null);
    }

    public void pushRegistrationError(String error, int errorCode) {
        Dialog.show("Registration Error", "Error " + errorCode + "\n" + error, "OK", null);
    }
}

You will then need to register to receive push notifications (its OK to call register every time the app loads) by invoking this code below (notice that the google project id needs to be passed to registration):

@Override
protected void onMain_RegisterForPushAction(Component c, ActionEvent event) {
    Hashtable meta = new Hashtable();
    meta.put(com.codename1.push.Push.GOOGLE_PUSH_KEY, findGoogleProjectId(c));
    Display.getInstance().registerPush(meta, true);
}

Sending the push is a more elaborate affair; we need to pass the elements to the push that is necessary for the various device types depending on the target device. If we send null as the destination device our message will be sent to all devices running our app. However, if we use the device key which you can get via Push.getDeviceKey() you can target the device directly. Notice that the device key is not the argument passed to the registration confirmation callback!

Other than that we need to send various arguments whether this is a production push (valid for iOS where there is a strict separation between the debug and the production push builds) as well as the variables discussed above:

@Override
protected void onMain_SendPushAction(Component c, ActionEvent event) {
    String dest = findDestinationDevice(c).getText();
    if(dest.equals("")) {
        dest = null;
    }
    boolean prod = findProductionEnvironement(c).isSelected();
    String googleServerKey = findGoogleServerKey(c).getText();
    String iOSCertURL = findIosCert(c).getText();
    String iOSCertPassword = findIosPassword(c).getText();
    String bbPushURL = findBbPushURL(c).getText();
    String bbAppId = findBbAppId(c).getText();
    String bbPassword = findBbPassword(c).getText();
    String bbPort = findBbPort(c).getText();
    Push.sendPushMessage(findPushMessage(c).getText(), dest, prod, googleServerKey, iOSCertURL, iOSCertPassword, bbPushURL, bbAppId, bbPassword, bbPort);
}

Unfortunately we aren’t done yet!

We must define the following build arguments in the project properties:

ios.includePush=true

rim.includePush=true
rim.ignor_legacy=true
rim.pushPort=...
rim.pushAppId=...
rim.pushBpsURL=...

Once you define all of these push should work for all platforms.

You can perform the same task using our server API to send a push message directly from your server using the following web API. Notice that all arguments must be submitted as post!

Argument Values Description

device

numeric id or none

Optional, if omitted the message is sent to all devices

packageName

com.myapp…​

The package name of your main class uniquely identifies your app. This is required even when submitting a device ID

email

[email protected]

The email address of the developer who built the app. This provides validation regarding the target of the push

type

numeric defaults to 1

The type of push, standard is 1 but there are additional types like 2 which is a silent push (nothing will be shown to the user) or 3 which combines a hidden payload with text visible to the user

auth

Google authorization key

Needed to perform the GCM push

certPassword

password

The password for the P12 push certificate for an iOS push

cert

URL

A url containing a downloadable P12 push certificate for iOS

body

Arbitrary message

The payload message sent to the device

production

true/false

Whether the push is sent to the iOS production or debug environment

burl

URL

The blackberry push URL

bbAppId

App ID sent by RIM

bbPass

Push password from RIM

bbPort

Push port from RIM

This can be accomplished on a Java server or client application using code such as this:

URLConnection connection = new URL("https://codename-one.appspot.com/sendPushMessage").openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
String query = "packageName=PACKAGE_NAME&[email protected]&device=" + deviceId +
                    "&type=1&auth=GOOGLE_AUTHKEY&certPassword=CERTIFICATE_PASSWORD&" +
                    "cert=LINK_TO_YOUR_P12_FILE&body=" + URLEncoder.encode(MESSAGE_BODY, "UTF-8");
try (OutputStream output = connection.getOutputStream()) {
    output.write(query.getBytes("UTF-8"));
}
int c = connection.getResponseCode();

This can be accomplished in PHP using code like this:

$args = http_build_query(array(
'certPassword' => 'CERTIFICATE_PASSWORD',
'cert' => 'LINK_TO_YOUR_P12_FILE',
'production' => false,
'device' => $device['deviceId'],
'packageName' => 'YOUR_APP_PACKAGE_HERE',
'email' => 'YOUR_EMAIL_ADDRESS_HERE',
'type' => 1,
'auth' => 'YOUR_GOOGLE_AUTH_KEY',
'body' => $wstext));
$opts = array('http' =>
    array(
        'method'  => 'POST',
        'header'  => 'Content-type: application/x-www-form-urlencoded',
        'content' => $args
    )
);
$context = stream_context_create($opts);
$response = file_get_contents("https://codename-one.appspot.com/sendPushMessage", false, $context);

Push Types & Badges

iOS App Badges

iOS provides the ability to send a push notification that triggers a numeric badge on the application icon. This can be achieved by sending a push notification with the type 100 and the number for the badge. That number would appear on the icon, when you launch the app the next time the default behavior is to clear the badge value.

There is also an option to send push type 101 and provide a badge number semi-colon and a message e.g. use a message body such as this: "3;This message is shown to the user with number 3 badge". Obviously, this feature will only work for iOS so don’t send these push types to other platforms…​

The badge number can be set thru code as well, this is useful if you want the badge to represent the unread count within your application. To do this we have two methods in display: isBadgingSupported() & setBadgeNumber(int). Notice that even if isBadgingSupported will return true, it will not work unless you activate push support!

To truly utilize this you might need to disable the clearing of the badges on startup which you can do with the build argument ios.enableBadgeClear=false.

iOS Beta Testing (Testflight)

In recent versions of iOS Apple added the ability to distribute beta versions of your application to beta testers using tools they got from the testflight acquisition. This is supported for pro users as part of the crash protection feature.

To take advantage of that use the build argument ios.testFlight=true and then submit the app to the store for beta testing. Make sure to use a release build target.


1. In fact Apple provided another trick starting with OS 8 but that doesn’t apply to games or Codename One and has its own set of problems.
2. slightly larger screen and different aspect ratio
Clone this wiki locally