Push Notifications to PhoneGap Apps using Notification Hubs Integration

PhoneGap (a distribution of the Apache Cordova project) is a free and open source framework that makes it easier to create app for key mobile device platforms using standardized web APIs, HTML and JavaScript. The essence of PhoneGap is that you write your app once in HTML and JavaScript, then deploy this web-based code to native devices (Android, iOS, Windows Phone, etc…). Unlike purely web-driven mobile apps, PhoneGap apps have some access to native resources, such as push notifications, the accelerometer, camera, storage, geolocation, the in-app browser. PhoneGap apps feel a bit like Web apps, but often with the behavior of native device apps.

Azure Mobile Services supports PhoneGap by providing a quickstart project that you can download from the portal. This TodoList sample project supports Android, iOS and Windows Phone 8 devices. To learn how to get started with PhoneGap and Mobile Services, see Get Started with Mobile Services.

This blog post shows how to use Mobile Services and Azure Notification Hubs to send push notifications to a PhoneGap app, particularly the PhoneGap quickstart sample app.

TodoList Notifications Sample

The TodoList Notifications sample project shows you how to work with Notification Hubs from a Mobile Services-enabled PhoneGap app. This sample uses the notification hub associated with the TodoList mobile service. Follow the steps in the readme to make this sample work with a mobile service that belongs to you. At least one device-specific Notification Hubs registration is required to receive notifications. I won’t go into detail on how Notification Hubs works, you can read more about it here.

The sample includes the script NotificationHubs.js, which is a very basic library for registering for notifications and storing registrations on the device to prevent duplicate registrations from being created. Because registrations can become stale, the sample creates or updates a registration each time the app starts. 

The sample uses the following PhoneGap/Cordova plugins:

  • PushPlugin: provides push notification registration functionality.
  • Device: returns device-specific info, like what platform is running
  • (Optional) Console: writes to the platform-specific console—helpful during debugging.

Using the PhoneGap PushPlugin

To register a device with Notification Hubs, the app must get a token from the platform-specific push notification service, in this case APNS (iOS), GCM (Android), MPNS (WP8). To do this, we use the PhoneGap PushPlugin, which knows how to talk to each of these native services to obtain a push token/ID/channel. The following code, from index.js, executes after the app loads to request a registration depending on the platform information returned from the device plugin:

 // Define the PushPlugin.
var pushNotification = window.plugins.pushNotification;

// Platform-specific registrations.
if ( device.platform == 'android' || device.platform == 'Android' ){     
    // Register with GCM for Android apps.
    pushNotification.register(
       app.successHandler, app.errorHandler,
       { 
        "senderID": GCM_SENDER_ID, 
        "ecb": "app.onNotificationGCM" 
        });
} else if (device.platform === 'iOS') {
    // Register with APNS for iOS apps.
    pushNotification.register(
        app.tokenHandler,
        app.errorHandler, {
            "ecb": "app.onNotificationAPN"
        });
}
else if(device.platform === "Win32NT"){    
    // Register with MPNS for WP8 apps.
    pushNotification.register(
        app.channelHandler,
        app.errorHandler,
        {
            "channelName": "MyPushChannel",
            "ecb": "app.onNotificationWP8",
            "uccb": "app.channelHandler",
            "errcb": "app.ErrorHandler"
    });
}

The registered callback function for a specific device type (also in index.js) then takes the token returned by the push notification service and sends it to Notification Hubs for registration. The following is the tokenHandler function that handles iOS registration that returns the APNS device token:

 // Handle the token from APNS and create a new hub registration.
tokenHandler: function (result) {
    if (mobileClient) {
        // Call the integrated Notification Hub client.
        var hub = new NotificationHub(mobileClient);

        // This is a template registration.
        var template = "{\"aps\":{\"alert\":\"$(message)\"}}";

        // (deviceId, ["tag1","tag2"], templateName, templateBody, expiration)
        hub.apns.register(result, null, "myTemplate", template, null).done(function () {
            alert("Registered with hub!");
        }).fail(function (error) {
            alert("Failed registering with hub: " + error);
        });
    }
}

The result passed to the callback is the device token. The registration is a template registration, without any tags (although the register function supports tags).  The register function can also create native registrations, but template registration is definitely the way to go with PhoneGap and it's multiple push platforms.

To prevent duplicate registrations for a given device, registrations are stored in local storage on the device.

Mobile Services REST APIs for Notification Hubs

Both Mobile Services and Notification Hubs provide native client libraries for all major device platforms, .NET, iOS, Android, and even HTML. For example, you can register for notifications from an iOS app by simply calling a register method on the mobile service client. Their is currently one scenario that lacks client library coverage, namely registering for push notifications from an HTML5/JavaScript client. Fortunately, Mobile Services provides its own REST APIs for registering with the associated notification hub. There are three basic REST methods, POST, PUT and DELETE. A new registration is created by a POST request to https://<mobile_service>.azure-mobile.net/push/registrationsids. Success returns an HTTP 201 with the registration ID in the Location header.  This is followed by a PUT request to https://<mobile_service>.azure-mobile.net/push/registrations/<regId> , the message body, which is the actual registration payload being added or updated, looks like the following for a template registration for APNS (iOS):

 {
    platform: "apns" // {"wns"|"mpns"|"apns"|"gcm"}--REQUIRED
    deviceId: "" // unique device token--REQUIRED
    tags: "tag" | ["a","b"] // non-empty string or array of tags (optional)
    templateBody: "{\"aps\":{\"alert\":\"$(message)\"}}" // --OPTIONAL (platform-specific)
    templateName: "myTemplate" // if template registration -- OPTIONAL (REQUIRED if template)
    headers: { } //-- OPTIONAL (used on WNS/MPNS templates)
    expiration: "" // if apns template -- OPTIONAL (used on APNS templates)
}

Success returns an HTTP 204. A response code of 410 indicates the registration ID in the URL is invalid and a new one must be created.

Because the actual registration payload is JSON, the sample basically just stores the registration payload in local storage on the device, retrieve it set or change any fields, and then attach it as the payload in the registration request. The following functions in NotificationHubs.js store and retrieve a registration, respectively:

 var storeInContainer = function (key, object) {
    localStorage.setItem(key, JSON.stringify(object));
};

var getFromContainer = function (key) {

    if (typeof localStorage.getItem(key) === 'string') {
        return JSON.parse(localStorage.getItem(key));
    }
    return undefined;
};

A registration is deleted by sending a DELETE request to https://<mobile_service>.azure-mobile.net/push/registrations/<regId> , and success returns HTTP 200. The following function sends a POST request to create a new registration:

 var createRegistrationId = function (hub) {

    // Variables needed to make the request to the mobile service.
    var method = "POST";
    var uriFragment = "/push/registrationids";

    var deferred = $.Deferred();

    // Send a Notification Hubs registration request to the Mobile Service.
    hub.mobileClient._request(method, uriFragment, null, null, null,
        function (error, response) {
        if (error) {
            console.log("Error: " + error);
            deferred.reject("Error: " + error);
        } else {

            // Get the unique registration ID from the Location header.
            var location = response.getResponseHeader("Location");            
            var regex = /\S+\/registrations\/([^?]+).*/;
            var regId = regex.exec(location)[1];

            // Return the registration ID.
            deferred.resolve(regId);
        }
    });
    return deferred.promise();
}

This function updates an existing registration with the supplied registration payload.

 // Update an existing registration--includes payload.
var updateRegistration = function (hub, regId, registration) {

    // Variables needed to make the request to the mobile service.
    var registrationPayload = buildCreatePayload(registration);
    var method = "PUT";
    var uriFragment = "/push/registrations/" + regId;

    var deferred = $.Deferred();

    // Send a Notification Hubs registration update to the Mobile Service.
    hub.mobileClient._request(method, uriFragment, registrationPayload, null,
        null, function (error, response) {
        if (error) {
            console.log("Error: " + error);
            deferred.reject("Error: " + error);
        } else {
            console.log("Updated registration: " + regId);
            deferred.resolve();
        }
    });
    return deferred.promise();
};

The NotificationHubs.js library also supports deleting a registration, but the sample doesn’t actually use those APIs.

Sending Notifications

Like the other Mobile Services push notification tutorials, this sample sends a notification to all registrations whenever a new item is inserted in the TodoItems table. Here’s the server script code that sends a template notification to all registrations:

 function insert(item, user, request) {
    // Execute the request and send notifications.
   request.execute({
       success: function() {
           // Create a template-based payload.
           var payload = '{ "message" : "New item added: ' + item.text + '" }';
           // Write the default response and send a notification
           // to all platforms.
           push.send(null, payload, {
               success: function(pushResponse){
               console.log("Sent push:", pushResponse);
               // Send the default response.
               request.respond();
               },
               error: function (pushResponse) {
                   console.log("Error Sending push:", pushResponse);
                    // Send the an error response.
                   request.respond(500, { error: pushResponse });
                   }
            });
       }
   });
}

That about covers the interesting parts of the sample. If you have troubles with the registration, check out this handy topic on Debugging Notification Hubs.

Running the App

The complete set of requirements and steps to run the sample are detailed in the sample readme file. Basically, you need to set the Mobile Service URL and application key and the GCM sender ID and then rebuild each platform.

The running program looks like the Mobile Services HTML quickstart app, except that on start an alert is displayed when registration is complete.

image       image

(Shown on a Windows Phone device.)

When a new item is inserted, a notification is raised on each registered device, regardless of the platform.

image

If you have any questions about or find any issues with the sample, please open an issue in GitHub.

Cheers,

Glenn Gailey