Mobile Push Notifications with Lightstreamer Server 7.0 and Client SDKs 4.0

Last updated: July 30, 2024 | Originally published: January 11, 2018

With the upcoming release of Lightstreamer 7.0, and its beta 2 already available for you to test, Mobile Push Notifications APIs jump on the Unified Client API model with SDKs version 4.0, employing a new asynchronous model and delivering all the latest features of iOS and Android push notifications.

In this blog post we take a look at the new APIs, with code examples and use cases for both Objective-C and Java.

Note: if you need download and example pointers, jump directly to the end of the post (“Get the SDKs”).

Quick Recap of Mobile Push Notifications

Mobile Push Notifications (or MPN for short) are handled by Lightstreamer as an alternative route for real-time updates. This means that they are backed by a subscription and by a data/metadata adapter pair, like any common real-time subscription. This also means that, if you have already integrated Lightstreamer in your service, adding push notifications to your app is a client-onlytrivial task.
Once an MPN subscription is activatedthe Server, instead of sending updates on the session’s stream connection, forwards them to an MPN service provider appropriate for the client’s platform (APNs for Apple platforms and FCM for Google platforms). MPN subscriptions are stored on an SQL database and consumed directly on the Server, with no need for the app to be online or connected.
The following requirements must be met for Lightstreamer MPN APIs to work properly:

  1. The Lightstreamer Server must have the MPN Module enabled. This also requires an SQL DB available and configured.
  2. The data/metadata adapter pair must expose items in MERGE or DISTINCT modes. COMMAND, RAW and unfiltered modes can’t be used for push notifications, together with the selector property.
  3. On iOS, your app must register for Remote Notifications using common iOS APIs. The Lightstreamer client takes charge once the device token is available.
  4. On Android, your app can obtain the device token using Firebase APIs. As with iOS, the Lightstreamer client takes charge once the device token is available.

The Device Token’s Lifecycle

Depending on the lifecycle of the app and the device it’s installed on, the token may be subject to a number of events, e.g. expiration, invalidation etc. By saving the device token on a local, restorable storage (NSUserDefaults on iOS, SharedPreferences on Android), both the client SDKs ensure they can handle these events automatically.

In particular:

  • App deletion/uninstall: if the app is deleted/uninstalled the token is invalidated. The Server detects an invalid token and suspends sending of push notifications. The suspension lasts for a configurable grace period, during which MPN subscriptions are maintained. If the grace period expires, the token and its subscriptions are deleted.
  • App restore/reinstall on the same device: if the app is restored/reinstalled within the grace period, the Server reactivates the MPN subscriptions and restarts sending push notifications. If it is reinstalled after the grace period, the token is accepted as new.
  • App restore/reinstall on a different device: if the app is restored/reinstalled on a different device (i.e. from a backup), the client detects the token change and sends both the previous and the new token to the Server, which can then move previous MPN subscriptions to the new device and restart sending push notifications.
  • Expiration: if the token expires, the system provides the app with a new token. In this case the client detects the token change and sends both the previous and the new token to the Server, which can then update existing MPN subscriptions and continue sending push notifications.

So, the first step to use the new MPN APIs is to obtain a device token and create an MPN device object.

See the following examples:

Obtaining the Device Token on iOS

Registration for Remote Notifications is usually implemented in the app delegate:

@implementation AppDelegate
    
- (BOOL) application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    // Step 1: Register for user notifications
    UIUserNotificationType types= UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
    UIUserNotificationSettings *mySettings= [UIUserNotificationSettings settingsForTypes:types categories:nil];
    
    [[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
}

- (void) application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
    
    // Step 2: Register for remote notifications
    [application registerForRemoteNotifications];
}

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

    // Step 3: Convert the token to a string
    NSString *token= [[[deviceToken description]
                       stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]
                      stringByReplacingOccurrencesOfString:@" " withString:@""];
    
    /* Step 4: This call will:
     * - Check on LSUserDefaults if a previous token has been saved
     * - Save the current token on LSUserDefaults
     * - Initialize the LSMPNDevice object with the previous and current tokens
     */
    LSMPNDevice *device= [[LSMPNDevice alloc] initWithDeviceToken:token];

    // The LSMPNDevice object is initialized and ready for use
}

- (void) application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    
    // Something went wrong
}

@end

Obtaining the Device Token on Android

You can obtain the token from Firebase:

// Obtain the token from Firebase
FirebaseInstanceId fcm = FirebaseInstanceId.getInstance();
String token = fcm.getToken();

