Handling notifications in the mobile app

This topic describes how to handle push notification in the app running on mobile devices.

Basics

In order to implement a push notification mechanism, you need to set up a server part (token maintainer and push notification server), based on a push notification framework such as Firebase Cloud Messaging (FCM) or Apple® Push Notification service (APNs). In Addition, you need to handle notification events in your mobile app. This section describes how to implement push notification in the app with the push notification API available in Genero BDL.

The same code base can be used to handle push notifications for Android™ (using FCM) and iOS (using APNs) devices. Only the content of the notification message will have to be processed with specific code, as the structure of the message differs depending on standards defined by the push notification framework.

Genero API for push notifications

Genero BDL provides an API to handle push notification on mobile apps. Dedicated front calls are available to register to a push server, fetch push notification data, and unregister:

To detect when a notification message arrives from the push server, a specific action called notificationpushed must be used by app code on a ON ACTION handler. This special action is referenced as a predefined action.

Android app permissions for FCM push notifications

Android apps using push notification services need specific permissions (Android manifest), such as:
  • android.permission.INTERNET
  • android.permission.GET_ACCOUNTS
  • android.permission.WAVE_LOCK
  • com.google.android.c2dm.permission.RECEIVE
  • application-package-name.permission.C2D_MESSAGE where application-package-name is the Android package name of your app (for example, com.mycompany.pushclient)

Permissions are automatically set for Android APK packages by the GMA buildtool. As some permissions need to be prefixed with the package name, they are applied with the --build-app-package-name option.

See the FCM documentation for more details about required permissions for push notifications.

iOS app certificates for APNs push notifications

iOS apps must be created with an Apple certificate for development or distribution, linked to an App ID (or Bundle ID) with push notification enabled. The provisioning profile used when building the IPA must be linked to the App ID with push enabled. Certificate, provisioning and bundle id must be specified to the GMI buildtool.

Handling push notification in the app

To handle push notifications in your mobile app, perform the following steps:
  1. Register to the push service and get the registration token
  2. Send the push notification token to your token maintainer
  3. Handle notification events with the notificationpushed action
  4. Eventually un-register from the push servers

1 - Registering to the push service and to the push provider

Register the app to the push notification service with the "registerForRemoteNotifications" front call.
  • When using FCM, you must provide Sender ID to identify the FCM project.
  • When using APNs, you can set the Sender ID to NULL.
Note: The app does not need to register for notification each time it is restarted. Even if the app is closed, the registration is still active until the unregisterFromRemoteNotifications front call is performed. At first execution, an app will typically ask if the user wants to get push notifications and register to the push service if needed. To disable push notification, apps usually implement an option that can be disabled (to unregister) and re-enabled (to register again) by the user. On Android, the app must register for notification each time it is upgraded.
Important:

When an app restarts, if notifications are pending and the app has already registered for push notification in a previous execution, the notificationpushed action will be raised as soon as a dialog with the corresponding ON ACTION handler activates. The app then performs a getRemoteNotifications front call as in the usual way, to get the pending notifications pushed to the device while the app was off.

However, special consideration needs to be given to iOS devices. When push notification arrives for an iOS app that has not started, there is no mechanism to wake up the app and get the push data. Therefore, when the user starts the app from the springboard, there will never be any push data available. Depending on the context, implement the following programming patterns to solve this problem:
  1. If the push notification contains a badge number, the app can verify if the badge is greater than 0 (with the getBadgeNumber front call) in order to perform a getRemoteNotifications front call. Even if there is no data available with the front call, it is recommended that the app sends a request directly to the server push provider to get last push data.
  2. If the push notification does not contain badge numbers, it is still recommended that the app performs a getRemoteNotification front call when it starts. If there is no push data available from the front call, the recommendation is that the app sends a request to the server push provider to see if there is push data available. This is by the way also recommended when receiving a notificationpushed action during application life time.
  3. If the user starts the app from the Notification Center, the app is launched with push data transmitted from the system, and the notificationpushed action is sent. It is recommended that the app perform a getRemoteNotifications front call and get the push data.
The registerForRemoveNotifications front call will return a registration token for the app which will be used by the push server (a.k.a push provider).
  • When using FCM, the returned identifier is the FCM "registration token".
  • When using APNs, the returned identifier is the APNs "device token".
DEFINE rec RECORD
           tm_host STRING,
           tm_port INTEGER,
           notification_type STRING,
           user_name STRING,
           registration_token STRING
       END RECORD

...

    LET rec.tm_host = "https://pushreg.example.orion"
    LET rec.tm_port = 4930
    LET rec.app_user = "mike"
    LET rec.notification_type = "FCM"

