
Здравствуйте, читатели Хабрахабр!
В данном посте я хочу уделить внимание сервису C2DM от компании Google и попытаюсь рассказать, как реализовать поддержку данного сервиса в вашем Android-приложении. Напомню, что C2DM — это специальный сервис, предоставляющий API для отправки сообщений приложениям, установленным на устройствах Android. Использование данного сервиса является незаменимым способом при необходимости передать сообщение пользовательскому приложению, зарегистрированному в системе, но не являющемуся на данный момент активным.
Хотя C2DM является одной из фундаментальных возможностей платформы Android, информации о нем в рунете мало. Попытка изменить данную ситуацию и является одной из задач этого поста.
Под катом я расскажу, как написать простые клиентское и серверное приложения, покажу некоторые «подводные камни», а также дам ссылки на примеры кода.
Cloud to Device Messaging
Но пока немного теории. Как я написал выше, C2DM это сервис дост��вки сообщений от пользовательских приложений к приложениям Android, подробнее можно прочитать здесь. Т.е. это некий аналог Push Notification, если говорить в терминах Apple. Общая схема взаимодействия показана на этой, найденной в интернете, картинке:

Из схемы видны три основные части:
- C2DM сервис. «Облако» от Google, отвечающее за доставку сообщений. А также регистрацию\разрегистрацию устройств и аутентификацию.
- Клиентская часть. Приложение Android, принимающее сообщения.
- Серверная часть. Клиентское приложение («3rd party server» в терминах Google), осуществляющее посылку сообщений.
- Android приложение регистрируется в C2DM, тем самым сообщает о своей готовности принимать сообщения, и получает Registration ID. Registration ID — это уникальный идентификатор устройства, зная который, серверная часть может отправить сообщение получателю. При регистрации нужно указать имя гугловской учетной записи, но об этом я напишу позже, когда буду рассматривать клиентскую часть.
- Приложение передает Registration ID серверной части, чтобы та знала кому можно отсылать сообщения.
- Серверная часть авторизуется на сервере Google, использую ту же учетную запись, что и Android приложение при регистрации в C2DM, и получает Auth Token. Подробно про авторизацию\аутентификацию в Google'вских сервисах написано здесь.
- Зная Registration ID и Auth Token серверная часть посылает сообщение Android-приложению.
Клиентская часть
Для использования C2DM Android устройство обязательно должно удовлетворять следующим требованиям:
- Оно должно быть версии Android 2.2 или выше. На предыдущих версиях C2DM не поддерживается!
- Должна быть любая рабочая Google'вская учетная запись. Если у вас работает Android Market, значит, она у вас есть.

Примечание: Если у вас нет Android 2.2 можно воспользоваться эмулятором, только нужно использовать не стандартный SDK Platform, а Google APIs (API 8 или выше), т.к. в стандартном SDK нет поддержки Google-аккаунтов.
После того как вы создали приложение, его необходимо зарегистрировать на специальном сайте, на котором нужно указать имя пакета (package name), а, самое главное, почту Google'вского аккаунта, который будет испо��ьзоваться для пересылки сообщений. Именно эту почту мы и будем использовать в клиенте при регистрации и на сервере при аутентификации. Должно придти письмо с подтверждением регистрации, после этого письма указанную учетную запись можно использовать для работы с C2DM.
Далее приступаем к модификации нашего «Hello, World!» приложения. Нам нужна реализация кода, отвечающего за регистрацию\разрегистрацию, и, конечно же, прием сообщений. Самим его написать с первой раза действительно сложно т.к. трудно учесть все нюансы, поэтому мы воспользуемся кодом, который любезно, в качестве примера, предоставляет сам Google. Качаем готовый набор классов для работы с C2DM на стороне клиента из svn хранилища. И добавляем их в проект, в директорию "src\com\google\android\c2dm". Должно получиться вот так:

Теперь нужно реализовать методы абстрактного класса C2DMBaseReceiver, для этого напишем класс C2DMReceiver, пока только логирующий вызовы, и поместим его рядом с Main. Содержимое класса C2DMReceiver:
- package com.home.c2dmtest;
- import com.google.android.c2dm.C2DMBaseReceiver;
- import android.app.Notification;
- import android.app.NotificationManager;
- import android.app.PendingIntent;
- import android.content.Context;
- import android.content.Intent;
- import android.util.Log;
- public class C2DMReceiver extends C2DMBaseReceiver {
- public C2DMReceiver(){
- super("<yourmail>@gmail.com");
- }
- @Override
- public void onRegistered(Context context, String registrationId) {
- Log.w("onRegistered", registrationId);
- }
- @Override
- public void onUnregistered(Context context) {
- Log.w("onUnregistered", "");
- }
- @Override
- public void onError(Context context, String errorId) {
- Log.w("onError", errorId);
- }
- @Override
- protected void onMessage(Context context, Intent intent){
- Log.w("onMessage", "");
- }
- }
Но приложение пока не готово для работы с C2DM, т.к. не заданы необходимые права и не зарегистрирован BroadcastReceiver. Для исправления этого нужно модифицировать AndroidManifest.xml, как это сделать можно прочитать здесь. Для моего примера файл выглядит так:
- <?xml version="1.0" encoding="utf-8"?>
- <manifest xmlns:android="schemas.android.com/apk/res/android"
- android:versionCode="1"
- android:versionName="1.0" package="com.home.c2dmtest">
- <application
- android:debuggable="true"
- android:label="@string/app_name">
- <activity android:name="Main"
- android:label="@string/app_name"
- android:theme="@android:style/Theme.NoTitleBar">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- <service android:name=".C2DMReceiver"/>
- <receiver
- android:name="com.google.android.c2dm.C2DMBroadcastReceiver"
- android:permission="com.google.android.c2dm.permission.SEND">
- <intent-filter>
- <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
- <category android:name="com.home.c2dmtest"/>
- </intent-filter>
- <intent-filter>
- <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
- <category android:name="com.home.c2dmtest"/>
- </intent-filter>
- </receiver>
- </application>
- <uses-sdk android:minSdkVersion="8" />
- <permission
- android:name="com.home.c2dmtest.permission.C2D_MESSAGE"
- android:protectionLevel="signature"/>
- <uses-permission android:name="com.home.c2dmtest.permission.C2D_MESSAGE"/>
- <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
- <uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name="android.permission.WAKE_LOCK"/>
- </manifest>
Примечание: Право android.permission.WAKE_LOCK не нужно для использования C2DM, но оно нам понадобится далее, и, чтобы не приводить этот файл дважды, я решил добавить его заранее.
Теперь можно регистрировать устройство в C2DM, для этого в OnCreate вызовем следующий код:
C2DMessaging.register(this, "<yourmail>@gmail.com");
Если все прошло нормально, то через пару секунд в метод onRegistered класса C2DMReceiver свалится новый Regestration ID. Если этого не произошло, нужно посмотреть лог LogCat'а на наличие ошибок.
Разрегистрироваться можно вызвав метод:
C2DMessaging.unregister(this);
Получить текущий Registration ID:
String id = C2DMessaging.getRegistrationId(this);
Registration ID может изменится в любую секунду, т.е. Google может прислать его новое значение, и, следовательно, такую ситуацию нужно уметь обрабатывать. В нашем примере для этого все уже сделано, реализация данного механизма находится в классе C2DMBaseReceiver метод handleRegistration.
Итоговый проект выглядит так:

