Telerik blogs

(This is a guest post by Evgeni Petrov - a Telerik engineer.)

Overview

What are push notifications?

Push notifications are messages sent from an application server to a specific device using the vendor infrastructure. Typically, push notifications are used to notify an application to update its data. For example, push notifications are sent when a feed has been updated, a message has been received, or a new appointment has been made. For more information about push notifications, see Push technology.

Why do we need push notifications?

Push notifications are designed to preserve phone resources. If an app queries the server every 10 seconds, this uses up precious battery life and bandwidth. In fact, an iOS app cannot poll a server continuously because there is no true multitasking. Instead, the user needs to actually start the application to update it.

General overview

There are three components in the life cycle of a push notification: the actual application, the OS vendor servers (or a third-party mediator), and the application server. Here's how it works:

  1. The application registers for push notifications with the OS. (Under the hood, the OS contacts the vendor push servers and requests a token.)
  2. The OS returns the token to the application.
  3. The token is sent to the application server.
  4. The application server uses the token to send push notifications to the device.

Let's Bring Push Notifications to Life

With the Cordova-based PushPlugin plugin, we will develop the push notification module of a promotional app. The sample app is designed to run on iOS and Android, and to display promotional text in a single paragraph. To run and test the app, we will also create a simple application server and a web form that sends verbatim promotional text to the application. We will be using the Apple Push-Notification Service (APN) for iOS, and Google Cloud Messaging (GCM) for Android.

Bear this in mind – because push notifications are unreliable, you should avoid using them to transport data. Instead, use them to let your app know that an update is needed.

Application Server

Let's start our project by creating an application server with Node.js. (Node.js is becoming more and more popular and you can check out why here.) This sample application server was tested on Windows 7 64-bit and Mac OS 10.7 with the latest stable Node.js installed – but you should be able to run it on any OS that supports Node.js.

Here are the requirements for our server:

  • Accept token from device and store it for later use.
  • Host and run a very simple web form to send messages. (NOTE: To keep the code as simple and clear as possible, we will not add any security features.)
  • Send push notifications for new promotions to all registered devices.
  • Store promotional text submitted with the web form.

Our simplified HTTP server will listen on port 80 and answer requests with status code 200 and an empty body. (Just browse to localhost in your favorite browser and look at the node process console output.)

NOTE:

  • Turn off IIS, if running.
  • Running Node.js is as simple as running node server.js in the command line.
  • Running on ports below 1024 may require admin/root privileges, so you can alter the port as needed, or use sudo, etc. to run the server.js module below.
var http = require('http'); //include http module
var url = require('url'); //include url module

var allDevices = []; //variable will hold all registered devices

http.createServer(function(request, response) { //the callback is called for each HTTP request received
    var currentDate = new Date(); //log received requests
    console.log("#Received request. " + currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds());
    request.setEncoding('utf8'); //set encoding to utf8
    response.writeHead(200, {
        'Content-Type': 'text/plain'
    }); //return 200 with text/plain MIME type
    response.end(); //end the response
}).listen(80); //start the server on port 80

console.log('Server running at http://127.0.0.1:80'); //log that the server is running 

Before trying to communicate with any devices, let's get the dirty work done and create form.html - our simple web form:

<!DOCTYPE html>
<html>
<head>
<title>Send Message to All Devices</title>
</head>
<body>
<h2>Send Message to All Devices</h2>
<form action="setPromotion" method="post">
<label>Message:</label>
<input type="text" name="text"></input>
<input type="submit" value="Submit">
</form>
</body>
</html>

Next, we need to configure how our server handles the form. In our project, the server sends the form when called without a path. When the server is called with the "/setPromotion" path, the server responds with the promotional text that is currently stored in the form:

var http = require('http');
var url = require('url');
var fs = require('fs'); //include fs module


var allDevices = [];
var promotionText; //variable will hold the promotional text

//the function will return the web form named form.html
function returnForm(response) {
    fs.readFile('form.html', function(err, html) {
        response.writeHeader(200, {
            "Content-Type": "text/html",
            "Content-Lenght": html.length
        });
        response.write(html);
        response.end();
    });
}

//the function is called when the form is submitted
function setPromotion(request, response) {
    request.on('data', function(data) { //on "data" event, read the contents of the form
        promotionText = decodeURI(data.slice(data.indexOf("=") + 1));
        promotionText = promotionText.replace(/\+/g, ' '); //remove old encoding of + with spaces
        //notify devices with push notification that a promotion is available
    })
    response.writeHeader(200, {
        "Content-Type": "text/html"
    });
    response.write("Successfully added to queue."); //display confirmation message for the submission
    response.end();
}

http.createServer(function(request, response) {
    var currentDate = new Date();
    console.log("#Received request. " + currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds());

    request.setEncoding('utf8');

    var parsedUrl = url.parse(request.url); //parse the url and use switch to create routes
    switch (parsedUrl.href) {
        case "/":
            returnForm(response);
            break;
        case "/setPromotion":
            setPromotion(request, response);
            break;
        default:
            response.writeHeader(200);
            response.end();
            break;
    }
}).listen(80);

console.log('Server running at http://127.0.0.1:80'); 

In your browser, you can test to see if the form appears and if using it correctly persists the message to the promotionText variable on the server. To track errors, you can add console.log anywhere you need it in the code.
The next step is to implement adding and removing devices, and querying for promotional text.

In the code below, we add routes for device registration and for retrieving promotional text. We also register the "/getPromotion" route that serves our promotion in JSON format.
To register, devices send their device token and platform in JSON format to the server.

var http = require('http');
var url = require('url');
var fs = require('fs');

var allDevices = [];
var promotionText;

function returnForm(response) {
    fs.readFile('form.html', function(err, html) {
        response.writeHeader(200, {
            "Content-Type": "text/html",
            "Content-Lenght": html.length
        });
        response.write(html);
        response.end();
    });
}

function setPromotion(request, response) {
    request.on('data', function(data) {
        promotionText = decodeURI(data.slice(data.indexOf("=") + 1));
        promotionText = promotionText.replace(/\+/g, ' ');
    })
    response.writeHeader(200, {
        "Content-Type": "text/html"
    });
    response.write("Successfully added to queue.");
    response.end();
}

//the function will be used for device registration
function registerDevice(request, response) {
    request.on('data', function(data) {
        var device = JSON.parse(data); //parse the message sent from the device
        for (var i = allDevices.length - 1; i >= 0; i--) {
            if (allDevices[i].token == device.token) //if device is already registered, do not register it
            return;
        };
        allDevices.push(device);
        console.log("#Registered device with platform:" + device.platform);
    })
    response.writeHeader(200, {
        "Content-Type": "application/json"
    });
    response.write(JSON.stringify({
        success: true
    })); //send simple success
    response.end();
}

//return the promotional text wrapped inside a json object
function getPromotion(request, response) {
    var responseObject = {
        text: promotionText
    };
    var responseString = JSON.stringify(responseObject);
    response.writeHeader(200, {
        "Content-Type": "application/json",
        "Content-Lenght": responseString.length
    });
    response.write(responseString);
    response.end();
}

http.createServer(function(request, response) {
    var currentDate = new Date();
    console.log("#Received request. " + currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds());

    request.setEncoding('utf8');

    var parsedUrl = url.parse(request.url);
    switch (parsedUrl.href) {
        case "/":
            returnForm(response);
            break;
        case "/setPromotion":
            setPromotion(request, response);
            break;
        case "/register":
            registerDevice(request, response);
            break;
        case "/getPromotion":
            getPromotion(request, response);
            break;
        default:
            response.writeHeader(200);
            response.end();
            break;
    }

}).listen(80);

console.log('Server running at http://127.0.0.1:80'); 

Now let's start developing some of the device-side functionality, so that we can check if our application server is implemented correctly.

App code

Let's begin by creating a new jQuery project in Graphite. From Project Properties, we'll add PushPlugin to the project and include a reference to PushNotification.js in index.html.
Then, we create a file named push.js in the js folder and include a reference to it in index.html.
Here is how the script tags in your index.html should look:

<script src="cordova.js" type="text/javascript"></script>
<script src="Plugins/PushPlugin/PushNotification.js"></script>
<script src="jquery-mobile/jquery-1.8.2.min.js" type="text/javascript"></script>
<script src="jquery-mobile/jquery.mobile-1.2.0.min.js" type="text/javascript"></script>
<script src="http://maps.google.com/maps/api/js?sensor=true"></script>
<script src="js/hello-world.js" type="text/javascript"></script>
<script src="js/push.js" type="text/javascript"></script>

Because there are differences in the code for iOS vs Android, we will implement the push notifications functionality separately.

iOS

  • First, we get the local value of the Push Notification JS object.
  • Next, we create success and failure functions for getting the APN token. We will send the token to our application server.
  • Then, we will subscribe to device ready event and register to get badges, sounds, and alerts for our application.
  • We also tell the PushPlugin to notify us when we get a notificationon by invoking pushCallbacks.onNotificationAPN . We will implement this later when we have actual push notifications to handle.
//wrap the entire functionality in a function to create a namespace
(function() {
    var pushNotification = window.plugins.pushNotification;

    var apnSuccessfulRegistration = function(token) {
        alert("Successfully got a token:" + token);
    }

    var apnFailedRegistration = function(error) {
        alert("Error: " + error.toString());
    }

    var deviceReady = function() {
        if (device.platform == "iOS") {
            pushNotification.register(apnSuccessfulRegistration,
            apnFailedRegistration, {
                "badge": "true",
                "sound": "true",
                "alert": "true",
                "ecb": "pushCallbacks.onNotificationAPN" //tell PushPlugin to call onNotificationAPN of global pushCallbacks object
            });
        }
    }
    //subscribe to "deviceready" event with a local function
    document.addEventListener("deviceready", deviceReady, false);

}());

Now if you run the application on your device or in the simulator, you should get something similar to this:

This occurs when the application is not signed with an AppID with enabled push notifications.

In our case, we do need to create a brand new AppID for the application. (For more information, click here).

Here, we can add a new AppID with com.icenium.PromotionalApp as a bundle identifier (or you can use whatever bundle identifier you like). We are not done with the AppID yet – we need to enable push notifications for it. Find it in the list and click "Configure". Enable push notifications and click "Configure" for Development Push SSL Certificate.

To set the newly created AppID for the sample application in Graphite, go to Project Properties and modify Application Identifier.

Next, we need to create a certificate signing request (CSR) for a SSL certificate. This will be used to connect our application server to the Apple server. Let's head back to Graphite and create a CSR from Options > General > Certification Management. To generate a certificate signing request (CSR), click the Create button and select Certification Request. Next, save the CSR file on the disk, upload it to the Apple web site, and download the certificate that Apple generates. Back in Graphite, we can now complete the pending request in Options > General > Certification Management by uploading the newly created certificate. The certificate is used to connect to the Apple Push Notification Service (APN).

Now, we will create a specific provision for our application with the new AppID. We will go here and create a new developer provision with our newly created AppID. You might need to hit Refresh a couple of times before the provision becomes active.

Once we have a new developer provision, we need to download it, and in Graphite, import it from Options > Mobile > iOS.
In Project Properties > iOS, we can set the codesigning identity to our newly created provision.

The next step is to finally get the device token:

In our project, we send this token to the application server, get registered for notifications, and POST an AJAX request to the server with the token. We also implement the onNotificationAPN callback which is called when a push notification is received.
In the example, the IP address belongs to my local machine. To get your IP, type ipconfig in the command line if you are running Windows, or use ifconfig if you are running Mac OS.

