Skip to content

Push Notifications Reference Implementation

Chidi Okwudire edited this page Jan 24, 2017 · 15 revisions

Parse simplifies the process of configuring push notifications while giving app developers sufficient flexibility in creating an implementation that is customized to meet their app's requirements. This page describes a reference push implementation and in the process guides you through different design/implementation considerations that hopefully will help in providing some direction.

Reference Push Flow Diagram

The following flowchart abstracts from the different platforms and defines a reference flow for handling incoming push messages.

parse4cn1 reference push flow

Note that the above flow is a 'happy flow', i.e., it does not include error handling. However, it's obviously important that you also consider error situations in your implementation.

The Parse4CN1 test app

The parse4cn1 test app is a CN1 app (with an unpretty UI) that was always part of the parse4cn1 project for unit test purposes. It now also features options to sending and handling push notifications in different app states. This is a very good way to better understand how the reference implementation that will be subsequently described works.

Screenshots

The following are screenshots taken from the app on different platforms.

ParseTestApp Android main page ParseTestApp iOS main page

The following screenshots illustrate handling of push messages on the different platforms and in different states.

  • The screenshot from Android illustrates the case where the app is opened via a push notification but was not completely in the foreground when ParsePush received the push notification payload. As such, the payload was buffered. The app was subsequently able to check for and retrieve the push payload via ParsePush.isAppOpenedViaPushNotification() and ParsePush.getPushDataUsedToOpenApp(), respectively.
  • The screenshot from iOS illustrates the case where the app is opened by clicking on a push notification and the app is already in the foreground so the IPushCallback.onAppOpenedViaPush() was invoked directly. In this regard, the dialog box title 'Push opened (foreground)' is somewhat confusing. It could better be 'App opened via push (callback)' or something along that line.
Android push example iOS push example

For debugging purposes, the log output of pars4cn1 is included in the app as illustrated below. Note that iOS includes additional (debug) logging since setting up push on iOS is by far the trickiest especially figuring out the application state when a push message is received/opened.

Android logging iOS logging

Executables

Precompiled versions of this app based on parse4cn1 release 3.1 are available for direct installation and testing.

  • Android: Get the .apk here
  • iOS: You need to compile the app with your own iOS certificate and provision profile to be able to run on your own test devices. See further instructions in the relevant section of the iOS setup guide here.

Note: This test app uses a test Parse backend project with free quota limitations. So if you intend to do extensive testing, please create your own Parse backend and recompile the app. Any abuse of this test project will not just affect you but everyone else that uses it so please be considerate in the number and frequency of your test push messages.

Source code

The source code is available on Github. The Main class and the StateMachine class are particularly interesting with respect to push notifications.

Reference Implementation

The reference flow described above is demonstrated by a reference implementation in the parse4cn1 test app described in the previous section. This implementation is already covered in the parse4cn1 push setup guides for the different platforms and will not be repeated here. Instead, this section identifies platform-specific implementations/difference w.r.t. the steps outlined in the reference flow.

Important:

  1. In the discussion that follows, unless otherwise stated explicitly, "scheduling of a notification" on ALL platforms only happens when the push payload contains an alert (or, in the case of Android/Windows Phone, a title). Furthermore, on Windows Phone, hidden messages are not supported so messages without an alert or title are completely 'lost' and never scheduled as notifications.
  2. This information in this section is based on experience gained during implementing push notifications in the parse4cn1 test app and thus may be incomplete or, in some respects, even incorrect. If you notice any errors, please file an issue so that this page can be updated.

Push message received

This section covers states (1), (2), (4) and (5) in the reference flow above.

Platform App in foreground App running but not in foreground App not running
Android IPushCallback.onPushReceivedForeground() will be called with the JSON payload. If the app does not handle the message, and there's notification data (an alert and/or title), a status bar notification is scheduled. Otherwise, the message is considered hidden/unprocessed and buffered for the app. The app can retrieve this data later via ParsePush.getUnprocessedData(). Note that any unprocessed data will be lost when the app is killed IPushCallback.onPushReceivedBackground() will be called with the JSON payload. If the app does not handle the message, the behavior is exactly the same as for app in foreground If there's notification data (an alert and/or title), a status bar notification is scheduled. Otherwise, the data is considered unprocessed and buffered for the app just as in the case where the app is in the foreground
iOS IPushCallback.onPushReceivedForeground() will be called with the JSON payload. If the app does not handle the message, handling is delegated to the Parse SDK (which as of October 2015 shows a popup dialog with the push alert text if available) The OS automatically schedules a notification. Unlike for Android, scheduling of a notification cannot be decided by the app in this case. Furthermore, if there's no alert AND the field content-available=1, the message is considered hidden/unprocessed in the refernce implementation and buffered for the app. The app can retrieve this data later (e.g., when it enters the foreground) via ParsePush.getUnprocessedData(). On the other hand, if there's an alert and/or content-available<>1, IPushCallback.onPushReceivedBackground() will be called with the JSON payload The OS automatically schedules a notification except if the message is a silent one. If the message is a silent one (content-available=1), no notification will be scheduled. Moreover, if background mode is also enabled as explained here, the app would be woken up to handle the silent message according to spec. However, from experiments, it's not 100% clear when the application:didReceiveRemoteNotification:fetchCompletionHandler callback is triggered and in what state the app will be. The latter will determine how the message is handled. As such, it is tricky to depend on specific callbacks/behavior here; it's best to employ this case for exactly what it's intended: Background processing of incoming data.

