Thursday, February 26, 2015

Introduction to Mobile Push Notifications with Lightstreamer 6.0

With the release of version 6.0 final, mobile push notifications (MPN for short) are now an integral part of Lightstreamer. Since our original post about 6.0 alpha, the MPN APIs have been streamlined, enhanced and extended to Android. With this blog post we take a second look at these new functionality, with code samples for both iOS/OS X and Android.

How MPNs Are Handled in Lightstreamer 6.0

Mobile push notifications are simply real-time push notifications diverted to a different channel. For this reason, they share many properties with their real-time counterpart:
  • they are initiated on the client with a subscription and terminated with an unsubscription;
  • each notification is originated by a data adapter real-time update.

This means that any subscription to any existing data adapter may be used for MPNs, with only a few restrictions:
  • COMMAND and RAW modes may not be used;
  • the unfiltered mode is not supported;
  • the selector is not supported.

On the other hand, MPN subscriptions do have some specific characteristics:
  • they are persistent: they survive the session and may be modified or deleted by a subsequent, different session;
  • they may be associated with a trigger: an optional expression that blocks the notification delivery until it evaluates to true;
  • their content may be formatted for direct consumption by the user: this applies in particular to iOS, where the MPN is shown to the user by the system.
MPN subscriptions are stored on the Lightstreamer Server in a specific database, with each subscription associated to the token/registration ID of the corresponding device. For this reason, the result of the device registration procedure must be forwarded to the client library. Subsequent requests for an MPN subscription will use the received device token/registration ID.

Once an MPN subscription is stored on the server, a module called (with a lack of fantasy) MPN Module loads them at server startup and subscribes to the appropriate data adapter internally. In this way, whenever the data adapter fires an update, even if the device is not currently connected, a mobile push notification may be created and delivered.

But first things first.

Registering the Device

Both iOS/OS X and Android require that the device is registered before it can receive any MPN. Client libraries now provide specific APIs to help the registration process and give the library the information it needs.

Registering the Device on iOS and OS X

On iOS/OS X the device registration procedure is managed by the system. The library is called at the end of the process to be notified of the registration result. Here is an example of the registration call for iOS < 8:

[application registerForRemoteNotificationTypes:
  UIRemoteNotificationTypeAlert
  UIRemoteNotificationTypeBadge
  UIRemoteNotificationTypeSound];

And here is an example for iOS >= 8:

NSSet *categories= // ...

UIUserNotificationSettings *mySettings= [UIUserNotificationSettings
  settingsForTypes:
    UIUserNotificationTypeBadge
    UIUserNotificationTypeSound
    UIUserNotificationTypeAlert
  categories:categories];
