Sunday, October 16, 2011

How to send mobile notifications using cURL and C2DM to an Android App


This tutorial explains the simplest possible steps to send notification messages to an Android device using the Google service 'Cloud To Device Messaging', aka 'C2DM'.

This tutorial is organized in the following sections.
1. Architectural Overview:  How C2DM works.
2. Prerequisites:  List of requirements before starting this tutorial.
3. Sign up for C2DM:  This is a human task done with a browser.
4. Android App:  Compile the provided source code into an APK and install it.
5. Generate web requests with cURL.  This simulates the messaging normally implemented in an appserver.
6. Live testing:  Run the app and send notifications.
7. Production notes:  Ideas on building a production-quality app.

Note: Throughout this tutorial, email addresses, passwords, package names, and ID strings are truncated and/or obfuscated for privacy.  You must use your own values.


For more information, see the list of references at the bottom of this article.


1. Architectural Overview

Entitlement:  A person must sign up to use the C2DM service with a Google Mail account.  The java package name of your application is enabled.

Messaging:  Several messages are exchanged between Appserver, Android device, and C2DM to set up notifications.  After that, notification messages may be sent from Appserver to C2DM to device.

The following figure highlights the steps and important data exchanged (click to enlarge):




2. Prerequisites

Before you start, you need the following:
- A Google Mail account.
- A working Android device, version 2.2 or later, with the Android Market installed and account enabled, Internet enabled (I used Wi-Fi only), and Google Talk signed-out.
- Eclipse and the Android SDK installed on your computer.  Know how to compile an APK and install it on your device.  Know how to display log messages using 'adb logcat'.
- cURL installed on your computer.  cURL is a command line tool for sending web requests:  http://curl.haxx.se/


3. Sign up for C2DM

Sign up here:   http://code.google.com/android/c2dm/signup.html 

When I signed up for development purposes, I entered the following:
-  Package name of android app:  <my_package_name>
-  In android market?  No
-  Estimated messages per day:  32
-  Estimated queries per sec:  0-5
-  Additional information:  developer sandbox
-  Contact email: <my_email@gmail.com>
-  Role (sender) account email:  <my_email@gmail.com>
-  Escalation contact:  <my phone number>

The account became active within a few days.


4. Android App

I wrote three extremely simple low-function classes to demonstrate notifications.  All three classes reside in package <my_package_name>.

Copy these three classes into your Eclipse environment with Android SDK.  Edit the package name and email address.  Diff AndroidManifest.xml into yours.  Compile and install the APK on your device.

HelloActivity 

This activity starts the intent which registers the device with C2DM.


package <my_package_name>;


import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;


public class HelloActivity extends Activity {

    /**
     * Hard-coded credentials
     */
    final static String _developerEmail = "<my_email>@gmail.com";

    /**
     * The app starts here.
     * You can view log messages using 'adb logcat'.
     */
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String m = "HelloActivity/onCreate";
        Log.i(m,"Entry.");
        
        TextView tv = new TextView(this);
        tv.setText("Hello, Android");
        setContentView(tv);
        
        // Start an intent to register this app instance with the C2DM service.
  Intent intent = 
            new Intent("com.google.android.c2dm.intent.REGISTER");
  intent.putExtra("app",
            PendingIntent.getBroadcast(this, 0, new Intent(), 0));
        intent.putExtra("sender", _developerEmail);
  startService(intent);
        
        Log.i(m,"Exit.");
    }
}



HelloRegistrationReceiver

This broadcast receiver handles the registration response from C2DM.  It writes the device registration ID from C2DM to log file.

Note: The registration ID string may be several hundred characters in length.


package <my_package_name>;


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;


public class HelloRegistrationReceiver extends BroadcastReceiver {


    /** 
     * Listens for a registration response message from C2DM.
     * Logs the received registration_id string.
     * You can view log messages using 'adb logcat'.
     */
    public void onReceive(Context context, Intent intent) {
String m = "HelloRegistrationReceiver/onReceive";
Log.i(m,"Entry.");

String action = intent.getAction();
Log.i(m,"action=" + action);

if ("com.google.android.c2dm.intent.REGISTRATION".equals(action)) {
  String registrationId = intent.getStringExtra("registration_id");
        Log.i(m,"registrationId=" + registrationId);
        String error = intent.getStringExtra("error");
        Log.i(m,"error=" + error);
    }


     Log.i(m,"Exit.");
    }
}



HelloMessageReceiver

This broadcast receiver handles receipt of notification messages from C2DM.  It writes the message payload to log file.


package <my_package_name>;


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;


public class HelloMessageReceiver extends BroadcastReceiver {