/* This call will:
 * - Check on SharedPreferences if a previous token has been saved
 * - Save the current token on SharedPreferences
 * - Initialize the MpnDevice object with the previous and current tokens
 */
MpnDevice device = new MpnDevice(appContext, token);

// The MpnDevice object is initialized and ready for use

Registering the Device

Once the app has created an MPN device object, it must be passed to the Lightstreamer Server so that it can be registered on its database.

Device registration, like most of the operations in the Unified Client API model, is an asynchronous operation and may be requested immediately after a connection, even if the connection is still ongoing. The request is enqueued and executed as soon as possible.

See the following examples:

Connecting and Registering the Device on iOS

- (void) connectAndRegister:(LSMPNDevice *)device {

    // Create the LSLightstreamerClient and start connection
    _client= [[LSLightstreamerClient alloc] initWithServerAddress:PUSH_SERVER_URL adapterSet:ADAPTER_SET];
    [_client connect];
    [_client addDelegate:self];
    
    // Add device listener and start registration
    [device addDelegate:self];
    [_client registerForMPN:device];
}

#pragma mark - LSMPNDeviceDelegate

- (void) mpnDeviceDidRegister:(nonnull LSMPNDevice *)device {
    
    // Registration succeeded
}

- (void) mpnDevice:(nonnull LSMPNDevice *)device didFailRegistrationWithErrorCode:(NSInteger)code message:(nullable NSString *)message {
    
    // Registration failed
}

Connecting and Registering the Device on Android

void connectAndRegister(MpnDevice device) {
    
    // Create the LSLightstreamerClient and start connection
    client = new LightstreamerClient(pushServerUrl, adapterName);
    client.addListener(this);
    client.connect();
    
    // Add device listener
    device.addListener(new DeviceListener() {

        @Override
        public void onRegistered() {
            
            // Registration succeeded
        }

        @Override
        public void onRegistrationFailed(int code, String message) {
            
            // Registration failed
        }
    });
    
    // Start registration
    client.registerForMpn(device);
}

Creating and Activating an MPN Subscription

Since updates from an MPN subscription are delivered as push notifications, you have to specify how to map the subscription’s fields to the notification payload. With new MPN APIs, you do this by providing directly the JSON structure to be used with the MPN service provider. The structure, that can be set in the notificationFormat property of the MPN subscription, may contain references to fields in the forms ${field_name} (a named argument) or $[field_index] (an indexed argument). These references are replaced by the Server with the content of the update.
To help filling this structure, an MPN builder class has been added that knows the appropriate structure for your MPN service provider and can build the notification format for you. In this way you don’t need to dive into the APNs or FCM message format documentation.
Subscribing to an MPN subscription is again an asynchronous operation: it can be requested immediately after device registration, even if the registration is still ongoing. The request is enqueued and executed as soon as possible.

See the following examples:

Activating an MPN Subscription on iOS

// Use the builder to prepare the notification's JSON structure
LSMPNBuilder *builder= [[LSMPNBuilder alloc] init];
[builder body:@"Stock ${stock_name} is now ${last_price}"];
[builder sound:@"Default"];
[builder badgeWithString:@"AUTO"];
[builder customData:@{@"item": item}];

// Prepare the MPN subscription
LSMPNSubscription *mpnSubscription= [[LSMPNSubscription alloc] initWithSubscriptionMode:@"MERGE" item:item fields:DETAIL_FIELDS];
mpnSubscription.dataAdapter= DATA_ADAPTER;
mpnSubscription.notificationFormat= [builder build];
[mpnSubscription addDelegate:self];

// Activate the MPN subscription
[_client subscribeMPN:mpnSubscription coalescing:YES];

Activating an MPN Subscription on Android

// Prepare the data part of the notification's JSON structure
Map<String, String> data= new HashMap<String, String>();
data.put("item", item);

// Use the builder to prepare the notification's JSON structure
String format = new MpnBuilder()
        .body("Stock ${stock_name} is now ${last_price}")
        .sound("default")
        .data(data)
        .build();

// Prepare the MPN subscription
MpnSubscription mpnSubscription = new MpnSubscription("MERGE", item, detailFields);
mpnSubscription.setDataAdapter(adapter);
mpnSubscription.setNotificationFormat(format);
mpnSubscription.addDelegate(this);

// Activate the MPN subscription
client.subscribe(mpnSubscription, true);

Other Features of MPN Subscriptions

