Пользователь
0,0
рейтинг
24 марта 2011 в 13:23

Разработка → Пишем приложение под Android с поддержкой Cloud to Device Messaging (C2DM) из песочницы



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

В данном посте я хочу уделить внимание сервису 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
Абдульманов Роман @NevRA
карма
50,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (34)

  • +3
    Все больше и больше статей про разработку под Android! Это не может не радовать!
  • 0
    Спасибо. Хабр опять торт!
    • 0
      Всегда пожалуйста!
  • +1
    Отличная статья! Очень хорошая идея Google! Очень радуюсь этой фичи, можно кучу отличных вещей сделать! Например для twitter-клиентов сделать апдейт по нотификации через C2DM. А некоторые IM — клиенты уже это поддерживают, например Trillian.
    • 0
      Какая идея?? Подобная нотификация уже давно есть на iOS и WP7, а гугловцы только в 2.2 допилили. Это вынужденная необходимость чтобы всякая ерунда в процессах не висела и не мучила батарею телефона.
      • 0
        >Подобная нотификация уже давно есть на iOS и WP7

        Подобная нотификация есть в BlackBerry c самого начала существования BIS, т.е. не то что «раньше, чем в айфоне», но даже «раньше, чем айфон вообще появился на свет» :)
  • +1
    Здорово, что кто-то написал про это на хабре.

    Делал для себя приложение которое мониторит обновления помеченные как rss2mobile в гугл ридере и присылает их мне через c2dm. Обычно рсс читалки едят трафик и батарейку, а тут даже и не заметно, что приложение работает.
    Все никак не хватало времени написать об этом на сдесь.

    кстати, для меня полезной была еще эта статья:
    mylifewithandroid.blogspot.com/2010/10/push-service-from-google.html
    • 0
      Добавил ссылку в пост
    • 0
      Жду статью о вашем приложении. И не только статью ;)
      • +1
        Приложение стабильно работает, сообщает мне о новых сериях сериалов и статьях на хабре.
        Но оно вообще без интерфейса — новые уведомления показываются в нотификейшн ареа, при клике покаывается лист вью со списком полученного, при выборе статьи — веб вью. Снизу тестовая кнопка «hello world».

        Возникло много вопросов, каким оно должно быть, что бы было удобно для кого-то кроме меня и я отложил это дело на потом. Хотя сам постоянно пользуюсь в таком виде :)

        Позже понял, что меня не устраивают ни одни из менеджеров контактов на андроиде (не склоняют имена, на транслитерируют, нету расширенной локализированной группировки — если человек в разных социальных сетях назван по разному эти контакты могут автоматом не объедениться). Вообщем сейчас это приложение затянуло и отнимает все время. И уже довольно мого сделано.

        Но самые интересные на мой взгляд вещи, с которыми боролся, в любом случае опишу.

        ЗЫ: именно это и доставляет наибольшее удовольствие в андроиде — если чего-то не хватает, можно сесть и написать это самому.
        • +1
          >Но самые интересные на мой взгляд вещи, с которыми боролся, в любом случае опишу.

          Ждем с нетерпением.
  • 0
    >Использование данного сервиса является незаменимым способом при необходимости передать сообщение пользовательскому приложению, зарегистрированному в системе, но не являющемуся на данный момент активным.

    — Странное предложение :-)
    То есть как бы девайс должен с облаком общаться, а тут можно подумать, что говорится о внутрисистемной коммуникации приложений :-)
    Конечно же, для последнего должно хватать BroadcastReceiver.

    Продолжение публиковать планируете?
    • 0
      >Продолжение публиковать планируете?

      Когда наберется побольше интересного материала, пока же его недостаточно для полноценного топика.
  • 0
    Правильно ли я понимаю, что для получения этих уведомлений телефон должен быть подключен к GPRS?
    • +1
      Должен быть подключен интернету. GPRS, Wi-fi и т.д.
      • 0
        "к интернету", конечно же.
        • 0
          Т.е. этот сервис не годится, когда надо доставить сообщение быстро, ибо соединение с Интернетом есть далеко не всегда. Жаль.
          • 0
            Да, когда нет интернета не годится, т.к. нужно соединение с Google'им облаком.
            • 0
              Для передачи сообщений на устройство используется GoogleTalk/Jabber, или нет?
              • 0
                Простите, если я не правильно понял ваш вопрос. Для передачи сообщений на устройство используется C2DM, а GoogleTalk/Jabber в этом посте не применяется, хотя, возможно, они и используют C2DM в своем работе.
                • 0
                  Я хотел узнать, какой протокол используется для доставки сообщений? Отдельный демон, ожидающий данные, или что-то иное, например, существующий Jabber (GoogleTalk)?
                  Просто, когда обсуждали принципы работы приложение Chrome to phone, остановились на том, что он использует сервисные сообщения существующей реализации джаббер-протокола, который достаточно удобен, т.к. сам поддерживает подключение и ожидает сообщения.
                  • 0
                    >Просто, когда обсуждали принципы работы приложение Chrome to phone

                    Где это обсуждение можно почитать?
                    • 0
                      Кажется, это было устное обсуждение принципов работы данной программы ))
                      Собственно, вопрос был таким: какой транспорт используется и какая «служба» отвечает за получение? Работает ли технология при отключённой фоновой синхронизации и/или автосинхронизации?
                      • +1
                        Ok. Про протокол, не знаю, об этом информации нет. Если говорить про уровень приложения то скорее всего это какой-то их собственный протокол, который более нигде не применяется, если говорить про уровень транспорта, то похоже, что это HTTPS, но это все мои догадки.
                        Служба? Вас имя, отвечающего за это, процесса интересует, зачем? Не интересовался, т.к. для разработчика эти данные не нужны, на офф. сайте информации тоже нет.
                        Насчет синхронизации, да, должно работать, т.к. такого требования нет в сисреках, и из логики, странно бы было отказывать в получении нотификаций приложениям, которые ничего не знаю про настройки аккаунтов у пользователя.
  • 0
    Спасибо, интересно.

    Немного не в теме, и не особо разбирался, но первая мысль — а безопасная ли фишка? Ну я в том плане что нормален тот факт что некий сервер может послать сообщение для обработки всем пользователям приложения при условии что оно даже не запущено? К примеру есть у злоумышленника серверное приложение. Также вместе с этим в маркет или куда либо ещё запускается клиентская часть в виде какого либо приложения раздающего няшки. В какой то момент пользователей набирается много, человек запускает сервер, и высылает адрес какой либо машинки в сети. Облако передает адрес всем смартфонам с клиентской частью, оно запускается, и начинает к примеру DDoS-ить адресата.

    Оно действительно возможно, или я чего не понял?
    • 0
      1. Android приложение регистрируется в C2DM, тем самым сообщает о своей готовности принимать сообщения, и получает Registration ID. Registration ID — это уникальный идентификатор устройства, зная который, серверная часть может отправить сообщение получателю.
    • 0
      А, блин, мне самому нужно глаза протереть: я умудрился пропустить часть про «вместе с этим в маркет или куда либо ещё запускается клиентская часть в виде какого либо приложения раздающего няшки» :)
    • 0
      судя по манифесту для этого нужны разрешения, которые отображаются при установке приложения. Поэтому если не доверяете приложению, то не ставьте.

      То, что вы описали можно сделать и без c2dm.
      • 0
        Да по моему на разрешения мало кто смотрит по большей части, да и не все поймут что оно c2dm такое.

        Можно и без него, но тут сервис какой нибудь к примеру повесить придется, да ещё и мониторить постоянно. Тут же если я все верно понял приложение вообще может месяцами не подавать признаков жизни пока не придет команда.
  • +2
    Может кому полезно будет. В начале года, когда знакомился с Node.JS написал небольшой модуль для работы с c2dm: github.com/SpeCT/node-c2dm
    • 0
      Спасибо, пригодится
  • 0
    < «C2DM является одной из фундаментальных возможностей платформы Android»

    Видимо именно по этой причине google до сих пор держит C2DM в лабсах и оставляет за собой право менять API если им так захочется.
    Нам всем нужен C2DM, но использовать его в реальных системах пока довольно рискованно.

    • 0
      Извините, как-то промахнулся с ответом. Он в следующем комментарии.
  • 0
    Наоборот, кажется, что все устаканилось и повсеместно применяется. Или я чего-то не знаю?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.