[[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];

Once the registration is completed, an API call is needed to forward the device token to the client library MPN subsystem:

- (void) application:(UIApplication *)application
  didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {

  dispatch_async(_backgroundQueue, ^() {
    LSMPNTokenStatus tokenStatus= [LSClient registrationForMPNSucceededWithToken:deviceToken];

    switch (tokenStatus) {
      case LSMPNTokenStatusFirstUse:
      case LSMPNTokenStatusNotChanged:
        // Everything normal, nothing to do
        break;

      case LSMPNTokenStatusChanged:
        // The library will automatically update the device 
        // token on the server and report when done
        break;
    }
  });
}

- (void) application:(UIApplication *)application
  didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {

  // MPN subscriptions can't be activated, should
  // disable MPN functionalities
}

The client library stores internally the device token on the NSUserDefaults database. This is necessary because the value may change: when this happens, the library automatically request to the server to update the token, so that MPN subscriptions created with the previous token may continue to work.

The result of registrationForMPNSucceeded call reports the status of the token: if it has changed, the library will request to update it as soon as a connection to Lightstreamer is opened. Later it will report the success or failure of the request to the connection delegate, with one of the following calls:

- (void) clientConnection:(LSClient *)client 
  didSucceedChangingDeviceTokenOnServerWithInfo:(LSMPNTokenChangeInfo *)info {

  // Everything normal, nothing to do
}

- (void) clientConnection:(LSClient *)client 
  didFailChangingDeviceTokenOnServerWithError:(LSException *)error {

  // Previous MPN subscriptions must be
  // manually reactivated
}

If the update request should fail, it means previous MPN subscriptions could not be recovered and must be reactivated. The client library provides extensive APIs to inquire, modify and deactivate MPN subscriptions. More on this later.

Registering the Device on Android

On Android the device registration procedure is managed by the client library. The app code is called at the end of the process to be notified of the registration result:

String senderId = // ...

try {
  LSClient.registerForMpn(getApplicationContext(), 
    senderIdnew MpnRegistrationListener() {
        
    public void registrationSucceeded(String registrationId, 
      MpnRegistrationIdStatus registrationStatus) {
      // Everything normal, nothing to do
    }

    public void registrationIdChangeSucceeded(MpnRegistrationIdChangeInfo regChange) {
      // Everything normal, nothing to do
    }
        
    public void registrationFailed(Exception e) {
      // MPN subscriptions can't be activated, should
      // disable MPN functionalities
    }
        
    public void registrationIdChangeFailed(Exception e) {
      // Previous MPN subscriptions must be 
      // manually reactivated
    }
  });

} catch (MpnRegistrationException e) {
  // Handle error appripriately

Similarly to iOS/OS X, on Android too the registration ID is stored internally in the SharedPreferences database. And here too the ID may change, and when this happens the library will request to update it as soon as a connection to Lightstreamer is opened. The success or failure of the request is later reported to the MpnRegistrationListener (implemented anonymously in the example above).

Again similarly to iOS/OS X, the registration may succeed with a different ID and the request may fail. It would mean previous MPN subscriptions could not be recovered and must be reactivated. See later for a description of the APIs the client library provides to inquire, modify and deactivate MPN subscriptions.

Activating an MPN Subscription

Terminology note: since MPN subscriptions actually live on the server and survive the session life cycle, we use a different terminology to make this distinction evident. So, while you subscribe to a table, you activate an MPN subscription. Correspondingly, while you unsubscribe from a table, you deactivate an MPN subscription.

Activating an MPN Subscription on iOS/OS X

Let's take a common table subscription and make it an MPN subscription. Here is an example of a table subscription:

LSExtendedTableInfo *tableInfo= [LSExtendedTableInfo extendedTableInfoWithItems:@[item]
  mode:LSModeMerge
  fields:@[@"last_price", @"time", @"pct_change", @"stock_name"]
  dataAdapter:@"QUOTE_ADAPTER"
  snapshot:YES];

_tableKey= [_client subscribeTableWithExtendedInfo:tableInfo
  delegate:self
  useCommandLogic:NO];

To make it an MPN subscription we have to describe how the notification must be formatted. This is accomplished by creating an LSMPNInfo object that incapsulates the table info object:

LSMPNInfo *mpnInfo= [LSMPNInfo mpnInfoWithTableInfo:tableInfo
  sound:@"Default"
  badge:@"AUTO"
  format:@"Stock ${stock_name} is now ${last_price}"];

Here we specify typical iOS push notification details, like the sound, the badge and the format the notification text must have. The text format may contain field content using common Unix variable expansion syntax. For a stock name of "Anduct" and a last price of "2.84", the previous format string would evaluate to:

"Stock Anduct is now 2.84"

And that is what a user would see on the display. Of course these fields must be part of the subscription. You may also refer to fields by position using a syntax like "$[1]". Remember that field positions are 1-based.

Note that the badge reports "AUTO". This special value means the icon badge will be automatically set and incremented by the server. This handy feature requires that when you reset the icon badge on the app, you correspondingly call an API to let the server reset the badge counter on its side:

- (void) applicationDidBecomeActive:(UIApplication *)application {

  // Reset the app's icon badge
  application.applicationIconBadgeNumber= 0;

  dispatch_async(_backgroundQueue, ^() {

    // Notify Lightstreamer that the app's icon badge has been reset
    [LSClient applicationMPNBadgeReset];
  });
}

To the MPN info object we can also add some custom information, to the benefit of the app logic. For example:

// Add custom data to match the notification
// and the corresponding item
mpnInfo.customData= @{@"item": item};

You can later extract this information when the MPN is delivered to the application:

- (void) application:(UIApplication *)application 
  didReceiveRemoteNotification:(NSDictionary *)userInfo {

  // Extract MPN custom data
  NSString *item= [userInfo objectForKey:@"item"];
  // ...
}

The MPN info object may also contain a trigger expression, but more on this later. Finally, new MPN categories of iOS 8 are also supported:

// Add category for iOS >= 8.0
mpnInfo.category= @"STOCK_PRICE_CATEGORY";

If a corresponding category has been set up during device registration, its actions will be presented when an MPN is delivered to the user.

Once the MPN info object is set up, we just call activateMPN in place of subscribeTable:

// Activate the MPN subscription
_mpnSubscription= [_client activateMPN:mpnInfo coalescing:YES];

Ignore the coalescing flag for now, more on this later. Before putting together the code, one last thing remains: set to NO the snapshot flag, as MPN subscriptions don't have a snapshot.

This is the resulting code for an MPN subscription activation:

LSExtendedTableInfo *tableInfo= [LSExtendedTableInfo extendedTableInfoWithItems:@[item]
  mode:LSModeMerge
  fields:@[@"last_price"@"time"@"pct_change"@"stock_name"]
  dataAdapter:@"QUOTE_ADAPTER"
  snapshot:NO];

LSMPNInfo *mpnInfo= [LSMPNInfo mpnInfoWithTableInfo:tableInfo
  sound:@"Default"
  badge:@"AUTO"
  format:@"Stock ${stock_name} is now ${last_price}"];

// Add custom data to match the notification
// and the corresponding item
mpnInfo.customData@{@"item": item};

// Add category for iOS >= 8.0
mpnInfo.category@"STOCK_PRICE_CATEGORY";

// Activate the MPN subscription
_mpnSubscription= [_client activateMPN:mpnInfo coalescing:YES];

From this moment on, updates for this item will be delivered as mobile push notifications to the device that activated this MPN subscription. Note that MPNs are throttled to a maximum frequency, with a default of 1 notification every 30 seconds per device. For more informations on how to configure the server's MPN Module see the General Concepts document and the extensive comments included in configuration files.

Wondering about that "coalescing" flag? Continue below to know more.

Activating an MPN Subscription on Android

Let's take again a common table subscription and make it an MPN subscription, using the Android client library. An example of table subscription is as follows:

SimpleTableInfo tableInfo= new SimpleTableInfo(item, "MERGE", "stock_name last_price time", true);
tableInfo.setDataAdapter("QUOTE_ADAPTER");

client.subscribeTable(tableInfo, this, false);

On Android, mobile push notifications are not necessarily meant to be shown to the user. They are always received by the app through an intent, which can then decide to show them or process them in another way. For this reason, we incapsulate the table info object in an MPN info object that is quite different from the iOS/OS X version. Here it mainly contains a key-value map, that can be filled in any way:

Map<String, String> data= new HashMap<String, String>();
data.put("stock_name", "${stock_name}");
data.put("last_price", "${last_price}");

MpnInfo mpnInfo= new MpnInfo(tableInfo, "Stock update", data);
mpnInfo.setDelayWhileIdle("false");
mpnInfo.setTimeToLive("300");

We specify the stock name and  the price as key-value pairs of the notification data map. Here too we can use the variable expansion syntax to fill part of the map values with fields from the update. With a stock name of "Anduct" and a price of "2.84", the previous MPN subscription will deliver a notification with the "Anduct" value for the "stock_name" key and "2.84" value for the "last_price" key.

The MPN info object may also contain a trigger expression, similarly to iOS/OS X, but more on this later. Finally, a few more parameters are present to specify how the notification must be delivered:
  • the collapseKey parameter (passed in the constructor in the example above) tells how to group notifications, so that they may be collapsed together in case many of them are pending;
  • the delayWhileIdle flag tells if the notifications can be delayed when the device is in standby;
  • the timeToLive parameter tells for how long the notification may be held if the device is switched off; after this time has passed the notification may be discarded.
Once the MPN info object is set up, we call the activateMpn method in place of the subscribeTable method:

// Activate the MPN subscription
MpnSubscription mpnSubscription= client.activateMpn(mpnInfo, true);

The second parameter of the activateMpn method is the coalescing flag. Continue below to know more about this important flag.

The Coalescing Flag

The activateMPN/activateMpn API call has a parameter called coalescing that tells the server how to behave when a subscription very similar to the one being activated is already present.

Recall that MPN subscriptions are persistent. If you activate them each time the app starts, just as you would do with common table subscriptions, they will accumulate. You end up with multiple notifications sent to the device for each update coming from the adapter. And you don't want this, people go nuts when their phone rings every few seconds.

The coalescing flag is there to avoid this situation. When set to YES/true, this is what happens on the server:
  1. an MPN subscription for the same device and with the same adapter set, data adapter, group (i.e. items), schema (i.e. fields) and trigger expression is searched on the database;
  2. if found, other parameters of the MPN info object (e.g. sound or badge for iOS/OS X, or the data map for Android) overwrite corresponding parameters of the existing subscription;
  3. if not found, a new MPN subscription is created.
If set to NO/false, the server always creates a new MPN subscription.

So, suppose you have a simple app with just one or two fixed MPN subscriptions. For example a chat, or a news feed, that always subscribes to same item. If this is your case, the coalescing flag let's you simplify greatly the MPN subscription process, since you don't have to verify if a previous subscription is present or not. You just activate your MPN subscription each time, sure that the server won't duplicate it.

On the other hand, if your case is more sophisticated and you activate and deactivate MPN subscriptions based on user activity, then the coalescing flag may not be for you, as you may end up with one subscription when the user actually requested for more than one.

Note that client libraries provide APIs to inquire, modify and delete existing MPN subscriptions (more on this later), so you can actually do this update-or-create check by yourself on the client. The coalescing flag is there just to simplify this procedure and guarantee its atomicity.

Triggers

Depending on the data model of your adapters, you may actually want to send an MPN on two different kind of events:
  1. when an update is fired, e.g.: when a news is received, you want to notify its headline to the user;
  2. when an update with a specific content is fired, e.g.: when a stock price raises above a certain threshold, you want to alert the user;
MPN subscriptions described in previous paragraphs fit well for case 1, but what about case 2? Triggers are there for this purpose.

A trigger is a Java language boolean expression that you may add to an MPN subscription before it is activated. This expression is evaluated on the server each time an update fires:
  • until it evaluates to false, nothing will happen: the MPN subscription remains active but no MPN is sent to the device;
  • when it evaluates to true, an MPN is sent to the device and the MPN subscription switches to "triggered" state, which means "inactive".
In other words, MPN subscriptions with a trigger send their MPN at most once. Once sent, they must be reactivated to send it again (if and when the trigger evaluates to true again, that is).

As with text format or data map content, the trigger expression may use values from update fields. For example:

"Double.parseDouble(${last_price}) > 100.0"

Remember that, typically, update fields are strings, convert their value appropriately. So, here is how to set a trigger on an MPN info object for iOS/OS X:

mpnInfo.triggerExpression= @"Double.parseDouble(${last_price}) > 100.0";

And here for Android:

mpnInfo.setTriggerExpression("Double.parseDouble(${last_price}) > 100.0");

You may legitimately wonder if such a code string, evaluated on the server, may pose a security risk. We have taken any possible precaution to avoid risks:
  • the code string is parsed with well known, state-of-the-art libraries (Janino);
  • the obtained code is executed within a class loader that shares nothing with the rest of the server;
  • most important of all, the server may restrict the code by matching its string with a list of regular expressions: if it does not match, the activation is refused;
  • finally, with the default configuration all triggers are refused.
Anyway, if a path comes to your mind that may lead to a server security threat, please let us know as soon as possible. See the General Concepts document and the extensive comments included in configuration files for more informations on how to protect your server.

Managing MPN subscriptions

As anticipated, MPN subscriptions are stored on the server in a specific database. There's no need for an app to store them locally, as there are APIs to inquire them, modify them and deactivate (i.e. delete) them. Let's take a peek at all of them.

Inquiring MPN Subscriptions

With inquires you can download one or more MPN subscriptions from the server to later modify or deactivate them. Each subscription has a unique key, called MPN key, that you can use to inquire a specific MPN subscription. But you may also inquire them collectively by passing an MPN status (active, triggered, etc.), or simply inquire them all.

For example, the following call inquires all the MPN subscriptions that are currently active with iOS/OS X:

NSArray *mpnSubscriptions= [_client inquireMPNsWithStatus:LSMPNSubscriptionStatusActive];

And the following is the same on Android:

List<MpnSubscription> mpnSubscriptions= client.inquireMpn(MpnSubscriptionStatus.Active);

Once MPN subscriptions have been inquired, they are present in the client library cache. Using the cache avoids a server roundtrip. You can access it in any moment with:

NSArray *mpnSubscriptions= [_client cachedMPNSubscriptions];

For iOS/OS X, and for Android:

List<MpnSubscription> mpnSubscriptions= client.getCachedMpnSubscriptions();

Client side caching has a pleasant side effect: MPN subscription object instances are unique and associated with their corresponding MPN key. You will never have two different instances for the same key. This is even more important if you consider coalescing (see above): when an activation coalesces your MPN subscription with a previous one, the activateMPN/activateMpn call will return the previous instance, and not a new (duplicated) one.

We can't be exhaustive in this tutorial, so check the client library documentation for a full list of the available inquire and cache APIs.

Modifying MPN Subscriptions

Once you have an MPN subscription object, whether you obtained it from an activation, an inquire or from the cache, you can modify it by passing a new MPN info object. For example, for iOS/OS X:

[mpnSubscription modify:newMpnInfo];

For Android:

mpnSubscription.modify(newMpnInfo);

A modification requires a roundtrip to the server, of course. A modification may change any parameter of the MPN subscription, except its adapter set. The MPN key is guaranteed to remain unchanged.

Deactivating (Deleting) MPN Subscriptions

Finally, you can deactivate MPN subscription both specifically and collectively. If you have an MPN subscription object, you can deactivate it by simply calling:

[mpnSubscription deactivate];

For iOS/OS X, and for Android:

mpnSubscription.deactivate();

But you may also deactivate all MPN subscriptions that have already triggered:

[_client deactivateMPNsWithStatus:LSMPNSubscriptionStatusTriggered];

For iOS/OS X, and for Android:

client.deactivateMpn(MpnSubscriptionStatus.Triggered);

Deactivating an MPN subscription means deleting it: the database does not keep informations on deactivated subscriptions. Moreover, once the last subscription has been deleted, even informations about the device are deleted (i.e. its device token/registration ID). In this situation, an inquire request may end with an error reporting "device unknown". This is normal: if there are no subscriptions, the server knows nothing about the device.

We know this may sound unusual, considering latest trends on collecting information on unsuspecting users. But you know, style will out. You will be able to write "Device information is collected only for the purpose of service" and really mean it.

Authentication and Authorization

With great power comes great responsibility. So, we have extended the MetadataProvider interface to provide appropriate MPN callbacks. In particular there are now three new methods to be implemented:

public void notifyMpnDeviceAccess(String user
  MpnDeviceInfo device)
  throws CreditsException, NotificationException;

public void notifyMpnSubscriptionActivation(String user, String sessionID
  TableInfo table, MpnSubscriptionInfo mpnSubscription)
  throws CreditsException, NotificationException;

public void notifyMpnDeviceTokenChange(String user
  MpnDeviceInfo device, String newDeviceToken)
  throws CreditsException, NotificationException;

The first method, notifyMpnDeviceAccess, can be used as a generic point to check if a user is entitled to use MPNs. For example, you could check user and device against a blacklist or whitelist.

The second method, notifyMpnSubscriptionActivaiton, is the correct point where to check if a user has access to specific content. In fact, the table info object is passed, you could check the user is entitled to the requested items and fields.

Finally, the last method notifies that a device is requesting a token/registration ID change.

Summing Things Up

A quick recap of what we have seen so far:
  • first of all, the app must register for MPNs;
  • activation of an MPN subscription is similar to a common table subscription, with a few more parameters to be specified;
  • MPN subscriptions may use a trigger to send an MPN only when a specific event occurs;
  • there are extensive APIs to inquire, modify and deactivate MPN subscriptions;
  • finally, the metadata adapter is the right place to authenticate and authorize the user to MPNs.
To develop our Stock List Demo apps (iOS/OS X version, Android version), we found the following startup cycle to be appropriate and easy enough:
  1. register for MPNs, if registration fails disable MPN functionalities;
  2. deactivate all triggered MPN subscriptions;
  3. inquire all active MPN subscriptions.
From this moment on, the app relies on the client library cache, there are no more roundtrips until a new MPN subscription is made. With cached informations we can add the appropriate thresholds to charts and switch the price notification on or off where appropriate.

We then use the MPN APIs in the following way:
  • when enabling the stock price notification, the app uses an activation with coalescing flag;
  • when adding a threshold, the app uses an activation with a trigger;
  • when changing a threshold, the app uses a modification with an updated trigger.
Each point is just one API call, and that makes code clear and maintainable, no need for combined search-delete-add operations. Moreover, the app can make use of the existing data adapter for Stock List demos. The metadata adapter, instead, needs to be extended to verify the correctness of triggers.

Overall, our guess is that these new MPN APIs make adding mobile push notifications to your apps as easy as possible. Consider the usage pattern described above as a suggestion, you may find a different way for your app. Anyway, feel free to take a look at the full source code of our apps for inspiration:
And here is the corresponding metadata adapter:
Hope you found this tutorial useful, any opinion is welcome. And remember to use mobile push notifications wisely.


No comments:

Post a Comment

Note: Only a member of this blog may post a comment.