A note on 'secret' push messages

As already mentioned above:

  • A push message with no alert and not title on Android will automatically be considered a 'secret' push for which no notification will be scheduled. Handling will depend on app state as already explained above.
  • A push message with content-available=1 qualifies as a secret message on iOS and will be handled as described above. However, see the following remark about how to properly set this parameter when using Parse Server.

A note on the push payload

Each platform has its own specification of the push payload. Furthermore, Parse may include it's own additional field(s). Gladly, it is possible to make the push payload received by the app consistent across all platforms as explained below.

Consider an example incoming push message with the following fields.

alert = "Parse + CN1 combined rocks!"
title = "Sample push"
content-available = "1"
user-defined = "my data"

The received push message on Android is as follows. Notice that it includes a Parse-specific push-hash field that can safely be ignored by your app.

{ 
  "push_hash": "<a_hash_value>",
  "alert": "Parse + CN1 combined rocks!", 
  "content-available": "1", 
  "title": "Sample push", 
  "user-defined": "my data" 
}

The received push message on Windows phone is as follows:

{
  "alert": "Parse + CN1 combined rocks!", 
  "content-available": "1", 
  "title": "Sample push", 
  "user-defined": "my data" 
}

The received push message on iOS is as follows:

{
  "aps": { 
    "alert": "Parse + CN1 combined rocks!", 
    "content-available": "1"
  },
  "title": "Sample push", 
  "user-defined": "my data" 
}

It is desirable to have as much consistency in the payload delivered to the app as possible. As such, the iOS utility class included in parse4cn1 and used in the reference implementation flattens any incoming push message to result in:

{ 
  "alert": "Parse + CN1 combined rocks!", 
  "content-available": "1", 
  "title": "Sample push", 
  "user-defined": "my data" 
}

Push message opened

This section covers state (3) in the reference flow above.

Note that if the app is not running when the message is opened, the app is always started by the OS on all platforms. Thus, it's only interesting to consider the cases where the app is already running.

Platform App in foreground App not in foreground
Android IPushCallback.onAppOpenedViaPush() will be invoked with the JSON push payload The data is buffered for the app which would be able to retrieve it via ParsePush.getPushDataUsedToOpenApp() as soon as it enters the foreground
iOS IPushCallback.onAppOpenedViaPush() will be invoked with the JSON push payload The incoming data is temporarily buffered until the app fully enters foreground at which time IPushCallback.onAppOpenedViaPush() will be invoked with the JSON push payload. The reason for this is that, from experiments, behavior is indeterministic if push messages are forwarded to the app while it's transitioning to the foreground (i.e., in Inactive state). In some cases, the push payload was not yet available when the CN1 start() lifecycle method was called and thus it was missed; whereas in other cases, the payload was available. This timing-related indeterminism makes it very hard for the app to handle this case in a consistent way if the payload is handled based on iOS lifecycle state. Thus, the choice to always delay delivering the message until we're sure that the app is in foreground.

One of the nice features offered by Parse is the ability to actually track the opening of push notifications.

Track opening of push notifications

The reference implementation includes the necessary Parse Analytics to do so, i.e., this should just work. Bear in mind though that push messages that do not result in 'status bar' notifications, e.g. silent push messages or push messages that are directly handled on Android as described above, will NOT be traceable like this. -->

A note on badging

Usually, it is logical to clear/update the badge icon when a push message is opened. At the time of writing, badging the app icon is only supported by Parse on iOS. This means that although it's possible to set and retrieve the badge count on all platforms as illustrated in the relevant usage examples, changes to the icon badge number will only be seen on iOS.

Push registration (failure)

Since push registration is completely handled in native code on all platforms and is not triggered by explicit user request, the ParsePush does not include any method to register for push notifications.

However, push notification registration might fail, in which case, it is very useful to be notified if registration fails. The IPushCallback.onPushRegistrationFailed() exists for this purpose. Detecting push failure is basically a best effort guess on Android since at the time of writing (January 2017), it is unclear how to exactly detect if push registration failed based on the API exposed by the Parse SDK. On Android, the implementation simply reports failure if creating/saving the ParseInstallation fails. On iOS, there's an explicit application didFailToRegisterForRemoteNotificationsWithError callback for detecting push registration failure so this is used. Moreover, ParseInstallation creation failure is also reported.

Discussion

Consistently handling push messages

As can be clearly seen from the above section, it is nearly impossible to consistently handle all push callback and ParsePush methods for retrieving buffered push data in exactly the same way on all platform due to differences in the platforms and the way they handle incoming push messages. Of course, in some cases, the implementation is simply a choice of the developer of the reference implementation.

It is up to each app developer to determine how to treat incoming push messages given the platforms of interest and their app requirements. The parse4cn1 test app illustrates a possible approach that can be broken down in two steps.

If your need is basically being able to receive push messages and have your app opened when the messages are clicked, you do not need to implement the IPushCallback or use any of the ParsePush methods to handling buffered push messages.

Step 1: Check for buffered push messages in the start() CN1 lifecycle method

Every CN1 app has a start() method in the main class. This seems like the best place to check for any buffered push messages. The following methods are provided for that purpose:

Step 2: Implement the IPushCallback interface

The choice of where to implement the IPushCallback interface methods is again up to the developer. This can be done in the same main class where start() is defined or somewhere else.