    /** 
     * Listens for a notification message from C2DM.
     * Logs the received message payload.
     * You can view log messages using 'adb logcat'.
     */
    public void onReceive(Context context, Intent intent) {
        String m = "HelloMessageReceiver/onReceive";
        Log.i(m,"Entry.");


        String action = intent.getAction();
        Log.i(m,"action=" + action);


        if ("com.google.android.c2dm.intent.RECEIVE".equals(action)) {
            String payload = intent.getStringExtra("payload");
            Log.i(m,"payload=" + payload);
            String error = intent.getStringExtra("error");
            Log.i(m,"error=" + error);
        }


        Log.i(m,"Exit.");
    }
}



Android Manifest

The following statements for permissions, activity, and receivers were used in this annoying file.


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="<my_package_name>"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="8" />


    <!-- Grant permission for this app to use the C2DM service. -->
    <uses-permission android:name="<my_package_name>.permission.C2D_MESSAGE" />
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <uses-permission android:name="android.permission.INTERNET" />


    <!-- Prohibit other applications from receiving our notifications. -->
    <permission android:name="<my_package_name>.permission.C2D_MESSAGE"
                android:protectionLevel="signature" />
    
    <application android:icon="@drawable/icon"
                 android:label="@string/app_name">
        <activity android:name=".HelloActivity" 
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <receiver android:name=".HelloRegistrationReceiver" 
                  android:permission="com.google.android.c2dm.permission.SEND">
          <intent-filter>
            <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
            <category android:name="<my_package_name>" />
          </intent-filter>
        </receiver>
        
        <receiver android:name=".HelloMessageReceiver"
            android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="<my_package_name>" />
            </intent-filter>
        </receiver>
        
    </application>
</manifest>



The three classes and Android Manifest complete the application.  Compile it and install it.  Don't start it just yet.

5. Generate web requests with cURL

I used cURL to simulate and fully understand the messages which would normally come from an appserver.  Messages are in the following formats:

Registration request

Sent from cURL to C2DM:

curl -v
     -X POST 
     -d "Email=<my_email>@gmail.com" 
     -d "Passwd=<my_password>"
     -d "source=<my_package_name>" 
     -d "accountType=GOOGLE" 
     -d "service=ac2dm"  
     "https://www.google.com/accounts/ClientLogin"

Registration response

Received from C2DM to cURL.

A successful response is 200 OK.

Three string values are returned in a successful response: 'SID', 'LSID', and 'Auth'.  The last value, 'Auth', is the important token string, <my_server_token>.  It is used subsequently to send notification messages through C2DM to the Android device.

Note: The server token string may be several hundred characters in length.


Notification message

Sent from cURL to C2DM:

curl -v
     -X POST
     -H "Authorization: GoogleLogin auth=<my_server_token>"
     -d "registration_id=<my_device_registration_id>" 
     -d "collapse_key=0"  
     -d "data.payload=<my_notification_payload>"
     "https://android.apis.google.com/c2dm/send"    

Notification response

A successful response is 200 OK.

Aside:  A message ID string is returned in a successful response, in the form  'id:0:1318898...'   Its presence provides comfort, but its official use is unknown thus far.


6. Live testing

After doing all the above, signing-up for C2DM, creating an APK, and installing it, I did the following:

Server-side registration request and response

I issued the following web request to C2DM using cURL and got the response:

~/sandbox$ curl -v -X POST -d "Email=<my_email>@gmail.com" -d "Passwd=<my_password>" -d "accountType=GOOGLE" -d "source=<my_package_name>" -d "service=ac2dm"  "https://www.google.com/accounts/ClientLogin"
* About to connect() to www.google.com port 443 (#0)
*   Trying 74.125.73.99... connected
* Connected to www.google.com (74.125.73.99) port 443 (#0)
>
> etc, etc, etc
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Cache-control: no-cache, no-store
< Pragma: no-cache
< Expires: Mon, 01-Jan-1990 00:00:00 GMT
< Date: Mon, 17 Oct 2011 00:28:18 GMT
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Content-Length: 818
< Server: GSE

SID=DQAAALUAAAAHcuF_ZOAiXnq1lFETmlU0pOZAF37oNlGkmXLGlRpS1MOPZlru1lPDvfBveeL...
LSID=DQAAALYAAACYYMPNPLuNwrH8QSRrzGWBmAbDzorzWnNvQBdYfVQZTAjWYDHQ7KIw8fLmPn...
Auth=DQAAALcAAACqNedh6Wg-zIupSaHqAAEWa2foDO9GtoRrNwd0KwVvWmRI9rXhXkCVuAEdiF...
* Connection #0 to host www.google.com left intact
* Closing connection #0
* SSLv3, TLS alert, Client hello (1):
~/sandbox$

I saved the Auth string as my 'server token' by manual copy-paste.

Android device registration

With my Android device connected to my computer via USB cable, I started logging debug messages:

    adb logcat

I started the application.

I found the following messages in the log from class HelloActivity.  These messages indicate that the activity started the registration request processing.

I/HelloActivity/onCreate(16293): Entry.
I/HelloActivity/registerWithC2DM(16293): Entry. Starting registration intent.
I/HelloActivity/registerWithC2DM(16293): Exit. Registration intent started.
I/HelloActivity/onCreate(16293): Exit.

Immediately next in the log, I found these messages from HelloRegistrationReceiver.  They indicate that registration was successful.  I saved the device registration ID string by manual copy-paste.

I/HelloRegistrationReceiver/onReceive(16293): Entry.
I/HelloRegistrationReceiver/onReceive(16293): action=com.google.android.c2dm.intent.REGISTRATION
I/HelloRegistrationReceiver/onReceive(16293): registrationId=gHcSt69NTZleRJ092QQRvegM7lKhkOUx3ngsFvX0G...
I/HelloRegistrationReceiver/onReceive(16293): error=null
I/HelloRegistrationReceiver/onReceive(16293): Exit.


Notification message generation

Back to the command-line, I issued the following web request to C2DM using cURL.  This request includes the server token string and the device registration ID string, manually inserted by copy-paste.

Note the message payload is "Eyeing pretty cURLs with bad intent".

~/sandbox$ curl  -v  -X POST  -H "Authorization: GoogleLogin auth=DQAAALcAAACqNedh6Wg-zIupSaHqAAEWa2foDO9GtoRrNwd0KwVvWmRI9rXhXkCVuAEdiF..."  -d "registration_id=gHcSt69NTZleRJ092QQRvegM7lKhkOUx3ngsFvX0G..."  -d "data.payload=Eyeing pretty cURLs with bad intent"  -d "collapse_key=0"  "https://android.apis.google.com/c2dm/send"
* About to connect() to android.apis.google.com port 443 (#0)
*   Trying 74.125.73.113... connected
* Connected to android.apis.google.com (74.125.73.113) port 443 (#0)
>
> etc, etc, etc...

< HTTP/1.1 200 OK
< Update-Client-Auth: DQAAALcAAABKauk6n7GUTKTvazwHtCBUuSiJU76WzEEwAK...
< Set-Cookie: DO_NOT_CACHE_RESPONSE=true;Expires=Mon, 17-Oct-2011 00:01:01 GMT
< Content-Type: text/plain
< Date: Mon, 17 Oct 2011 00:01:00 GMT
< Expires: Mon, 17 Oct 2011 00:01:00 GMT
< Cache-Control: private, max-age=0
< X-Content-Type-Options: nosniff
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
< Server: GSE
< Transfer-Encoding: chunked

id=0:1398806640216282%45ed4ba940600030
* Connection #0 to host android.apis.google.com left intact
* Closing connection #0
* SSLv3, TLS alert, Client hello (1):
~/sandbox$

Notification message receipt

Within a few seconds, my Android device received the notification message from C2DM.

I found the following messages in the log from class HelloMessageReceiver.  These indicate that the notification message was received.

I/HelloMessageReceiver/onReceive(16641): Entry.
I/HelloMessageReceiver/onReceive(16641): action=com.google.android.c2dm.intent.RECEIVE
I/HelloMessageReceiver/onReceive(16641): payload=Eyeing pretty cURLs with bad intent
I/HelloMessageReceiver/onReceive(16641): error=null
I/HelloMessageReceiver/onReceive(16641): Exit.

Observe that the expected message payload has been received "Eyeing pretty cURLs with bad intent".  This proves the notification system works.

Say 'woo hoo'.

Repeat

I repeated sending notification messages with a different message payload.   I received them succesfully on the Android app.


7. Production Notes

This article has presented an extremely simple low-function example.

Production apps should automatically propagate the device registration ID string from the Android app to the appserver.

Production apps must also add robustness.  For example, client-side Android apps must tolerate and retry or work around a variety of conditions, such as internet access disabled, going into or out of range, or a registration rejection from C2DM.  These may be handled in an Android Service.  Appserver-side apps must handle registration rejections during the initial attempt, as well as re-register gracefully whenever a notification message is rejected because the registration has expired.  And when the client propagates its device registration ID to the appserver, it must handle rejections and retry as well.


References:

All information for this work was learned from the following articles:

- Official doc:  http://code.google.com/android/c2dm/
- I started here:  http://blog.mediarain.com/2011/03/simple-google-android-c2dm-tutorial-push-notifications-for-android/
- More complex and realistic sample Android apps.  Click Source-> Browse-> trunk
  * http://code.google.com/p/jumpnote/
  * http://code.google.com/p/chrometophone/
- Android-side and server-side tutorial (my favorite):  http://www.vogella.de/articles/AndroidCloudToDeviceMessaging/article.html