Подробнее о реализации поддержки GCM на Android-клиенте

в 11:11, , рубрики: android, gcm, server push, Разработка под android, метки: , ,

Тут уже писали об GCM. Для чего эта статья?

Верно, писали. Буквально на этой неделе на Хабре была опубликована статья GCM – новый сервис Push-уведомлений от Google (если вы еще не знакомы с Google Cloud Messaging for Android, то советую прочитать её перед прочтением этой статьи, тем более в моей статье не описываются процесс создания проекта с GCM). Не знаю использовал её автор GCM в реальном приложении или нет, а вот мне пришлось. Поэтому-то я и хочу описать кое-что, чему не нашлось места в предыдущей статье, или что не было объяснено. Добавить это все комментарием в предыдущую статью, боюсь, невыполнимая задача.

Необходимые разрешения

  • <uses-permission android:name="android.permission.INTERNET" />
    

    Тут всё ясно, без доступа к интернету GCM нам и не нужен

  • <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    

    GCM требует доступ к Google-аккаунту

    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    

    По этому поводу в прошлой теме даже был спор, но никто из участников не решил посмотреть в исходных код. Документация этот момент умалчивает, и лишь говорит, что возможно вы захотите захватить PowerManager.WakeLock. Так вот, если вы пользуетесь стандартной библиотекой GCM, то вам придется добавлять такое разрешение.

    Вкратце механизм работы такой: наше приложение подписывается на получение широковещательных запросов. При получении запроса мы устанавливаем полученному Intent'у имя класса (setClassName()) в имя нашего сервиса расширяющего GCMBaseIntentService, затем захватываем WakeLock с флагом PowerManager.PARTIAL_WAKE_LOCK (не даем уснуть только CPU, экран и прочее спит спокойно), запускаем Intent как сервис, по выходу из onHandleIntent сервиса освобождаем WakeLock.

    Не поверили и не стали добавлять это разрешение, и в итоге получаем вот такое исключение:

    java.lang.SecurityException: Neither user 10110 nor current process has android.permission.WAKE_LOCK.

  • <permission
        android:name="{имя пакета приложения}.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />
    <uses-permission
        android:name="{имя пакета приложения}.permission.C2D_MESSAGE" />
    

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

    Примечание: если вы выставили minSdkVersion в 16 или выше (Jelly Bean и последующие версии), то это разрешение вам не нужно (года через 2, надеюсь, можно будет опускать).

  • <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

    Собственно разрешение на регистрацию в GCM и получение сообщений.

Изменяется ли код регистрации (registationId)?

Рассмотрим код из приложения-примера:

final String regId = GCMRegistrar.getRegistrationId(this);
if (regId.equals("")) {
    // Automatically registers application on startup.
    GCMRegistrar.register(this, SENDER_ID);
}

Вроде бы других условий нет. Так что, не изменяется? Если перейти по этой ссылке: http://developer.android.com/intl/ru/guide/google/gcm/adv.html#reg-state, можно узнать что все-таки может измениться. Таких случая два:

  1. Обновление программы
  2. Создание резервной копии и восстановление из неё

Для проверки на обновление программы я написал небольшой класс-помощник. Может быть кому-нибудь пригодится:

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;

public final class ApplicationVersionHelper
{
    public static final APP_VERSION_PREFS = "application_version";

    public static boolean isApplicationVersionCodeEqualsSavedApplicationVersionCode(Context context)
    {
        return getApplicationVersionCode(context) == getApplicationVersionCodeFromPreferences(context);
    }

    public static int getApplicationVersionCode(Context context)
    {
        PackageManager pm = context.getPackageManager();
        PackageInfo packageInfo;
        int applicationVersion = 1;
        try
        {
            packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
            applicationVersion = packageInfo.versionCode;
        }
        catch (NameNotFoundException ignored)
        {
        }
        return applicationVersion;
    }
	
    public static int getApplicationVersionCodeFromPreferences(Context context)
    {
        return context.getSharedPreferences(APP_VERSION_PREFS, Context.MODE_PRIVATE).getInt("application_version_code", 0);
    }

    public static void putCurrentPackageVersionInPreferences(Context context)
    {
        context.getSharedPreferences(APP_VERSION_PREFS, Context.MODE_PRIVATE).edit().putInt("application_version_code", getPackageVersion(context)).commit();
    }
}

Обратите внимание на то, что настройки получаются не через PreferenceManager.getDefaultSharedPreferences, а через именованный файл настроек. Для чего это делается, я объясню позже.
Теперь нам нужно вызвать putCurrentPackageVersionInPreferences после успешной регистрации в GCM и на нашем сервисе, а код проверки регистрации превращается в:

final String regId = GCMRegistrar.getRegistrationId(this);
if (regId.equals("") || !isApplicationVersionCodeEqualsSavedApplicationVersionCode(this)) {
    // Automatically registers application on startup.
    GCMRegistrar.register(this, SENDER_ID);
}

Для обработки создания резервной копии (не все об этой возможности вообще знают. Если стало интересно, то читать здесь — http://developer.android.com/intl/ru/guide/topics/data/backup.html) я предлагаю следующее решение: просто не сохранять настройки с именем из константы ApplicationVersionHelper.APP_VERSION_PREFS при бэкапе. Вот и пригодился именованный файл настроек :) Тогда isApplicationVersionCodeEqualsSavedApplicationVersionCode вернет false при восстановлении данных и мы отправим запрос на регистрацию.

Обработчики в GCMIntentService

В GCMIntentService (классе унаследованном от GCMBaseIntentService)
нам предстоит переопределить несколько методов. Кратко по ним:

  • protected void onRegistered(Context context, String registrationId)

    этот метод вызывается после успешной регистрации в GCM, отсюда нам нужно передать registrationId на наш сервер

  • protected void onUnregistered(Context context, String registrationId)

    этот метод вызывается после успешной отмены регистрации в GCM, так же передаем registrationId на наш сервер для исключения из рассылки (многие приложения никогда не будут пользоваться данной возможностью)

  • protected void onMessage(Context context, Intent intent)

    получение сообщения от GCM, если есть полезная нагрузка (payload), то данные лежат в intent

  • protected void onDeletedMessages(Context context, int total)

    получение уведомления от GCM об удаленных сообщениях, что это такое и с чем их есть смотрите здесь — http://developer.android.com/intl/ru/guide/google/gcm/adv.html#payload

  • public void onError(Context context, String errorId)

    невосстановимая ошибка при получении данных, в errorId код ошибки

  • protected boolean onRecoverableError(Context context, String errorId)

    восстановимая ошибка при получении данных, в errorId код ошибки. Если вернем true, то разрешим совершить еще одну попытку, если false, то прекратим попытки. Я рекомендую в этом методе возвращать super.onRecoverableError(context, errorId);

Чистим за собой!

Не забудьте отменить процесс регистрации, если он запущен, и вызвать GCMRegistrar.onDestroy в методе onDestroy вашей главной Activity. Вот как это сделано у меня:

@Override
protected void onDestroy()
{
    if (registerTask != null)
    {
        registerTask.cancel(true);
    }
    try
    {
        CMRegistrar.onDestroy(this);
    }
    catch(Exception ignored)
    {

    }
    super.onDestroy();
}

registerTask тут — асинхронное задание (AsyncTask).

Заключение

Советую прочитать http://developer.android.com/intl/ru/guide/google/gcm/index.html (а там 5 пунктов) перед использованием GCM в своем приложении, а если есть вопросы (как насчет WAKE_LOCK разрешения), то не бояться залезть в исходных код.

Автор: 4ex

* - обязательные к заполнению поля