...
    DIALOG ATTRIBUTES(UNBUFFERED)
      INPUT BY NAME rec.tm_host,
                    rec.tm_port,
                    rec.notification_type,
                    rec.user_name,
                    rec.registration_token
            ATTRIBUTES(WITHOUT DEFAULTS)
      END INPUT
      ...
      ON ACTION register
         LET rec.registration_token = register(rec.notification_type, rec.user_name)

...

FUNCTION register(notification_type, app_user)
    DEFINE notification_type STRING,
           app_user STRING
    DEFINE registration_token STRING
    TRY
        CALL ui.Interface.frontCall(
                "mobile", "registerForRemoteNotifications",
                [ ], [ registration_token ] )
        IF tm_command( "register", notification_type,
                       registration_token, app_user, 0 ) < 0 THEN
           RETURN NULL
        END IF
    CATCH
        MESSAGE "Registration failed."
        RETURN NULL
    END TRY
    MESSAGE SFMT("Registration succeeded (token=%1)", registration_token)
    RETURN registration_token
END FUNCTION

2 - Sending a push notification token to your token maintainer

Once registered to the FCM or APNs service, the app must also register to the push server or push provider by sending the token obtained in step 1.

This is typically done by using a RESTFul HTTP POST, sending the token (along with additional application user information) to a dedicated server program that maintains the list of registered devices/tokens.

The device token maintainer can be implemented in BDL as a Web Service program, as described in Implementing a token maintainer.

In this tutorial, the tm_command() function implements token registration (as well as badge number handling for APNs):

IMPORT com
IMPORT util

...
    LET rec.tm_host = "https://pushreg.example.orion"
    LET rec.tm_port = 4930
...

FUNCTION tm_command( command, notification_type, registration_token,
                     app_user, badge_number )
    DEFINE command STRING,
           notification_type STRING,
           registration_token STRING,
           app_user STRING,
           badge_number INTEGER
    DEFINE url STRING,
           json_obj util.JSONObject,
           req com.HTTPRequest,
           resp com.HTTPResponse,
           json_result STRING,
           result_rec RECORD
                          status INTEGER,
                          message STRING
                      END RECORD
    TRY
        LET url = SFMT( "http://%1:%2/token_maintainer/%3",
                        rec.tm_host, rec.tm_port, command )
        LET req = com.HTTPRequest.create(url)
        CALL req.setHeader("Content-Type", "application/json")
        CALL req.setMethod("POST")
        CALL req.setConnectionTimeOut(5)
        CALL req.setTimeOut(5)
        LET json_obj = util.JSONObject.create()
        CALL json_obj.put("notification_type", notification_type)
        CALL json_obj.put("registration_token", registration_token)
        CALL json_obj.put("app_user", app_user)
        CALL json_obj.put("badge_number", badge_number)
        CALL req.doTextRequest(json_obj.toString())
        LET resp = req.getResponse()
        IF resp.getStatusCode() != 200 THEN
           MESSAGE SFMT("HTTP Error (%1) %2",
                      resp.getStatusCode(),
                      resp.getStatusDescription())
           RETURN -2
        ELSE
           LET json_result = resp.getTextResponse()
           CALL util.JSON.parse(json_result, result_rec)
           IF result_rec.status >= 0 THEN
              RETURN 0
           ELSE
              MESSAGE SFMT("Notification maintainer message:\n %1", result_rec.message)
              RETURN -3
           END IF
        END IF
    CATCH
        MESSAGE SFMT("Failed to post token registration command: %1", STATUS)
        RETURN -1
    END TRY
END FUNCTION

When the app is declared as push notification client to the push server, continue with the normal program flow.

3 - Handling push notification events

To get and handle notification events, the current active dialog must implement the notificationpushed special action.

To control action view rendering defaults and current field validation behavior when the notificationpushed action is used, consider setting action default attributes for this action in your .4ad file as follows:
<ActionDefaultList>
  ...
  <ActionDefault name="notificationpushed" validate="no" defaultView="no" contextMenu="no"/>
  ...
</ActionDefaultList>
Another option is to define these action defaults attributes in the ON ACTION handler:
ON ACTION notificationpushed (VALIDATE=NO, DEFAULTVIEW=NO)
   ...
In the ON ACTION block for this action, query for notification messages by using the "getRemoteNotifications" front call, (passing the Sender ID as parameter when using FCM, for APNs the Sender ID must be NULL). This front call returns a JSON string containing a list of notification messages to be processed:
DEFINE notifs DYNAMIC ARRAY OF RECORD
           info STRING,
           ts DATETIME YEAR TO FRACTION(3)
       END RECORD

...
   DEFINE x INTEGER

   DIALOG ...
      DISPLAY ARRAY notifs TO sr.*
      END DISPLAY
       ...
      ON ACTION notificationpushed
         LET x=handle_notification()
         CALL DIALOG.setCurrentRow("sr",x)
       ...
   END DIALOG