MPN subscriptions are based on real-time subscriptions, and as such they employ similar semantics. E.g. you can specify one item, multiple items or a group ID, a schema name or a list of fields, a maximum frequency, a buffer size etc. 
Additionally to common subscription parameters, and similarly to old MPN APIs, MPN subscriptions also provide two peculiar features:

  • subscription coalescing, and
  • trigger expression.

Coalescing MPN Subscriptions

MPN subscriptions are persistent entities associated with the device. They survive the session, of course, or they could not be able to send push notifications when the app is offline. Hence, your app must be aware if an MPN subscription is already active or not: activating the same MPN subscription multiple times would lead to receiving the same notification multiple times.

However, if you specify the coalescing flag as YES/true during activation, the MPN subscription is coalesced into any existing one that has the same base parameters.
Base parameters are:

  • Adapter set
  • Data adapter
  • Items or group ID
  • Fields or schema name
  • Trigger expression

The coalescing flag comes of help when your app needs a fixed set of MPN subscriptions to be always active. In this simplified case, your app can safely create and activate the MPN subscriptions it needs at startup (specifying the coalescing flag as YES/true) and then forget about them. Even if these subscriptions are activated multiple times, in particular each time the app is started, the Server will keep just one for each set of base parameters.
On the other hand, if your app needs a variable set of MPN subscriptions, you need to verify which is already active and which is not, before activating new ones. More on this later.

Using Trigger Expressions

MPN subscriptions can operate in two different ways:

  • If no trigger expression is specified, which is the default, each update results in a push notification being sent (unless frequency restrictions apply).
  • If a trigger expression is specified, each update is tested against the expression, and only if it evaluates to true a push notification is sent. Moreover, after this push notification, the subscription is suspended.

Trigger expressions are valuable tools in situations where you would need additional logic on the adapter to implement decisions that are specified on the client. Some examples:

  • A trader wants a notification when a stock price raises over a certain threshold.
  • A user wants a notification when their name is cited in a chat room.
  • A recipient wants a notification when a package being delivered gets within a certain distance.

Adding bits of logic like these on the adapter side requires a measurable effort and a rolling restart of the Server, while specifying them as simple expressions on the client-side, directly on the MPN subscription, is trivial.

Trigger expressions must be specified as a string containing a Java language boolean expression, even on the iOS platform. It can include references to fields in the form of named or indexed arguments, evaluated as strings. The expression can be set with the triggerExpression property of the MPN subscription. Syntax verification, compilation and evaluation are performed on the Server.

The examples above could be specified as follows:

  • Stock price above a threshold: Double.parseDouble(${last_price}) > 100.0
  • Name cited in a chat room: ${message}.indexOf(“MyNickName”) >= 0
  • Package coordinates within 7 miles of Manhattan, NY (consider that 1° ≈ 70 miles): Math.sqrt(Math.pow(Double.parseDouble(${lat}) – 40.758896, 2.0) + Math.pow(Double.parseDouble(${long}) – -73.985130, 2.0)) < 0.1

As you see, possibilities are endless.

Managing MPN Subscriptions

Since new MPN APIs are asynchronous, inquiry operations of old MPN APIs are now no more necessary. They have been replaced by events that notify when the list of existing MPN subscriptions is available, and a property on Lightstreamer client object, MPNSubscriptions, to access them.

See the following examples:

Checking Currently Active MPN Subscriptions on iOS

#pragma mark - LSMPNDeviceDelegate

- (void) mpnDeviceDidUpdateSubscriptions:(nonnull LSMPNDevice *)device {
    // List of existing subscription has been updated

    // Search for a subscription on a specific item name stored in _item
    for (LSMPNSubscription *mpnSubscription in _client.MPNSubscriptions) {
        if ([_item isEqualToString:mpnSubscription.itemGroup]) {
            
            // Subscription found
            // [...]
            break;
        }
    }
}

Checking Currently Active MPN Subscriptions on Android

// MpnDeviceListener

void onSubscriptionsUpdated() {
    // List of existing subscription has been updated
    
    // Search for a subscription on a specific item name stored in thisGroup
    List<MpnSubscription> subs = client.getMpnSubscriptions();
    for (MpnSubscription sub : subs) {
        if (sub.getItemGroup().equals(thisGroup)) {
            
            // Subscription found
            // [...]
            break;
        }
    }
}

MPN subscriptions can be modified in place, without the need to first unsubscribe them, by using the copy constructor. The copy inherits the unique ID of the MPN subscription, and when it is submitted for activation through the Lightstreamer client object, the Server uses its properties to update the existing subscription.

See the following examples:

Modifying an Active MPN Subscription on iOS

