вторник, 4 декабря 2012 г.

cordova android PUSH plugin

Создание плагинов cordova (phonegap) для android на примере PUSH сервиса.


  1. Перед реализацией плагина нужно убедиться, что библиотеки cordova-2.0.0.jar, gcm.jar, json-simple-1.1.1.jar лежат в папке libs, и с этими файлами проделана процедура Add to Build Path (они должны быть в списке Referenced Libraries, иначе надо кликнуть по библиотеке правой кнопкой и выбрать Build Path / Add to Build Path).
  2. В файле res/xml/config.xml в тэге <plugins> декларируем создаваемый на java плагин:
    <plugin name="PushPlugin" value="ru.andrew.plugin.PushPlugin"/>
     Что означает, что java-класс будет называться 
    PushPlugin, и находиться он будет  в пакете ru.andrew.plugin.

  3. Прописываем необходимые разрешения в конфигурационном файле приложения AndroidManifest.xml;

     В корневом тэге <manifest> прописываем разрешения:
        <uses-permission android:name="ru.andrew.androidnativepush.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.GET_ACCOUNTS" />
        <uses-permission android:name="android.permission.WAKE_LOCK" />
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    В тэге <application> прописываем receiver и service:
            <receiver
                android:name="com.google.android.gcm.GCMBroadcastReceiver"
                android:permission="com.google.android.c2dm.permission.SEND" >
                <intent-filter>
                    <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                    <action android:name="com.google.android.c2dm.intent.REGISTRATION" />

                    <category android:name="ru.andrew.androidnativepush" />
                </intent-filter>
            </receiver>
            
            <service android:name=".GCMIntentService" />

    в этих кусках кода нужно всюду имя пакета ru.andrew.androidnativepush заменить на своё, которое мы указываем в самом верху AndroidMainfest.xml в атрибуте package.
  4. Создаём java-класс плагина:
    package ru.andrew.plugin;

    import java.io.IOException;

    import org.apache.cordova.api.Plugin;
    import org.apache.cordova.api.PluginResult;
    import org.json.JSONArray;
    import org.json.JSONException;

    import android.content.Context;
    import android.net.ConnectivityManager;
    import android.net.NetworkInfo;
    import android.util.Log;

    import com.google.android.gcm.GCMRegistrar;

    public class PushPlugin extends Plugin {
    private static final String TAG = "PushPlugin";

    public PluginResult execute(String action, JSONArray args, String callbackId) {
    try {
    if ("echo".equals(action)) {
    return echo(args);
    } else if ("registerPush".equals(action)) {
    return registerPush(args);
    } else if ("unregisterPush".equals(action)) {
    return unregisterPush(args);
    } else {
    return new PluginResult(PluginResult.Status.INVALID_ACTION);
    }
    } catch (JSONException ex) {
    ex.printStackTrace();
    return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
    }
    }

    private PluginResult unregisterPush(JSONArray args) {
    /*
    Intent unregIntent = new Intent("com.google.android.c2dm.intent.UNREGISTER");
    unregIntent.putExtra("app", PendingIntent.getBroadcast(cordova.getActivity()
    .getApplicationContext(), 0, new Intent(), 0));
    cordova.getActivity()
    .getApplicationContext().startService(unregIntent);
    */
    try {
    GCMRegistrar.unregister(cordova.getActivity()
    .getApplicationContext());
    Log.d(TAG, "PUSH уведомления дерегистрированы.");
    return new PluginResult(PluginResult.Status.OK);
    } catch (Exception ex) {
    ex.printStackTrace();
    return new PluginResult(PluginResult.Status.ERROR, ex.getMessage());
    }
    }

    private PluginResult echo(JSONArray args) throws JSONException {
    String echo = args.getString(0);
    if (echo != null && echo.length() > 0) {
    return new PluginResult(PluginResult.Status.OK, echo);
    } else {
    return new PluginResult(PluginResult.Status.ERROR);
    }
    }

    private PluginResult registerPush(JSONArray args) throws JSONException {
    String projectIdStr = args.getString(0);
    if (isBlank(projectIdStr)) {
    return new PluginResult(PluginResult.Status.ERROR,
    "project id must be set in parameters");
    } else {
    try {
    long projectId = Long.parseLong(projectIdStr);
    String deviceId = registerPush(projectId);
    return new PluginResult(PluginResult.Status.OK, deviceId);
    } catch (NumberFormatException ex) {
    ex.printStackTrace();
    return new PluginResult(PluginResult.Status.ERROR,
    "project id must be wellformed number");
    } catch (IOException ex) {
    ex.printStackTrace();
    return new PluginResult(PluginResult.Status.ERROR,
    ex.getMessage());
    }
    }
    }

    private String registerPush(long projectId) throws IOException {
    inetIsOk();
    GCMRegistrar.checkDevice(cordova.getActivity().getApplicationContext());
    GCMRegistrar.checkManifest(cordova.getActivity()
    .getApplicationContext());
    String regId = GCMRegistrar.getRegistrationId(cordova.getActivity()
    .getApplicationContext());
    if (regId.equals("")) {
    GCMRegistrar.register(
    cordova.getActivity().getApplicationContext(), projectId
    + "");
    Log.i(TAG,
    "just now registered: "
    + GCMRegistrar.getRegistrationId(cordova
    .getActivity().getApplicationContext()));
    regId = GCMRegistrar.getRegistrationId(cordova.getActivity()
    .getApplicationContext());
    return regId;
    } else {
    Log.e(TAG, "Already registered: " + regId);
    return regId;
    }
    }

    public void inetIsOk() throws IOException {
    ConnectivityManager connMgr = (ConnectivityManager) cordova
    .getActivity().getApplicationContext()
    .getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    if (networkInfo != null && networkInfo.isConnected()) {
    return;
    //
    } else {
    throw new IOException("Нет интернета");
    }
    }

    public static boolean isBlank(String s) {
    if (s == null || "".equals(s.trim()))
    return true;
    else
    return false;
    }
    }

  5. Создаём java-класс GCM-сервиса, реализующие callback-функции регистрации / дерегистрации в сервисе, получения сообщения и получения ошибки.
    Этот класс в соответствии с 5 пунктом 2 шага спецификации google должен расширять класс GCMBaseIntentService.
    Неприятным ограничением в реализуемых функциях является невозможность использования Toast-уведомлений.

    package ru.andrew.androidnativepush;

    import java.util.Random;

    import android.app.Notification;
    import android.app.NotificationManager;
    import android.app.PendingIntent;
    import android.content.Context;
    import android.content.Intent;
    import android.util.Log;

    import com.google.android.gcm.GCMBaseIntentService;

    public class GCMIntentService extends GCMBaseIntentService {

    @Override
    protected void onError(Context arg0, String arg1) {
    Log.d("GCM onError", arg1);
    }

    @Override
    protected boolean onRecoverableError(Context context, String errorId) {
    Log.d("GCM onRecoverableError", errorId);
    return false;
    }

    @SuppressWarnings("deprecation")
    @Override
    protected void onMessage(Context ctx, Intent intt) {
    Log.d("onMessage", String.valueOf(intt));
    for (String s : intt.getExtras().keySet()) {
    Log.d(s, intt.getExtras().getString(s));
    }

    String ns = Context.NOTIFICATION_SERVICE;
    NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns);
    int icon = R.drawable.ic_launcher;// notification_icon;
    CharSequence tickerText = intt.getStringExtra("message");
    long when = System.currentTimeMillis();

    Notification notification = new Notification(icon, tickerText, when);
    notification.flags = Notification.DEFAULT_LIGHTS
    | Notification.FLAG_AUTO_CANCEL;

    CharSequence contentTitle = "myApp";
    Random r = new Random();
    int notificationId = r.nextInt();
    Intent notificationIntent = new Intent(this, MainActivity.class);
    PendingIntent contentIntent = PendingIntent.getActivity(this,
    notificationId, notificationIntent,
    PendingIntent.FLAG_UPDATE_CURRENT);
    notification.setLatestEventInfo(ctx, contentTitle, tickerText,
    contentIntent);

    mNotificationManager.notify(notificationId, notification);
    }

    @Override
    protected void onRegistered(Context context, String arg1) {
    Log.d("onRegistered", arg1);
    Log.d("onRegistered", "Зарегистрирован в сервисе получения PUSH-уведомлений");
    // Toast.makeText(context, "Зарегистрирован в сервисе получения PUSH-уведомлений", Toast.LENGTH_LONG).show();
    }

    @Override
    protected void onUnregistered(Context context, String arg1) {
    Log.d("onUnregistered", arg1);
    Log.d("onUnregistered", "Отключен от сервиса получения PUSH-уведомлений");
    // Toast.makeText(context, "Отключен от сервиса получения PUSH-уведомлений", Toast.LENGTH_LONG).show();
    }

    }


    Здесь функция onMessage(Context ctx, Intent intt) ответственна за поведение программы при получении нового уведомления. Переменная intt содержит данные, передаваемые в сообщении. Они могут быть получены так:
    intt.getStringExtra("message")

    Обычным поведением программы при получении уведомления является создание двух эффектов: сообщение должно быть пролистано в строке статуса вверху экрана и оно должно попасть в список уведомлений, который можно открыть потянув пальцем строку статуса сверху вниз, и в этом списке по нажатию на уведомление происходит переход на нужный Intent нашего приложения.

    Первый эффект реализуется кодом:
    NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    int icon = R.drawable.ic_launcher;// иконка нашего приложения
    CharSequence tickerText = intt.getStringExtra("message");
    long when = System.currentTimeMillis();
    Notification notification = new Notification(icon, tickerText, when);
    notification.flags = Notification.DEFAULT_LIGHTS
    | Notification.FLAG_AUTO_CANCEL;
    к уведомлению будет прикреплена иконка нашего приложеиня icon (чтобы пользователь знал, куда его хотят отправить :) ), и будет прокручен текст tickerText, содержащийся в поле message сообщения. Параметр when говорит, когда нужно показать уведомление, поскольку мы хотим здесь и сейчас, мы ставим текущее время (при обработке сформированного уведомления системой время уже будет немного больше, что наверное означает, что показываются все уведомления с парметром времени меньшим чем текущее. Но это домыслы автора, не подкреплённые авторитетными ссылками или опытом).
    Флаг Notification.FLAG_AUTO_CANCE означает, что иконка приложения будет исчезать из строки статуса при прочтении сообщения. Как оказывается, это не дефолтное поведение уведомления, что меня несколько удивило.

    Второй эффект реализуется кодом:
    CharSequence contentTitle = "myApp";
    Random r = new Random();
    int notificationId = r.nextInt();
    Intent notificationIntent = new Intent(this, MainActivity.class);
    PendingIntent contentIntent = PendingIntent.getActivity(this,
    notificationId, notificationIntent,
    PendingIntent.FLAG_UPDATE_CURRENT);
    notification.setLatestEventInfo(ctx, contentTitle, tickerText,
    contentIntent);
    mNotificationManager.notify(notificationId, notification);
    при создании notificationIntent мы должны указать класс активити MainActivity.class, на которую должен будет перейти пользователь при клике на уведомление в списке уведомлений.
    notificationId - это уникальный номер сообщения в нашей системе, Поскольку обычно у нас есть сервер, хранящий все сообщения всем пользователям, этот параметр будет равен id этого сообщения в базе. Он должен лежать в одном из полей сообщения и браться оттуда, Здесь он берётся как случайное число, потому что если задать его константой, а потом несколько раз послать сообщения, система будет думать, что ей несколько раз приходит одно и то же сообщение, и не будет показывать их как новые сообщения.

    Конструктор new Notification(icon, tickerText, when) и метод notification.setLatestEventInfo(ctx, contentTitle, tickerText, contentIntent) устарели начиная с 17 версии SDK. Но на момент написания этой статьи этой версии нет и месяца, что означает, что у многих устройств сейчас замена им не будет поддерживаться и в соотвествующих местах кода приложение будет выбрасывать исключение вместо того, чтобы скромно проиллюстрировать работу такой желанной функциональности. Поэтому здесь я выбрал воспользоваться объявленными устаревшими методами. Можно было бы программно запросить у системы номер SDK и в зависимости от того, переваливает ли она вышеуказанный порог или нет выполнять соответственно новый или старый код, но для целей данного поста мне кажется это неким перебором.



    Редактируем index.html
  1. Для использования русских букв в интерфейсе в тэге <head> нужно добавить строку:
    <meta charset="utf-8">

  2. Для вызова метода плагина необходимо дождаться, чтобы cordova была полностью загружена. Для этого, например, для целей тестирования в файле index.html добавим событие окончательной готовности девайса:
    document.addEventListener("deviceready", myCallbackFunction, false);
    Функция myCallbackFunction будет вызвана когда девайс будет готов.

  3. Реализуем метод myCallbackFunction:
    function myCallbackFunction(){


    cordova.exec(success, fail, "PushPlugin", "registerPush", ['999999999999'])
    }
    function success(data){
    alert('success: ' + data);
    }


    function fail(data){
    alert('failed: ' + data);
    }

    здесь:
    success(data) - метод, который будет вызван при успешном окончании метода плагина;
    fail(data) - метод, который будет вызван при неудачном окончании метода планига;
    "PushPlugin" - имя плагина, задекларированного в config.xml;
    "echo" - действие (селектор), на основании которого будет вызван тот или иной метод;
    ['999999999999'] - массив данных в формате JSON, который передаётся методу плагина. В данной плагине это номер проекта, зарегистрированного в GCM сервисе google.

  4. Запускаемся, получаем множество ошибок, мужественно их преодолеваем и идём пить кофе.