(function() {
    var server = "http://192.168.56.25";

    //make ajax post to the application server to register device
    var sendTokenToServer = function sendTokenToServer(token) {
        $.ajax(server + "/register", {
            type: "post",
            dataType: 'json',
            data: JSON.stringify({ //JSON object with token and the device platform
                token: token,
                platform: device.platform
            }),
            success: function(response) {
                console.log("###Successfully registered device.");
            }
        });
    }

    //the function adds callbacks for PushPlugin
    //the function uses global object "pushCallbacks"
    var addCallback = function addCallback(key, callback) {
        if (window.pushCallbacks === undefined) {
            window.pushCallbacks = {}
        }
        window.pushCallbacks[key] = callback;
    };

    var pushNotification = window.plugins.pushNotification;

    var apnSuccessfulRegistration = function(token) {
        sendTokenToServer(token.toString(16));
        addCallback('onNotificationAPN', onNotificationAPN);
    }

    var apnFailedRegistration = function(error) {
        alert("Error: " + error.toString());
    }

    //the function is a callback when we receive notification from APN
    var onNotificationAPN = function(e) {
        //get promotional text from the application server

        //here we can set a badge manually, or play sounds
        //for more information about PushPlugin, see https://github.com/phonegap-build/PushPlugin
    };

    var deviceReady = function() {
        if (device.platform == "iOS") {
            pushNotification.register(apnSuccessfulRegistration,
            apnFailedRegistration, {
                "badge": "true",
                "sound": "true",
                "alert": "true",
                "ecb": "pushCallbacks.onNotificationAPN"
            });

        }
    }
    document.addEventListener("deviceready", deviceReady, false);
}()); 

If everything has worked, in your node.js console, you should see output confirmation for the registered device.

Android

For the Android implementation, we will start by creating a Google API project. You can find the tutorial for that part here. Once we have created our API project, we use its project number to request a token. In our application code, we need to add a function to get the actual promotional text when we receive a push notification. Once we have the promotional text, we will add it to a paragraph on the landing page of our jQuery starting project. Here is how we enhance push.js with the described functionality:

(function() {
    var server = "http://192.168.56.25";

    //add promotion text to the paragraph designed for it
    var addPromotionToDOM = function addPromotionToDOM(promotionText) {
        $("#promotionParagraph").text(promotionText);
    }

    //get promotion text from the application server
    var getPromotionFromServer = function getPromotionFromServer() {
        console.log('### getting promotion text.');
        $.ajax(server + "/getPromotion", {
            type: "post",
            dataType: 'json'
        }).done(function(data) {
            console.log("### got promotion text.");
            addPromotionToDOM(data.text);
        });
    }

    var sendTokenToServer = function sendTokenToServer(token) {
        //make ajax post to the application server to register device
        $.ajax(server + "/register", {
            type: "post",
            dataType: 'json',
            data: JSON.stringify({
                token: token,
                platform: device.platform
            }),
            success: function(response) {
                //log that the device was registered successfully
                console.log("###Successfully registered device.");
            }
        });
    }

    var addCallback = function addCallback(key, callback) {
        if (window.pushCallbacks === undefined) {
            window.pushCallbacks = {}
        }
        window.pushCallbacks[key] = callback;
    };

    var pushNotification = window.plugins.pushNotification;

    var apnSuccessfulRegistration = function(token) {
        sendTokenToServer(token.toString(16));
        addCallback('onNotificationAPN', onNotificationAPN);
    }

    var apnFailedRegistration = function(error) {
        alert("Error: " + error.toString());
    }

    //the function is a callback when a notification from APN is received
    var onNotificationAPN = function(e) {
        getPromotionFromServer();
    };

    //the function is a callback for all GCM events
    var onNotificationGCM = function onNotificationGCM(e) {
        switch (e.event) {
            case 'registered':
                if (e.regid.length > 0) {
                    //your GCM push server needs to know the regID before it can push to this device
                    //you can store the regID for later use here
                    console.log('###token received');
                    sendTokenToServer(e.regid);
                }
                break;
            case 'message':
                getPromotionFromServer();
                break;
            case 'error':
                alert('GCM error = ' + e.msg);
                break;
            default:
                alert('An unknown GCM event has occurred.');
                break;
        }
    }

    var deviceReady = function() {
        if (device.platform == "iOS") {
            pushNotification.register(apnSuccessfulRegistration,
            apnFailedRegistration, {
                "badge": "true",
                "sound": "true",
                "alert": "true",
                "ecb": "pushCallbacks.onNotificationAPN"
            });

        } else {
            //register for GCM
            pushNotification.register(

            function(id) {
                console.log("###Successfully sent request for registering with GCM.");
                //set GCM notification callback
                addCallback('onNotificationGCM', onNotificationGCM);
            },

            function(error) {
                console.log("###Error " + error.toString());
            }, {
                "senderID": "921067258193",
                "ecb": "pushCallbacks.onNotificationGCM"
            });
        }
    }
    document.addEventListener("deviceready", deviceReady, false);
}()); 