for (LSMPNSubscription *mpnSubscription in _client.MPNSubscriptions) {
    if ([_item isEqualToString:mpnSubscription.itemGroup]) {

        // Subscription found, copy it and add a trigger expression
        LSMPNSubscription *copy= [[LSMPNSubscription alloc] initWithMPNSubscription:mpnSubscription];
        copy.triggerExpression= @"Double.parseDouble(${last_price}) > 100.0";
        
        // Modify the subscription on the Server
        [_client subscribeMPN:copy coalescing:NO]; // When modifying the coalescing flag is ignored
        break;
    }
}

Modifying an Active MPN Subscription on Android

List<MpnSubscription> subs = client.getMpnSubscriptions();
for (MpnSubscription sub : subs) {
    if (sub.getItemGroup().equals(thisGroup)) {
            
        // Subscription found, copy it and add a trigger
        MpnSubscription copy= new MpnSubscription(sub);
        copy.setTriggerExpression("Double.parseDouble(${last_price}) > 100.0");
        
        // Modify the subscription on the Server
        client.subscribe(copy, false); // When modifying the coalescing flag is ignored
        break;
    }
}

Last but not least, MPN subscriptions may be deactivated singularly, by specifying the MPN subscription object, or collectively, by specifying their status. This second option is useful, for example, to quickly clean up all the MPN subscriptions that are already triggered.

See the following examples:

Deactivating MPN Subscriptions on iOS

// Deactivate single subscription
[_client unsubscribeMPN:mpnSubscription];

// Deactivate all subscriptions in "TRIGGERED" state
[_client unsubscribeMultipleMPN:@"TRIGGERED"];

Deactivating MPN Subscriptions on Android

// Deactivate single subscription
client.unsubscribe(mpnSubscription);

// Deactivate all subscriptions in "TRIGGERED" state
client.unsubscribeMpnSubscriptions("TRIGGERED");

The Metadata Adapter

The metadata adapter maintains its role of authentication and authorization of MPN operations also with Server version 7 and new APIs. The 3 events related to MPN are the following:

// Notification that a certain user is trying to access a certain device
public void notifyMpnDeviceAccess(String user, String sessionID, MpnDeviceInfo device)
        throws CreditsException, NotificationException;

// Notification that a certain user is trying to activate an MPN subscription
public void notifyMpnSubscriptionActivation(String user, String sessionID, TableInfo table, MpnSubscriptionInfo mpnSubscription)
        throws CreditsException, NotificationException;

// Notification that a certain user is trying to change the token of a certain device
public void notifyMpnDeviceTokenChange(String user, String sessionID, MpnDeviceInfo device, String newDeviceToken)
        throws CreditsException, NotificationException;

The purpose of the 3 events is unchanged compared to Server 6.x. The only difference is the addition of sessionID in the method signature and the elimination of platform-specific subclasses of MpnSubscriptionInfo (this object now provides the same notificationFormat property found on the client, subclasses are no more needed).

More in detail:

NotifyMpnDeviceAccess(String user, String sessionID, MpnDeviceInfo device) 

This notification provides a chance to authorize a user to access to a certain device, specified by:

  • Platform: either “Apple” or “Google”
  • Device token
  • App ID, corresponding to the bundle ID for Apple platforms and the package name for Google platforms

For instance, here you can cross-validate the device token with an external database.

NotifyMpnSubscriptionActivation(String user, String sessionID, TableInfo table, MpnSubscriptionInfo mpnSubscription) 

This notification provides a chance to validate the parameters of the MPN subscription, including:

  • Items
  • Fields
  • The notification format
  • The trigger expression
  • The device, specified by platform, token and app ID

For instance, if a certain item must be visible via real-time subscriptions but not via push notifications, this is the correct place to check and block its access.

NotifyMpnDeviceTokenChange(String user, String sessionID, MpnDeviceInfo device, String newDeviceToken)

This notification provides a chance to authorize a token change by a certain user on a certain device. The device is specified by platform, token and app ID.

For instance, here you can cross-validate the new device token with an external database before accepting it.

Documentation and Examples

The General Concepts document, included in the Server distribution, has an updated and expanded chapter 5 dedicated to MPN. Here you can find details about:

  • Server configuration
  • Device and subscription lifecycles
  • Trigger expression specifications
  • Database compatibility
  • Full workflow sequence diagram

Have a look before starting your integration journey.

The MPN Stock-List demos for iOS and Android have also been updated to the new APIs and are available on GitHub:

Their distributions on the corresponding app stores have also been updated and are available for you to test.

As usual, if you have any feedback leave a comment or contact us at support@lightstreamer.com.