Теперь нужно сделать наш проект более наглядным, расширим обработку сообщений. Пусть при поступлении нового сообщения будет появляться Notification, а при его выборе будет запускаться наше приложение с текстом заданным сервером.
Ok, для этого модифицируем код onMessage следующим образом:
- @Override
- protected void onMessage(Context context, Intent receiveIntent)
- {
- String data = receiveIntent.getStringExtra("message");
- if(data != null)
- {
- Log.w("C2DMReceiver", data);
- Intent intent = new Intent(this,Main.class);
- intent.putExtra("message", data);
- NotificationManager mManager = (NotificationManager)
- getSystemService(Context.NOTIFICATION_SERVICE);
- Notification notification = new Notification(android.R.drawable.ic_dialog_info,
- "My C2DM message", System.currentTimeMillis());
- notification.setLatestEventInfo(context,"App Name","C2DM notification",
- PendingIntent.getActivity(this.getBaseContext(), 0,
- intent,PendingIntent.FLAG_CANCEL_CURRENT));
- mManager.notify(0, notification);
- }
- }
message — это идентификатор сообщения, которое передает сервер. Мы его добавим в новый Intent, чтобы потом получить в Main'е.
Модифицируем Main, чтобы вывести передаваемое сообщение:
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- TextView view = new TextView(this);
- String message = getIntent().getStringExtra("message");
- if(message == null)
- view.setText("Hello, World!!!");
- else
- view.setText("Your message: " + message);
- setContentView(view);
- String id = C2DMessaging.getRegistrationId(this);
- if(id == "")
- {
- C2DMessaging.register(this, "<yourmail>@gmail.com");
- }
- }
В реальной жизни нам конечно нужно написать механизм передачи Registration ID серверу и прочие мелочи, но т.к. этой пример мы на этом остановимся, а Registration ID захардкодим на сервере.
Серверная часть
Первым делом нужно получить Auth Token. Об этом хорошо написано в этом блоге (Там же много другой полезной информации). В общем, есть пример, все на русском, поэтому не будем повторяться и будем считать, что мы его получили.
Значит у нас есть Registration ID и Auth Token. Дело за малым, нужно передать сообщение. Почему-то именно про эту часть в интернете информации очень мало, хотя здесь нет ничего сложного.
Нам нужно установить соединение с сервером Google, сформировать запрос в правильном формате. И все. Упрощенный пример отправки сообщения приведен в следующем коде:
- public boolean sendData(
- String authToken,
- String registrationId,
- String collapse,
- String key,
- String value)
- throws IOException {
- // Устанавливаем Registration ID
- StringBuilder postDataBuilder = new StringBuilder();
- postDataBuilder.append("registration_id").
- append("=").append(registrationId);
- // Задаем collapse_key - группирует сообщения, если collapse key одинаковый, а
- // устройство, например, выключено, то будем отослано только одно сообщение,
- // а не все сразу.
- postDataBuilder.append("&").append("collapse_key").append("=").
- append(collapse);
- // Добавляем передавамую дату, в формате <data.><key>=<value>
- postDataBuilder.append("&").append("data."+key).append("=").
- append(URLEncoder.encode(value, "UTF-8"));
- byte[] postData = postDataBuilder.toString().getBytes("UTF-8");
- URL url = new URL("android.clients.google.com/c2dm/send");
- //Устанавливаем соединение
- //Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("lazerboy.local", 8080));
- HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(/*proxy*/);
- conn.setDoOutput(true);
- conn.setHostnameVerifier(this.new MyHostnameVerifier());
- conn.setRequestMethod("POST");
- conn.setRequestProperty("Content-Type",
- "application/x-www-form-urlencoded;charset=UTF-8");
- conn.setRequestProperty("Content-Length", Integer.toString(postData.length));
- conn.setRequestProperty("Authorization", "GoogleLogin auth=" + authToken);
- OutputStream out = conn.getOutputStream();
- out.write(postData);
- out.close();
- // Получаем код ответа.
- int responseCode = conn.getResponseCode();
- if (responseCode == HttpServletResponse.SC_UNAUTHORIZED ||
- responseCode == HttpServletResponse.SC_FORBIDDEN) {
- System.out.printf("Unauthorized - need token");
- return false;
- }
- if (responseCode == HttpServletResponse.SC_OK )
- {
- System.out.printf("Data sent to device!");
- return true;
- }
- System.out.printf("Something wrong, response message: ", conn.getResponseMessage());
- return false;
- }
Здесь упущена одна очень важная деталь: обработка сообщений от сервера. Так, например, никак не обрабатывается ситуация когда устаревает Auth Token. Но я хотел написать пример рабочего приложения, а не ready to use библиотеку. Тем более все, что нужно, можно найти в исходниках от Google, например, здесь.
Если код отработает успешно, то пользователь на своем девайсе увидит наше уведомление в notification area, и если щелкнет по нему, то запуститься наше приложение с текстом от сервера. Ура, это именно то, чего мы и добивались.
Заключение
Я показал, как можно без особой головной боли прикрутить поддержку C2DM к приложению на базе Android. Что для этого нужно сделать, как на стороне клиента, так и на стороне сервера. Конечно, остались «белые пятна», но повторюсь, основной задачей было донести идеи и показать примеры кода, чтобы максимально облегчить людям, читающим этот пост, начало разработки приложений с поддержкой C2DM.