On line 106 you need to replace the sender ID with your project number. Now, let's return to our application server for the final touches.

For this to work we also need to set the promotionParagraph ID to the landing paragraph of the application. Here is a snippet from index.html:

<div data-role="page" id="home">
<div data-role="header">
<h1>Hello, World!</h1>
</div>
<div data-role="content">
<h1>Welcome to Icenium!</h1>
<p id="promotionParagraph"> <!-- paragraph contains promotional messages -->
Icenium&trade; enables you to build cross-platform device applications regardless of your
development platform by combining the convenience of a local development toolset with the
power and flexibility of the cloud.
</p>
</div>
<div class="nav">
<ul data-role="listview">
<li><a href="#page1" data-transition="slide">UI Interaction Example</a></li>
<li><a href="#page2" data-transition="slide">Geolocation Example</a></li>
</ul>
</div>
<div data-role="footer" data-position="fixed" data-id="oneFooter">
<span class="footerText">Built with Icenium&trade;</span>
</div>
</div>

Application server

To send our push notifications, we will use two open source modules for Node.js: node-apn and node-gcm. Again, we will add the code separately for iOS and for Android.
Let's start by installing the two open source modules. Go to the folder where your server js file is. In the command line, type npm install apn and npm install gcm. If everything is OK, you should see two folders in the folder where your server js file is: ./node_modules/apn and ./node_modules/gcm.

Our application server needs to establish an SSL connection with the push notificaion servers of Apple. For this, we will use the private key and certificate that we created earlier for our AppID. We need to create PEM files for the certificate and the private key. You can read more here. Go to Options > General > Certification Management, select the cryptographic identity that we created earlier and export it. For the next part we will need to do some format conversion with OpenSSL.

Creating PEM files from certificate and p12 file

If you're running Windows, download OpenSSL for Windows from here. Note that Microsoft Visual Studio 2008 C++ Distributable is required for OpenSSL under Windows. Mac OS should have OpenSSL out of the box.
To create the required PEM files, follow the instructions found here.

Now copy the PEM files you created to the folder where your server js file is. In our example, we have named them cert.pem and key.pem. Here's what we need to add to our server code:

NOTE: Make sure that port 2195 is not filtered by your system administrator.

var apns = require('apn');
var options = {
    cert: 'cert.pem',
    /* certificate file path */
    certData: null,
    /* string or buffer containing certificate data; if supplied, uses this instead of certificate file path */
    key: 'key.pem',
    /* key file path */
    keyData: null,
    /* string or buffer containing key data; if supplied, uses this instead of key file path */
    passphrase: null,
    /* passphrase for the key file */
    ca: null,
    /* string or buffer of CA data to use for the TLS connection */
    pfx: null,
    /* file path for private key, certificate, and CA certificates in PFX or PKCS12 format. If supplied, this will be used instead of certificate and key above */
    pfxData: null,
    /* PFX or PKCS12 format data containing the private key, certificate, and CA certificates. If supplied, this will be used instead of loading from disk. */
    gateway: 'gateway.sandbox.push.apple.com', // 'gateway.push.apple.com',/* gateway address */
    port: 2195,
    /* gateway port */
    rejectUnauthorized: true,
    /* value of rejectUnauthorized property to be passed through to tls.connect() */
    enhanced: true,
    /* enable enhanced format */
    errorCallback: undefined,
    /* callback when error occurs function(err,notification) */
    cacheLength: 100,
    /* number of notifications to cache for error purposes */
    autoAdjustCache: true,
    /* whether the cache should grow in response to messages being lost after errors */
    connectionTimeout: 0 /* the duration (in milliseconds) the socket should stay alive with no activity. 0 = Disabled. */
};
var apnsConnection = new apns.Connection(options);

apnsConnection.on('error', function(error) {
    console.log('Error with APN ' + error);
});

