Создание плагинов cordova (phonegap) для android на примере PUSH сервиса.
- Перед реализацией плагина нужно убедиться, что библиотеки 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).
- В файле res/xml/config.xml в тэге <plugins> декларируем создаваемый на java плагин:
<plugin name="PushPlugin" value="ru.andrew.plugin.PushPlugin"/>
Что означает, что java-класс будет называться PushPlugin, и находиться он будет в пакете ru.andrew.plugin. - Прописываем необходимые разрешения в конфигурационном файле приложения 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:<receiverandroid: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. - Создаём 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;elsereturn false;}} - Создаём 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 {@Overrideprotected void onError(Context arg0, String arg1) {Log.d("GCM onError", arg1);}@Overrideprotected boolean onRecoverableError(Context context, String errorId) {Log.d("GCM onRecoverableError", errorId);return false;}@SuppressWarnings("deprecation")@Overrideprotected 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);}@Overrideprotected void onRegistered(Context context, String arg1) {Log.d("onRegistered", arg1);Log.d("onRegistered", "Зарегистрирован в сервисе получения PUSH-уведомлений");// Toast.makeText(context, "Зарегистрирован в сервисе получения PUSH-уведомлений", Toast.LENGTH_LONG).show();}@Overrideprotected 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
- Для использования русских букв в интерфейсе в тэге <head> нужно добавить строку:
<meta charset="utf-8"> - Для вызова метода плагина необходимо дождаться, чтобы cordova была полностью загружена. Для этого, например, для целей тестирования в файле index.html добавим событие окончательной готовности девайса:document.addEventListener("deviceready", myCallbackFunction, false);Функция myCallbackFunction будет вызвана когда девайс будет готов.
- Реализуем метод 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. - Запускаемся, получаем множество ошибок, мужественно их преодолеваем и идём пить кофе.
Комментариев нет:
Отправить комментарий