Saturday, July 9, 2011

C2DM – A 10 minutes recipe

First of all, If you want to deeply understand the issue, visit two places:

  1. The project home page.
  2. A video session from Google I/O 2010.

If you are interested in a quick overview and some code examples, keep reading.

Android Cloud to Device Messaging (C2DM) is a lightweight framework enabling the device to refresh its data from the server using “push” messages.
Instead the device will go to the server periodically, looking for new data, spending precious battery time, the server (which hold the state of the data) simply sends a lightweight message to the device, telling him “dude, I got some new fresh data ready for you, come get it”.

Life cycle:

  • The device sends a registration request to a C2DM server, and gets back a registration ID.
  • The device sends that ID to the application server.
  • The application server sends an authentication request to a C2DM server, and gets back a ClientLogin authorization token.
  • The application server use the authorization token and the registration Id he got from the device, and sends a message to a C2DM server.
  • The C2DM server sends the message to the specific device with the given registration ID. (There’s an option to tell the C2DM server to wait if the device is idle, and also to use a collapse key to prevent flooding the device with similar messages).
* The application on the device doesn’t need to be running to get a message. The C2DM server will wake it up.

Before writing any code, you need to freely sign up to the C2DM service from the signup page.

Now, let's get our hands dirty.

- On the Android side -
  • If you test the following example on the emulator make sure that you use Google API 8 or later. You also have to register a Google user on the virtual device under "Settings" –> "Accounts Sync".
  • If you test the following example on a real device make sure that the Android Market is installed.

Lines need to be added to AndroidManifest.xml:

<manifest package="com.example.myapp" ...>
    <!-- Only this application can receive the messages and registration result -->
    <permission android:name="com.example.myapp.permission.C2D_MESSAGE" 
                android:protectionLevel="signature" />
    <uses-permission android:name="com.example.myapp.permission.C2D_MESSAGE" />
    <!-- This app has permission to register and receive message -->
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <!-- Send the registration id to the server -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
    <application android:label="rich-client" android:icon="@drawable/icon">
        <service android:name=".C2DMReceiver" />
        <!-- Only C2DM servers can send messages for the app. -->
        <!-- If permission is not set - any other app can generate it -->
        <receiver android:name="com.google.android.c2dm.C2DMBroadcastReceiver"
                  android:permission="com.google.android.c2dm.permission.SEND">
            <!-- Receive the actual message -->
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="com.example.myapp" />
            </intent-filter>
            <!-- Receive the registration id -->
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                <category android:name="com.example.myapp" />
            </intent-filter>
        </receiver>
        <!-- Activities -->
        ...
    </application>
    ...
</manifest>


Now, add a package: com.google.android.c2dm and put there the following files: C2DMBaseReceiver, C2DMBroadcastReceiver, C2DMessaging.
(You can find them in the Chrome to Phone example here, under trunk –> android –> c2dm)

Create a class named C2DMReceiver in your root package (com.example.myapp).

public class C2DMReceiver extends C2DMBaseReceiver
{
    public C2DMReceiver()
    {
        super("sender.adress@gamil.com");
    }
    @Override
    public void onRegistered(Context context, String registrationId)
    {
        Log.v("C2DMReceiver-onRegistered", registrationId);
        // send registration id and service/acct name to http server
    }
	
    @Override
    public void onUnregistered(Context context)
    {
        Log.v("C2DMReceiver-onUnregistered", "got here!");
    }
	
    @Override
    public void onError(Context context, String errorId)
    {
        Log.v("C2DMReceiver-onError", errorId);
    }
	
    @Override
    protected void onMessage(Context context, Intent intent)
    {
        Log.i("GenericNotifier", "onMessage called");
        Bundle extras = intent.getExtras();
        String message = (String)extras.get("payload");
        if(message != null)
        {
            Log.i("Message is ", message);
            Log.i("GenericNotifier", message);
        }
    }
}


Add this code to your activity, where you want to register your device to the C2DM service:

public void registerMyAccount()
{
    String registrationId = C2DMessaging.getRegistrationId(this);
    if(registrationId == null || registrationId.equals(""))
    {
        C2DMessaging.register(this, C2DMConfig.C2DM_SENDER);
    }
    else
    {
        // If you don't save the registration ID in the server,
        // and the device is already registered, 
        // you can send the registration Id again here
    }
}

- On the Server side -

Here, you do two things:



  1. Get authentication token from a Google server.
  2. Send a message to a specific device with the registration ID you got from that device.

You can implement the server side however you like, here is a simple implementation:

public class C2DMManager
{
    private static C2DMManager instance = null;
    Map<String, String> authentications;
    private OutputStreamWriter writer;
    private final String AUTHENTICATION_URL = "https://www.google.com/accounts/ClientLogin";
    private final String SEND_MESSAGE_URL = "https://android.clients.google.com/c2dm/send";
    private final static String AUTH = "authentication";
    private static final String UPDATE_CLIENT_AUTH = "Update-Client-Auth";
    public static final String PARAM_REGISTRATION_ID = "registration_id";
    public static final String PARAM_COLLAPSE_KEY = "collapse_key";
    private static final String UTF8 = "UTF-8";
    private C2DMManager()
    {
       authentications = new HashMap<String, String>();
       getAuthentication();
    }
    public static C2DMManager getInstance()
    {
        if(instance == null)
        {
            instance = new C2DMManager();
        }
        return instance;
    }
    private void getAuthentication()
    {
        try
        {
            // Construct data
            String data = URLEncoder.encode("Email", "UTF-8") + "=" + 
                          URLEncoder.encode("your.account@gmail.com", "UTF-8");
            data += "&" + URLEncoder.encode("Passwd", "UTF-8") + "=" +
                          URLEncoder.encode("yourpassword", "UTF-8");
            data += "&" + URLEncoder.encode("accountType", "UTF-8") + "=" +
                          URLEncoder.encode("GOOGLE", "UTF-8");
            data += "&" + URLEncoder.encode("source", "UTF-8") + "=" +
                          URLEncoder.encode("companyName-appName-versionNumber", "UTF-8");
            data += "&" + URLEncoder.encode("service", "UTF-8") + "=" +
                          URLEncoder.encode("ac2dm", "UTF-8");
            URL url = new URL(AUTHENTICATION_URL);
            URLConnection connection = url.openConnection();
            connection.setDoOutput(true);
            writer = new OutputStreamWriter(connection.getOutputStream());
            writer.write(data);
            writer.flush();
            // Read response, and get authentication details
            BufferedReader rd = new BufferedReader(
                new InputStreamReader(connection.getInputStream()));
            String line = "";
            while ((line = rd.readLine()) != null)
            {
                System.out.println("HttpResponse: " + line);
                if(line.startsWith("Auth="))
                {
                    authentications.put(AUTH, line.substring(5));
                }
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
    public void sendMessage(String pRegistrationID)
    {
        try
        {
            String auth_key = authentications.get(AUTH);
            // Send a sync message to this Android device.
            StringBuilder postDataBuilder = new StringBuilder();
            postDataBuilder.append(PARAM_REGISTRATION_ID).append("=")
                           .append(pRegistrationID);
            postDataBuilder.append("&").append(PARAM_COLLAPSE_KEY).append("=")
                           .append("0");
            postDataBuilder.append("&").append("data.payload").append("=")
                           .append(URLEncoder.encode("Lars war hier", UTF8));
            byte[] postData = postDataBuilder.toString().getBytes(UTF8);
            HttpsURLConnection connection = (HttpsURLConnection) new URL(SEND_MESSAGE_URL)
                .openConnection();
            connection.setHostnameVerifier(new CustomizedHostNameVerifier());
            connection.setDoOutput(true);
            connection.setUseCaches(false);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Type",
                                          "application/x-www-form-urlencoded;charset=UTF-8");
            connection.setRequestProperty("Content-Length",
                                          Integer.toString(postData.length));
            connection.setRequestProperty("Authorization", "GoogleLogin auth=" + auth_key);
            OutputStream out = connection.getOutputStream();
            out.write(postData);
            out.close();
            int responseCode = connection.getResponseCode();
 
            // Validate the response code
            if (responseCode == 401 || responseCode == 403)
            {
                System.out.println("C2DM: Unauthorized - need token");
            }
            // Check for updated token header
            String updatedAuthToken = connection.getHeaderField(UPDATE_CLIENT_AUTH);
            if (updatedAuthToken != null && !auth_key.equals(updatedAuthToken))
            {
                authentications.put(AUTH, updatedAuthToken);
            }
            String responseLine = new BufferedReader(new InputStreamReader(
                connection.getInputStream())).readLine();
            if (responseLine == null || responseLine.equals(""))
            {
                throw new IOException("Got empty response from Google AC2DM endpoint.");
            }
            String[] responseParts = responseLine.split("=", 2);
            if (responseParts.length != 2) 
            {
                throw new IOException("Invalid response from Google " + 
                                      responseCode + " " + responseLine);
            }
   
            if (responseParts[0].equals("id")) 
            {
                System.out.println("Successfully sent data message to device: " + responseLine);
            }
 
            if (responseParts[0].equals("Error")) 
            {
                throw new IOException(err);
            }
        } 
        catch (Exception e) 
        {
            throw new RuntimeException(e);
        }
    }
    private static class CustomizedHostNameVerifier implements HostnameVerifier
    {
        public boolean verify(String hostname, SSLSession session)
        {
            return true;
        }
    }
}

That’s all. Simple, isn’t it?
Now try to learn some more about the cool features the C2DM offers (such as send-with-retry, wait if idle, collapse keys etc.).

No comments:

Post a Comment