apnsConnection.on('transmitted', function(notification) {
    console.log('Notification sent to APN ' + notification.toString());
});

apnsConnection.on('connected', function() {
    console.log('Connected to APN.');
});

apnsConnection.on('disconnected', function() {
    console.log('Disconnected from APN.');
});

apnsConnection.on('transmissionError', function(errorCode, notification) {
    console.log("Transmision error with APN. Error code " + errorCode + " notification: " + notification.toString());
}); 

Finally, it's time for some push notifications. Let's add a function that lists all iOS devices and sends them a generic push notification that a new promotion is available.

function notifyDevices() {
	var textMessage = "New promotion available.";
	for (var i = allDevices.length - 1; i >= 0; i--) {
		if (allDevices[i].platform == "iOS") {
			var token = allDevices[i].token;

			var device = new apns.Device(token);
			var note = new apns.Notification();
			note.expiry = Math.floor(Date.now() / 1000) + 3600*24; //set to expire 24 hours after the notification is sent
			note.badge = 1;
			note.alert = textMessage;
			note.device = device;

			apnsConnection.sendNotification(note);
		}
	};
}

We need to call this function when we receive new promotional text.

function setPromotion(request, response) {
  request.on('data', function(data) {
		promotionText = decodeURI(data.slice(data.indexOf("=") + 1));
		promotionText = promotionText.replace(/\+/g, ' ');
		notifyDevices(); //ping all devices that a new promotion is available
	})
	response.writeHeader(200, { "Content-Type": "text/html"} );
	response.write("Successfully added to queue.");
	response.end();
}

Now, iOS devices should display the promotional text in the landing page paragraph. We just need to add the code for Android devices to our project, and we are done.

To send messages to GCM (Google Cloud Messaging), we use the API key for the Google API project we created earlier. You can find your own API key here, in the API Access tab. The final touches to our server code look like this:

var http = require('http');
var url = require('url');
var fs = require('fs');

//GCM implementation
var GCM = require('gcm').GCM;
var apiKey = 'AIzaSyBjXSsrENe404ICODKEMtCdyYuOsuqLzbU'; //replace with the API key for your Google API project
var gcm = new GCM(apiKey);
//

var apns = require('apn');

The only thing left is to enumerate all Android tokens and send push notifications to them. To do that, we need to modify the notifyDevices() function.

function notifyDevices() {
  	var allAndroidTokens = [];
	var textMessage = "New promotion available.";
	for (var i = allDevices.length - 1; i >= 0; i--) {
		if (allDevices[i].platform == "iOS") {
			var token = allDevices[i].token;

			var device = new apns.Device(token);
			var note = new apns.Notification();
			note.expiry = Math.floor(Date.now() / 1000) + 3600*24; //set to expire 24 hours after the notification is sent
			note.badge = 1;
			note.alert = textMessage;
			note.device = device;

			apnsConnection.sendNotification(note);
		} else {
			allAndroidTokens.push(allDevices[i].token); //for Android devices, remember the token
		}
	};

	if (allAndroidTokens.length == 0)
		return;

	var message = {
	    'registration_id' : allAndroidTokens, 
	    'data.message' :  textMessage,
	    'data.msgcnt' : 0,
	};

	gcm.send(message, function(err, messageId){ //send message to all registered Android devices 
	    if (err) {
	        console.log("Something has gone wrong with GCM send!");
	    } else {
	        console.log("Sent with message ID: " + messageId + " on GCM.");
	    }
	});
}

Conclusion

It was a long road but if you are reading this you are ready to create some amazing apps using push notifications. Keep in mind that the implementation here is very basic and you can do a lot more to make it production ready. For example, you can use the APN feedback service to keep track of undelivered messages and identify unreachable devices. A few more ideas: fetch promotions when app starts, unsubscribe from promotions, and so on. 


Evgeni Petrov

About the Author
is one of the mobile guys at Icenium. He has been involved in numerous mobile project throughout his career. He's passionate about UX and Developer Tools. When he's not looking at the computer monitor, he loves to play drum solos. Evgeni works as a Software Developer @ Telerik.


Related Posts

Comments

Comments are disabled in preview mode.