...

понедельник, 28 декабря 2015 г.

[Из песочницы] Push уведомления в Android. Грабли, костыли и велосипеды

На написание данной статьи меня подтолкнула задача, которая была поставлена передо мной в одном из рабочих проектов: реализовать Push-уведомления в приложении. Казалось, все просто: штудируешь документацию, примеры и вперед. К тому же, опыт работы с уведомлениями уже был. Но не тут то было…
Сервис, в рамках которого реализовано приложение под Android, предъявляет довольно жесткие требования к работе Push-уведомлений. Необходимо в пределах 30-60 секунд оповестить пользователя о некотором действии. При успешном оповещении с устройства пользователя отправляется запрос на сервер с соответствующим статусом. Из документации известно, что сервис GCM (Google Cloud Messaging) не гарантирует доставку PUSH-уведомлений на устройства, поэтому в качестве backdoor варианта, при нарушении этих временных рамок, наш сервис уведомляет пользователя с помощью SMS сообщения. Поскольку стоимость SMS сообщения существенно выше чем PUSH-уведомления, необходимо максимально сократить поток SMS сообщений на клиентские устройства.

Проштудировав документацию и прикрутив пуш-уведомления, мы разослали нескольким клиентам первую сборку приложения для теста и стали ждать. Результаты были примерно следующими:

  • при активном Wifi соединении все работает идеально: уведомления доставляются, клиенты рады.
  • при активном мобильном интернете началось самое веселье.

Некоторые клиенты писали, что испытывают задержки в доставке пушей, либо получали одновременно и PUSH и SMS, что достаточно не практично. Другие писали, что вовсе не получали уведомлений, а только SMS. У третьих, как и у нас на тестовых устройствах, все было ок. Собрав с недовольных клиентов максимально возможную информацию, стали разбираться в проблеме и вывели следующий список ограничений (этот список позже вылился в полноценный FAQ):
  • включенный режим Энергосбережения (например, Stamina на устройствах Sony) влияет на работу Push уведомлений;
  • у пользователя обязательно должен быть минимум 1 активный Google аккаунт на устройстве;
  • необходимо удостовериться в том, что на устройстве установлена актуальная версия приложения “Сервисы Google Play”;
  • проверить, не отключены ли уведомления для приложения (галочка на страничке приложения в настройках телефона);
  • проверить, не ограничена ли работа фонового режима для приложения (настройка расположена в меню «Использование данных»);
  • в документации к GCM указано, что уведомления рассылаются только по определенным портам, поэтому настройки роутера, файервола и антивируса так же стоит учитывать.

Разослав данную памятку по всем клиентам, мы снова стали ждать результатов. И они оказались снова «не очень». Стали копать дальше.

На данном этапе очень сильно помогла статья, написанная ребятами из Mail.ru. В ней очень подробно описаны тонкости реализации GCM на клиентской стороне, а так же моменты, в связи с которыми отказываются работать Push уведомления в мобильных сетях. В конечном счете было принято решение о том, чтобы держать свое соединение с сервером в связке с GCM.

Перед тем, как приступить к решению, стоить выделить несколько очень важных моментов, которые позволяют сузить круг потенциально «нерабочих» устройств:

  • проблема возникает только при подключении к мобильному интернету;
  • по данным клиентов, проблема возникает на версии андроида 4 и выше.

И так, перейдем к реализации.

Бывалый разработчик под Android сходу скажет, что решений задачи как минимум 2: использовать Service или AlarmManager. Мы попробовали оба варианта. Рассмотрим первый из них.

Для того, чтобы создать неубиваемый системой сервис, который постоянно будет висеть в фоне и выполнять нашу задачу, мы использовали метод:

startForeground(int notificationID, Notification notification);


где
  • notificationId — некоторый уникальный идентификатор уведомления, который будет выведен в статус баре и в выезжающей шторке;
  • notification — само уведомление.

В данном случае обязательным условием является отображение уведомления в статус баре. Такой подход гарантирует то, что сервису будет дан больший приоритет (поскольку он взаимодействует с UI частью системы) в момент нехватки памяти на устройстве и система будет выгружать его одним из последних. Нам это уведомление не нужно, поэтому мы воспользовались следующим велосипедом: достаточно запустить одновременно с первым сервисом второй и для обоих сервисов в качестве notificationID использовать одно и тоже значение. Затем убить второй сервис. При этом уведомление пропадет из статус бара, но функциональные и приоритетные возможности первого сервиса останутся.

