вторник, 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. Запускаемся, получаем множество ошибок, мужественно их преодолеваем и идём пить кофе.


воскресенье, 11 ноября 2012 г.

Android Push Notification for PhoneGap

PUSH уведомления в андроиде с помощью phonegap

по результатам работы с сервисом pushwoosh я остался ужасно недоволен его возможностями и для использования технологии PUSH на андроиде написао свой плагин.

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

Здесь будет рассмотрена реализация PUSH-технологии в phonegap с помощью PushWoosh.

Англоязычный оригинал статьи здесь.

Кроме прочтения информации об архитектуре этой службы нужно ещё

Чтобы интегрировать Pushwoosh в наше PhoneGap приложение нужно сделать сделать следующие простые шаги:

1. Получить кода плагина для Android с https://github.com/shaders/phonegap-cordova-push-notifications/tree/master/Android
2. Скопировать папку “src” в наш проект.
2.a Скопировать файл Pushwoosh.jar в папку “libs” и добавить его в classpath проекта.
Для Eclipse: http://www.wikihow.com/Add-JARs-to-Project-Build-Paths-in-Eclipse-(Java)
Для IDEA: http://stackoverflow.com/questions/1051640/correct-way-to-add-lib-jar-to-an-intellij-idea-project
3. Добавить PushNotification.js из папки www в нашу папку www на диске
4. Добавить ссылку на файл PushNotification.js используя тэги <script> в нашем html-файле:
<script type="text/javascript" src="PushNotification.js"></script>
5. Добавить новую строку о плагине в файл “res/xml/config.xml” (Для версий кордова Cordova < 2.0 нужно добавить эту строку в plugins.xml.)
<plugins>
    <plugin name="PushNotification" value="com.pushwoosh.test.plugin.pushnotifications.PushNotifications" onload="true"/>

Добавить новую строку в “cordova.xml” (если у нас нет cordova.xml, нужно добавить эту строку в res/xml/config.xml)

<cordova>

    <access origin="https://cp.pushwoosh.com" subdomains="true" />


6. Регистрируемся для push уведомлений:
Добааляем следующую функцию в наш javascript-файл, вводим соответсвующие Project ID  и Pushwoosh App ID, которые быти упомянуты в преамбуле
function initPushwoosh()
{
    var pushNotification = window.plugins.pushNotification;
    pushNotification.registerDevice({ projectid: "GOOGLE_PROJECT_ID", appid : "PUSHWOOSH_APP_ID" },
        function(status) {
            var pushToken = status;
            console.warn('push token: ' + pushToken);
        },
        function(status) {
            console.warn(JSON.stringify(['failed to register ', status]));
        }
    );
    document.addEventListener('push-notification', function(event) {
        var title = event.notification.title;
            var userData = event.notification.userdata;
            if(typeof(userData) != "undefined") {
            console.warn('user data: ' + JSON.stringify(userData));
        }
        navigator.notification.alert(title);
    });
}

Добавим метод init() в функцию onload в HTML:
<body onload="init();" >

и добавим саму функцию init():

function init() {

    document.addEventListener("deviceready", initPushwoosh, true);


    //rest of the code

}


Если вам нужна регистрация PUSH не при запуске приложения, а позже, исправьте код под свои нужды.

7. Получение push уведомлений. Смотрите следующий кусок кода в функции initPushwoosh

  document.addEventListener('push-notification', function(event) {

        var title = event.notification.title;

            var userData = event.notification.userdata;


        console.warn('user data: ' + JSON.stringify(userData));

        navigator.notification.alert(title);
});

8. Добавляем следдующие изменеия в наш AndroidManifest.xml под тэгом manifest. Заменяем PACKAGE_NAME именем пакета приложения. (Имя пакета можно найти в AndroidManifest.xml под тэгом manifest в самом верху файла.)

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
 <!--library-->
 <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
 <!-- GCM connects to Google Services. -->
 <uses-permission android:name="android.permission.INTERNET"/>
 <!-- GCM requires a Google account. -->
 <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
 <!-- Keeps the processor from sleeping when a message is received. -->
 <uses-permission android:name="android.permission.WAKE_LOCK"/>
 <!--
  Creates a custom permission so only this app can receive its messages.
  NOTE: the permission *must* be called PACKAGE.permission.C2D_MESSAGE,
        where PACKAGE is the application's package name.
 -->
 <permission
         android:name="PACKAGE_NAME.permission.C2D_MESSAGE"
         android:protectionLevel="signature"/>
 <uses-permission
         android:name="PACKAGE_NAME.permission.C2D_MESSAGE"/>
 <!-- This app has permission to register and receive data message. -->
 <uses-permission
         android:name="com.google.android.c2dm.permission.RECEIVE"/>
 <!-- GCM requires Android SDK version 2.2 (API level <img src="http://www.pushwoosh.com/wp-includes/images/smilies/icon_cool.gif" alt="8)" class="wp-smiley"> or above. -->
 <!-- The targetSdkVersion is optional, but it's always a good practice
      to target higher versions. -->
 <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="16"/>

9. Добавляем следующие изменения в AndroidManifest.xml в теге application. Заменяем PACKAGE_NAME именем пакета приложения. (Имя пакета можно найти в AndroidManifest.xml под тэгом manifest в самом верху файла.)
<activity android:name="com.arellomobile.android.push.PushWebview"/>
<activity android:name="com.arellomobile.android.push.MessageActivity"/>
<activity android:name="com.arellomobile.android.push.PushHandlerActivity"/>
<!--
  BroadcastReceiver that will receive intents from GCM
  services and handle them to the custom IntentService.
  The com.google.android.c2dm.permission.SEND permission is necessary
  so only GCM services can send data messages for the app.
-->
<receiver
        android:name="com.google.android.gcm.GCMBroadcastReceiver"
        android:permission="com.google.android.c2dm.permission.SEND">
    <intent-filter>
        <!-- Receives the actual messages. -->
        <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
        <!-- Receives the registration id. -->
        <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
        <category android:name="PACKAGE_NAME"/>
    </intent-filter>
</receiver>
<!--
  Application-specific subclass of PushGCMIntentService that will
  handle received messages.
-->
<service android:name="com.arellomobile.android.push.PushGCMIntentService"/>


10. Добавляем следующие ищзменения в AndroidManifest.xml в тэге start activity.

<activity android:name="YourStartActivity"
           android:label="@string/app_name"
           android:configChanges="orientation|keyboardHidden"
           android:launchMode="singleTop"
         >
     <intent-filter>
         <action android:name="PACKAGE_NAME.MESSAGE"/>
         <category android:name="android.intent.category.DEFAULT"/>
     </intent-filter>
     <intent-filter>
         <action android:name="android.intent.action.MAIN"/>
         <category android:name="android.intent.category.LAUNCHER"/>
     </intent-filter>
 </activity>
Для совместимости с Android 4 пожалуйста удостовертесь, что вы используете по крайней мере 11 версию Android API.
SDK будет работать на более старых устройствах.