...

FUNCTION handle_notification()
    DEFINE notif_list STRING,
           notif_array util.JSONArray,
           notif_item util.JSONObject,
           notif_data util.JSONObject,
           aps_record util.JSONObject,
           gcm_data_s STRING,
           gcm_genero_notification_s STRING,
           gcm_genero_notification util.JSONObject,
           info, other_info STRING,
           i, x INTEGER
    CALL ui.Interface.frontCall(
              "mobile", "getRemoteNotifications",
              [ ], [ notif_list ] )
    TRY
        LET notif_array = util.JSONArray.parse(notif_list)
        IF notif_array.getLength() > 0 THEN
           CALL setup_badge_number(notif_array.getLength())
        END IF
        FOR i=1 TO notif_array.getLength()
            LET info = NULL
            LET other_info = NULL
            LET notif_item = notif_array.get(i)
            -- Try APNs msg format
            LET aps_record = notif_item.get("aps")
            IF aps_record IS NOT NULL THEN
               LET info = aps_record.get("alert")
               LET notif_data = notif_item.get("custom_data")
               IF notif_data IS NOT NULL THEN
                  LET other_info = notif_data.get("other_info")
               END IF
            ELSE
               -- Try GCM msg format
               LET gcm_data_s = notif_item.get("data")
               IF gcm_data_s IS NOT NULL THEN
                  LET notif_data = util.JSONObject.parse(gcm_data_s)
                  IF notif_data IS NOT NULL THEN
                     LET gcm_genero_notification_s = notif_data.get("genero_notification")
                     LET gcm_genero_notification = util.JSONObject.parse(
                                                        gcm_genero_notification_s )
                     IF gcm_genero_notification IS NOT NULL THEN
                        LET info = gcm_genero_notification.get("content")
                     END IF
                     LET other_info = notif_data.get("other_info")
                  END IF
               END IF
            END IF
            IF info IS NULL THEN
               LET info = "Unexpected message format"
            END IF
            MESSAGE SFMT("Notification message:\n%1\n%2", info, other_info)
            CALL notifs.appendElement()
            LET x = notifs.getLength()
            LET notifs[x].info = SFMT("%1 (%2)", info, other_info)
            LET notifs[x].ts = CURRENT
        END FOR
    CATCH
        MESSAGE "Could not extract notification info"
    END TRY
    RETURN IIF(x==0,1,x)
END FUNCTION
When using APNs, the app must handle the badge numbers attached to the device token. The app must:
  1. Query the current badge number with the getBadgeNumber front call.
  2. Compute the new badge number based on the number of notifications consumed.
  3. Reset the badge number with the setBadgeNumber front call.
  4. Inform the token maintainer to sync the badge number in the central database.
The following function handles badge numbers for the app:
FUNCTION setup_badge_number(consumed)
    DEFINE consumed INTEGER
    DEFINE badge_number INTEGER
    TRY -- If the front call fails, we are not on iOS...
        CALL ui.Interface.frontCall("ios", "getBadgeNumber", [], [badge_number])
    CATCH
        RETURN
    END TRY
    IF badge_number>0 THEN
       LET badge_number = badge_number - consumed
    END IF
    CALL ui.Interface.frontCall("ios", "setBadgeNumber", [badge_number], [])
    IF tm_command( "badge_number", "APNS", rec.registration_token,
                   rec.user_name, badge_number) < 0 THEN
       ERROR "Could not send new badge number to token maintainer."
       RETURN
    END IF
END FUNCTION

4 - Unregistering the app from push notification

If the app no longer wants to get push notifications, unregister from the push provider (using a RESTful POST, in the regunreg_token() function), and unregister from the push service by using the "unregisterFromRemoteNotifications" front call.
  • When using FCM, you must pass the FCM Sender ID as parameter.
  • When using APNs, the parameter must be NULL.

...

    LET rec.tm_host = "https://pushreg.example.orion"
    LET rec.tm_port = 4930

    CALL unregister(rec.registration_token, rec.app_user)

...

FUNCTION unregister(notification_type, registration_token, app_user)
    DEFINE notification_type STRING,
           registration_token STRING,
           app_user STRING
    IF tm_command( "unregister", notification_type,
                   registration_token, app_user, 0 ) < 0 THEN
       RETURN
    END IF
    TRY
        CALL ui.Interface.frontCall(
                "mobile", "unregisterFromRemoteNotifications",
                [ ], [ ] )
    CATCH
        MESSAGE "Un-registration failed (broacast service)."
        RETURN
    END TRY
    MESSAGE "Un-registration succeeded"
END FUNCTION