Реализовав данный подход, мы отправили сборку на тест. По результатам выяснилось, что система все-таки выгружает сервис, а по логам мы видели, как происходили существенные временные разрывы при запросе данных в фоне с нашего сервера. Поэтому приступили к реализации второго варианта — AlarmManager.

AlarmManager — это класс, который предоставляет работу с, грубо говоря, «будильником». Он позволяет указать время, по достижении которого система отправит широковещательное уведомление, которое позволит пробудить наше приложение и даст ему возможность выполнить необходимые действия. В работе этого метода есть некоторые ограничения, и их необходимо обработать:

  • данные о «будильниках» будут стерты после перезагрузки устройства;
  • данные о «будильниках» будут стерты после обновления приложения.

Первыми граблями, на которые мы наступили, был метод
setRepeating()


который позволяет установить повторяющийся с некоторым интервалом «будильник». Прикрутив данный способ, стали тестировать, и тесты показали обратное — «будильник» не повторялся. Стали разбираться в чем дело, посмотрели документацию. И именно там нашли ответ на вопрос — начиная с 19 API lvl (Kitkat) абсолютно все «будильники» в системе стали разовыми. Вывод — всегда читайте документацию.

Эти грабли не были поводом для расстройства, ведь решение задачи довольно простое — запускать единоразовый «будильник» и после срабатывания переустанавливать его. При реализации этого подхода мы наткнулись на следующие грабли — оказалось, что для разных уровней API необходимо по разному устанавливать будильники, при этом в документации ничего сказано не было. Но данная проблема решилась достаточно просто — методом «тыка» и «гугления». Ниже представлен пример кода, позволяющий правильно устанавливать «будильники»:

private static void setUpAlarm(final Context context, final Intent intent, final int timeInterval)
{
    final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    final PendingIntent pi = PendingIntent.getBroadcast(context, timeInterval, intent, 0);
    am.cancel(pi);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
    {
        final AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(System.currentTimeMillis() + timeInterval, pi);
        am.setAlarmClock(alarmClockInfo, pi);
    }
    else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
        am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeInterval, pi);
    else
        am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeInterval, pi);
}


Хочу обратить внимание на флаг AlarmManager.RTC_WAKEUP — именно с помощью него система позволит нашему приложению «проснуться» при неактивном экране, когда устройство находится в заблокированном состоянии.

Данный подход с «будильникам» дал нам нужный результат — приложение в фоне корректно опрашивает сервер на наличие новых данных. Сейчас мы дорабатываем алгоритм. На данный момент реализуем и тестируем следующую оптимизацию, которая позволит сузить круг устройств и тем самым уменьшить нагрузку на сервер:

  • в сообщении, отправленном средствами GCM на устройство, содержится некоторый уникальный ID;
  • получив данные GET запросом в фоновом режиме проверяем, существуют ли уже запись с таким ID на устройстве;
  • если локально на устройстве таких данных нет, мы запоминаем этот ID и время его получения T1;
  • ждем PUSH с таким же ID, при получении запоминаем время T2 и проверяем разницу между T2 и T1;
  • если разница составляет больше некоторого временного критерия (значения), то на устройстве наблюдается проблема с доставкой уведомлений и для корректной работы сервиса необходимо постоянно запрашивать данные в фоновом режиме с сервера (критерий советую выбирать исходя из решаемой задачи. В нашем случае, был выбран критерий равный 5 минутам);
  • данную разницу стоит вычислять несколько раз, например 5-10 раз, только после этого делать вывод о том, что устройство действительно содержит проблему с получением Push уведомлений (таким образом исключается ситуация банального разрыва соединения, таймаута и пр.);
  • необходимо прогонять данный алгоритм периодически (например, раз в неделю, или после обновления ОС на устройстве).

Всем добра. И поменьше подобных костылей.

P.S.
В процессе тестирования очень помог инструмент, который дает возможность посмотреть информацию по отправленным пушам. Этот инструмент доступен разработчикам бесплатно. Рекомендую всем его использовать.

P.S.S.
Предрекаю, в комментариях наверняка будут вопросы о расходе батарейки. Я провел несколько тестов, оставив личный телефон на ночь с включенным мобильным интернетом. Результаты были в районе 20-25% расхода заряда за 8-9 часов. Так же клиенты, которым мы отправляли тестовые сборки, не жаловались на проблемы с увеличением расхода заряда батареи.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

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

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