Thursday, November 4, 2010

C2DM: Sending Messages

This post follows on from my previous post where we called ClientLogin to obtain an authentication key.  The reason why we needed this key is so that we can do something really cool...send out push notifications to our Android applications.  And similar to that post, the code that is provided here should be just enough to produce a result; a real application will need to implement functionality to handle the different responses that the C2DM service can return, as well as handle things like exponential backoff for when the service is unavailable.

The basics of sending a message are really quite simple; all we need to do is send a well formed HTTPS request to https://android.apis.google.com/c2dm/send and pass along the registration ID of the device that we wish to send a message to, our authentication token, and the payload of the message itself.  So just like we did before when obtaining the authentication token, we start off with the HttpURLConnection object:
HttpURLConnection connection = null;
try {
    URL url = null;
    try {
        url = new URL("https://android.apis.google.com/c2dm/send");
    } catch (MalformedURLException e) {
        // Exception handling
    }
    connection = (HttpURLConnection) url.openConnection();
    connection.setDoOutput(true);
    connection.setUseCaches(false);
    connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
    connection.setRequestProperty("Authorization", "GoogleLogin auth=" + token);
} catch (Exception e) { 
    // Exception handling
}
Once again, I'm using HttpURLConnection as my application is going to be hosted using Google App Engine, and HttpsURLConnection is simply not allowed.  Also, notice how we need to send our authentication token; it needs to be prefixed with the String "GoogleLogin auth=".

Next up, we need to set the parameters that we're going to pass to the C2DM service.  For this, we need to include the registration id that the device received from the C2DM service when the device first registered itself (yes, that means the device will also need to pass that id to us as soon as it gets it).  We also need to specify our message payload in the form of key-value pairs, with the key prefixed with "data.":
StringBuilder sb = new StringBuilder();
addEncodedParameter(sb, "registration_id", deviceRegistrationId);
addEncodedParameter(sb, "collapse_key", "goobr.blogspot.com.someKey");
addEncodedParameter(sb, "data.payload1", "payload1 message data");
addEncodedParameter(sb, "data.payload2", "payload2 message data");
addEncodedParameter(sb, "data.anotherPayload", "even more message data");
String data = sb.toString();
One thing to note is the collapse_key parameter.  This is simply a required key that will be used to collapse our messages so that if the device is switched off, it won't get a ton of messages when it comes online again...in this situation, only the last message will actually be received by the device (although C2DM makes no guarantee on message order, so it might not actually be the last message that was sent).  And just like before, we're using the same addEncodedParameter() method to format our parameters into a String:
public static void addEncodedParameter(StringBuilder sb, String name, String value) {
    if (sb.length() > 0) {
        sb.append("&");
    }
    try {
        sb.append(URLEncoder.encode(name, "UTF-8"));
        sb.append("=");
        sb.append(URLEncoder.encode(value, "UTF-8"));
    } catch (UnsupportedEncodingException e) {
        // Exception handling
    }
}
Finally, we come to the actual sending of the message itself, and processing the response:
try {
    DataOutputStream stream = new DataOutputStream(connection.getOutputStream());
    stream.writeBytes(data);
    stream.flush();
    stream.close();

    switch (connection.getResponseCode()) {
    case 200:
        // Success, but check for errors in the body
        break;
    case 503:
        // Service unavailable
        break;
    case 401:
        // Invalid authentication token
        break;
    }
} catch (IOException e) {
    // Exception handling
}
If the service is unavailable, then we need to check for (and honor) any Retry-After header in the response, and failing that, we need to implement exponential backoff (that is, keep doubling our wait time before trying the request again).  Also, even if we get a 200 response code, that doesn't mean that the message went through.  We still need to check the response body for any of the following:
  • Error=QuotaExceeded
  • Error=DeviceQuotaExceeded
  • Error=InvalidRegistration
  • Error=NotRegistered
  • Error=MessageTooBig
  • Error=MissingCollapseKey
On the first two errors, there's not a lot we can do but wait for a while and try again...it just means that too many messages have already been sent.  For the InvalidRegistration and NotRegistered errors, we need to make sure that we stop sending messages to this registration id as the device has either unregistered itself, uninstalled our Android app (uh-oh!), or simply switched off notifications.  And for the final two errors, well we simply need to look at our code and make sure that:
  • the message is 1024 bytes in size or less and
  • we include a collapse_key in the request
One final thing to note is that the response may include an Update-Client-Auth header from time to time, and all this means is that the authentication token that was used to send the message is about to expire, and so the C2DM framework has generated a new one that we should start using instead.

And that's all there is to it really.  See...told you it was really quite simple! Have fun sending push notifications to your Android apps. :)

1 comment: