вторник, 24 апреля 2012 г.

Оповещаем Android о событиях на сервере

Допустим, у нас есть достаточно популярное Android-приложение, в которое мы хотим добавить опцию оповещения пользователей о каких-либо событиях. Например, пользователь должен иметь возможность оперативно получить новость или персональное сообщение от другого пользователя. Как решить такую задачу?
Решение в виде сервиса на устройстве, который регулярно опрашивает сервер выглядит несовременно, да и опасно: достаточно большое количество пользователей могут задосить нам сервер. Увеличение интервала опроса в нашем случае решает проблему нагрузки с одной стороны, но снижает скорость доставки с другой.
Второй вариант: использовать Cloud to Device Messaging Framework. Действительно хорошее решение, но в ряде случаев не подходит. Во-первых, передаче сообщений через сторонние сервера могут препятствовать соображения конфиденциальности, а во-вторых, если приложение должно поддерживать версию Android 2.1 и ниже, то этот фреймворк нам не поможет.
Третий вариант - использовать свой http push сервер, например nginx + nginx push stream module. Можно поспорить, что лучше, много запросов или много коннектов а также сравнить производительность разных серверов, но цель у нас сейчас другая. Давайте рассмотрим простой вариант реализации такого оповещения.


1. Собираем и конфигурируем nginx


Скачиваем исходники nginx-а и модуля. Выполняем configure c параметром
--add-module=path/to/wandenberg-nginx-push-stream-module

Выполняем make и make install. Ничего необычного.
Также просто можем поступить и с конфигурацией сервера: берём официальный пример настроек, он нас пока вполне устраивает:

  location /pub {
       # activate publisher (admin) mode for this location
       push_stream_publisher admin;
       # query string based channel id
       set $push_stream_channel_id  $arg_id;
   }

   location ~ /sub/(.*) {
       # activate subscriber (streaming) mode for this location
       push_stream_subscriber;
       # positional channel path
       set $push_stream_channels_path $1;
   }


при такой конфигурации можно опубликовать сообщение в канал c именем channel командой:
curl -X POST 'http://myserver.com/pub?id=channel' -d 'Text of message'
и забрать его оттуда:
curl http://myserver.com/sub/channel
Для того, чтобы nginx сохранял сообщения, которые ещё не доставлены добавим ещё две директивы:
push_stream_store_messages on; (в блок location /pub) и push_stream_message_ttl 10m; (в блок http). Параметр во второй директиве говорит, что недоставленное сообщение будет сохраняться в канале 10 минут.
Это только самые необходимые настройки. Остальной процесс тюнинга сервера оставим за рамками этой статьи, тут у каждого админа есть свои особенные приёмы и предпочтения.


2. Делаем тестовое Android-приложение


На клиентской стороне нам понадобится “слушать” nginx из сервиса и переподключаться к нему при любых сбоях, если сеть доступна в данный момент. Для того чтобы иметь достаточно “живучий” сервис, используем библиотеку WakefulIntentService. Для работы этого компонента нам потребуется разрешение android.permission.WAKE_LOCK. Добавим в AndroidManifest.xml остальные разрешения и объявление сервиса и получим:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="net.multipi.messager"
 android:versionCode="1"
 android:versionName="1.0" >
 
 <uses-sdk android:minSdkVersion="7" />
 
 <uses-permission android:name="android.permission.INTERNET"/>
 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 <uses-permission android:name="android.permission.WAKE_LOCK"/>
 <uses-permission android:name="android.permission.VIBRATE"/>
 
 <application
     android:icon="@drawable/ic_launcher"
     android:label="@string/app_name" >
     <activity
         android:name=".MessagerActivity"
         android:label="@string/app_name" >
         <intent-filter>
             <action android:name="android.intent.action.MAIN" />
             <category android:name="android.intent.category.LAUNCHER" />
         </intent-filter>
     </activity>
     <service android:name=".AppService" />
 </application>
</manifest>


Главное Activity будет предельно простым, в его метод onCreate добавим только запуск сервиса:
WakefulIntentService.sendWakefulWork(getApplicationContext(), AppService.class);
Тут AppService - класс, расширяющий WakefulIntentService. Он и будет делать всю работу, незримо но надёжно :)
Вот его код:



package net.multipi.messager;
 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
 
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
 
import com.commonsware.cwac.wakeful.WakefulIntentService;
 
public class AppService extends WakefulIntentService {
 
 private static final String SUB_HOST = "http://myserver.com/sub/channel";
 
 public AppService() {
  super("AppService");
 }
 
 @Override
 protected void doWakefulWork(Intent intent) {
  while(true) {
   try {
    if (isOnline()) {
     readMessage();
    }
   } catch (Exception e) {
    e.printStackTrace();
   }
  }
 }
 
 private void readMessage() throws Exception {
  HttpURLConnection conn = (HttpURLConnection) new URL(SUB_HOST).openConnection();
  conn.setUseCaches(false);
        conn.setDoOutput(true);
        conn.setDoInput(true);
 
        int respCode = -1;
        for (int i=0; i<10 && respCode != HttpURLConnection.HTTP_OK; i++){
            respCode = conn.getResponseCode();
            Thread.sleep(100);
        }
        if (respCode != HttpURLConnection.HTTP_OK) {
            throw new Exception("http error code " + respCode);
        }
 
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String message = br.readLine();
        sendNotify(getResources().getString(R.string.msg_title), message);
 }
 
 private boolean isOnline() {
  ConnectivityManager conMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
  NetworkInfo i = conMgr.getActiveNetworkInfo();
  if (i == null || !i.isConnected()) {
   return false;
  }
  return true;
 }
 
 private void sendNotify(String title, String message) {
  NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
  Notification notification = new Notification(android.R.drawable.star_off, title, System.currentTimeMillis());
  notification.vibrate = new long[]{0,100,200,300};
  notification.defaults = Notification.DEFAULT_ALL;
  notification.flags |= Notification.FLAG_AUTO_CANCEL;
  PendingIntent intent = PendingIntent.getActivity(getApplication(), 0, new Intent(), 0);
  notification.setLatestEventInfo(getApplication(), title, message, intent);
  notificationManager.notify(0, notification);
 }
}


В методе doWakefulWork запускаем вечный цикл, в котором проверяем есть ли подключение к сети. Проверку выполняем в методе isOnline, реализованном тут же. Если подключение обнаружено, будет вызван блокирующий метод readMessage, который остановит выполнение вечного цикла до прихода сообщения с сервера. В методе readMessage мы открываем соединение к нашему серверу и читаем статус ответа, а при успешном статусе и строку ответа, которую выводим методом sendNotify. Вывод выполняем в строку статуса устройства со значком, вибрацией и т.п.
Для проверки работы собранной нами связки запустите приложение на устройстве, закройте его, отправьте curl - запрос к своему серверу и наблюдайте как в строке статуса вашего Android-устройства появится сообщение с текстом, который вы отправили.
Чтобы отправлять "адресные" сообщения на конкретное устройство, следует каждому из устройств выделить отдельное название канала. Учитывая, что имя канала - просто строка, можно составить её как хэш от любого известного на сервере идентификатора устройства или пользователя: например IMEI или номера телефона.

Комментариев нет:

Отправить комментарий