Pull to refresh

Пишем приложение под Android с поддержкой Cloud to Device Messaging (C2DM)

Reading time 15 min
Views 35K


Здравствуйте, читатели Хабрахабр!

В данном посте я хочу уделить внимание сервису C2DM от компании Google и попытаюсь рассказать, как реализовать поддержку данного сервиса в вашем Android-приложении. Напомню, что C2DM — это специальный сервис, предоставляющий API для отправки сообщений приложениям, установленным на устройствах Android. Использование данного сервиса является незаменимым способом при необходимости передать сообщение пользовательскому приложению, зарегистрированному в системе, но не являющемуся на данный момент активным.

Хотя C2DM является одной из фундаментальных возможностей платформы Android, информации о нем в рунете мало. Попытка изменить данную ситуацию и является одной из задач этого поста.

Под катом я расскажу, как написать простые клиентское и серверное приложения, покажу некоторые «подводные камни», а также дам ссылки на примеры кода.

Cloud to Device Messaging


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

Клиентская часть


Для использования C2DM Android устройство обязательно должно удовлетворять следующим требованиям:
  1. Оно должно быть версии Android 2.2 или выше. На предыдущих версиях C2DM не поддерживается!
  2. Должна быть любая рабочая Google'вская учетная запись. Если у вас работает Android Market, значит, она у вас есть.
Если все выполняется, можно переходить к созданию приложения. Для этого мы будем использовать Eclipse + Android SDK. Можно создать самый простой «Hello, World» на TextView. Описывать процедуру создания приложения не буду, про это и так много написано, например, здесь или здесь. Должно получиться что-то вроде:


Примечание: Если у вас нет 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:
  1. package com.home.c2dmtest;
  2.  
  3. import com.google.android.c2dm.C2DMBaseReceiver;
  4.  
  5. import android.app.Notification;
  6. import android.app.NotificationManager;
  7. import android.app.PendingIntent;
  8. import android.content.Context;
  9. import android.content.Intent;
  10. import android.util.Log;
  11.  
  12. public class C2DMReceiver extends C2DMBaseReceiver {
  13.     public C2DMReceiver(){
  14.         super("<yourmail>@gmail.com");
  15.     }
  16.  
  17.     @Override
  18.     public void onRegistered(Context context, String registrationId) {
  19.         Log.w("onRegistered", registrationId);
  20.     }
  21.  
  22.     @Override
  23.     public void onUnregistered(Context context) {
  24.         Log.w("onUnregistered""");
  25.     }
  26.  
  27.     @Override
  28.     public void onError(Context context, String errorId) {
  29.         Log.w("onError", errorId);
  30.     }
  31.  
  32.     @Override
  33.     protected void onMessage(Context context, Intent intent){
  34.         Log.w("onMessage""");
  35.     }
  36. }
  37.  

Но приложение пока не готово для работы с C2DM, т.к. не заданы необходимые права и не зарегистрирован BroadcastReceiver. Для исправления этого нужно модифицировать AndroidManifest.xml, как это сделать можно прочитать здесь. Для моего примера файл выглядит так:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="schemas.android.com/apk/res/android"
  3.       android:versionCode="1"
  4.       android:versionName="1.0" package="com.home.c2dmtest">
  5.     <application 
  6.      android:debuggable="true" 
  7.      android:label="@string/app_name">
  8.         <activity android:name="Main"
  9.                   android:label="@string/app_name"
  10.                   android:theme="@android:style/Theme.NoTitleBar">
  11.             <intent-filter>
  12.                 <action android:name="android.intent.action.MAIN" />
  13.                 <category android:name="android.intent.category.LAUNCHER" />
  14.             </intent-filter>
  15.         </activity>
  16.     <service android:name=".C2DMReceiver"/>
  17.     <receiver 
  18.               android:name="com.google.android.c2dm.C2DMBroadcastReceiver"
  19.               android:permission="com.google.android.c2dm.permission.SEND">
  20.         <intent-filter>
  21.             <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
  22.             <category android:name="com.home.c2dmtest"/>
  23.         </intent-filter>
  24.         <intent-filter>
  25.             <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
  26.              <category android:name="com.home.c2dmtest"/>
  27.         </intent-filter>
  28.      </receiver>
  29.     </application>
  30.     <uses-sdk android:minSdkVersion="8" />
  31.     <permission 
  32.                 android:name="com.home.c2dmtest.permission.C2D_MESSAGE" 
  33.                 android:protectionLevel="signature"/>
  34.     <uses-permission android:name="com.home.c2dmtest.permission.C2D_MESSAGE"/>
  35.     <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>    
  36.     <uses-permission android:name="android.permission.INTERNET" />
  37.    <uses-permission android:name="android.permission.WAKE_LOCK"/>
  38. </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 следующим образом:
  1.     @Override
  2.     protected void onMessage(Context context, Intent receiveIntent) 
  3.     {
  4.         String data = receiveIntent.getStringExtra("message");
  5.         if(data !null)
  6.         {        
  7.             Log.w("C2DMReceiver", data);
  8.  
  9.             Intent intent = new Intent(this,Main.class);
  10.             intent.putExtra("message", data);
  11.  
  12.             NotificationManager mManager = (NotificationManager) 
  13.                 getSystemService(Context.NOTIFICATION_SERVICE);
  14.             Notification notification = new Notification(android.R.drawable.ic_dialog_info
  15.                 "My C2DM message"System.currentTimeMillis());
  16.             notification.setLatestEventInfo(context,"App Name","C2DM notification",
  17.                 PendingIntent.getActivity(this.getBaseContext()0
  18.                     intent,PendingIntent.FLAG_CANCEL_CURRENT));
  19.             mManager.notify(0, notification);
  20.         }
  21.     }

message — это идентификатор сообщения, которое передает сервер. Мы его добавим в новый Intent, чтобы потом получить в Main'е.

Модифицируем Main, чтобы вывести передаваемое сообщение:
  1.  @Override
  2.     public void onCreate(Bundle savedInstanceState) {
  3.         super.onCreate(savedInstanceState);
  4.  
  5.         TextView view = new TextView(this);
  6.  
  7.         String message = getIntent().getStringExtra("message");
  8.         if(message == null)
  9.          view.setText("Hello, World!!!");
  10.         else
  11.          view.setText("Your message: " + message);
  12.  
  13.         setContentView(view);
  14.  
  15.         String id = C2DMessaging.getRegistrationId(this);
  16.         if(id == "")
  17.         {
  18.             C2DMessaging.register(this"<yourmail>@gmail.com");
  19.         }
  20.     }

В реальной жизни нам конечно нужно написать механизм передачи Registration ID серверу и прочие мелочи, но т.к. этой пример мы на этом остановимся, а Registration ID захардкодим на сервере.

Серверная часть


Первым делом нужно получить Auth Token. Об этом хорошо написано в этом блоге (Там же много другой полезной информации). В общем, есть пример, все на русском, поэтому не будем повторяться и будем считать, что мы его получили.

Значит у нас есть Registration ID и Auth Token. Дело за малым, нужно передать сообщение. Почему-то именно про эту часть в интернете информации очень мало, хотя здесь нет ничего сложного.

Нам нужно установить соединение с сервером Google, сформировать запрос в правильном формате. И все. Упрощенный пример отправки сообщения приведен в следующем коде:
  1. public boolean sendData(
  2.             String authToken,
  3.             String registrationId,
  4.             String collapse,
  5.             String key,
  6.             String value)
  7.         throws IOException {
  8.  
  9.         // Устанавливаем Registration ID
  10.         StringBuilder postDataBuilder = new StringBuilder();
  11.         postDataBuilder.append("registration_id").
  12.             append("=").append(registrationId);
  13.  
  14.         // Задаем collapse_key - группирует сообщения, если collapse key одинаковый, а
  15.         // устройство, например, выключено, то будем отослано только одно сообщение, 
  16.         // а не все сразу.
  17.         postDataBuilder.append("&").append("collapse_key").append("=").
  18.             append(collapse);
  19.  
  20.         // Добавляем передавамую дату, в формате <data.><key>=<value>
  21.         postDataBuilder.append("&").append("data."+key).append("=").
  22.             append(URLEncoder.encode(value, "UTF-8"));
  23.  
  24.         byte[] postData = postDataBuilder.toString().getBytes("UTF-8");
  25.  
  26.         URL url = new URL("android.clients.google.com/c2dm/send");
  27.  
  28.         //Устанавливаем соединение
  29.         //Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("lazerboy.local", 8080));
  30.         HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(/*proxy*/);
  31.         conn.setDoOutput(true);
  32.         conn.setHostnameVerifier(this.new MyHostnameVerifier());
  33.         conn.setRequestMethod("POST");
  34.         conn.setRequestProperty("Content-Type"
  35.                 "application/x-www-form-urlencoded;charset=UTF-8");
  36.         conn.setRequestProperty("Content-Length"Integer.toString(postData.length));
  37.         conn.setRequestProperty("Authorization""GoogleLogin auth=" + authToken);
  38.  
  39.         OutputStream out = conn.getOutputStream();
  40.         out.write(postData);
  41.         out.close();
  42.  
  43.         // Получаем код ответа.
  44.         int responseCode = conn.getResponseCode();
  45.  
  46.         if (responseCode == HttpServletResponse.SC_UNAUTHORIZED ||
  47.                 responseCode == HttpServletResponse.SC_FORBIDDEN) {
  48.             System.out.printf("Unauthorized - need token");
  49.             return false;
  50.         }
  51.         if (responseCode == HttpServletResponse.SC_OK )
  52.         {
  53.             System.out.printf("Data sent to device!");
  54.             return true;
  55.         }
  56.  
  57.         System.out.printf("Something wrong, response message: ", conn.getResponseMessage());
  58.  
  59.         return false;
  60.     }

Здесь упущена одна очень важная деталь: обработка сообщений от сервера. Так, например, никак не обрабатывается ситуация когда устаревает Auth Token. Но я хотел написать пример рабочего приложения, а не ready to use библиотеку. Тем более все, что нужно, можно найти в исходниках от Google, например, здесь.

Если код отработает успешно, то пользователь на своем девайсе увидит наше уведомление в notification area, и если щелкнет по нему, то запуститься наше приложение с текстом от сервера. Ура, это именно то, чего мы и добивались.

Заключение


Я показал, как можно без особой головной боли прикрутить поддержку C2DM к приложению на базе Android. Что для этого нужно сделать, как на стороне клиента, так и на стороне сервера. Конечно, остались «белые пятна», но повторюсь, основной задачей было донести идеи и показать примеры кода, чтобы максимально облегчить людям, читающим этот пост, начало разработки приложений с поддержкой C2DM.

Полезные ссылки

  1. Android Cloud to Device Messaging
  2. Место где нужно зарегистрировать учетную запись для использования C2DM
  3. Русскоязычный блог на тему C2DM и Android
  4. Sample от Google
  5. Еще один блог на тему
  6. Пример использования С2DM
Tags:
Hubs:
+54
Comments 34
Comments Comments 34

Articles