Написать пост

Приключения в Android: уведомления пользователей

Аватар Типичный программист

Обложка поста Приключения в Android: уведомления пользователей

Позвольте мне рассказать вам одну историю. В Spire мы постоянно создаем и поддерживаем передовые платформы. И это учитывая то, что наша команда состоит всего лишь из 4 разработчиков и одного дизайнера. Вместе мы создали несколько клевых нативных приложений для iOS, Android и Web. С такой маленькой группой мы не можем реализовывать крупные проекты из-за нехватки времени. Однако совсем недавно Urban Airship уведомили нас, что они изменили свою бизнес-модель и отключили наши push-уведомления, пока мы не заплатим довольно внушительную сумму. С этих самых пор мы начали свое маленькое исследование.

Примерно за неделю я вместе с Робом создал легковесный кроссплатформенный сервер push-уведомлений, используя Node.js. Немного подумав, мы пришли к выводу, что наша ситуация с Urban Airship, возможно, не единична. Поэтому мы открыли исходный код нашего проекта — yodel — для всех нуждающихся. Я, будучи прежде всего Android-инженером, люблю изучать новые технологии, разрабатываемые Google. Именно поэтому я хотел попробовать функциональность “уведомления пользователя” (User Notification), о котором мы слышали в конференции Google I/O 2013.

Возможности user notification были недоступны для некоторых разработчиков довольно долгое время, но, к счастью, сейчас ими могут воспользоваться все желающие. Из-за предыдущих ограничений популярный проект node-gcm, который мы использовали для Android, поддерживает структуру только классических сообщений. Именно поэтому я решил взяться за еще один проект, расширяющий возможности node-gcm и добавляющий поддержку последних функций Google Cloud Messaging.

Так что же такое User Notification?

Давайте рассмотрим принцип работы push-уведомлений под Android. Есть три ключевых объекта:

  • Android-устройство, на котором запущено ваше приложение;
  • ваш сервер уведомлений;
  • сервис Google Cloud Messaging.

Процесс принятия push-уведомления вашим приложением состоит из нескольких шагов:

  1. Ваше приложение регистрирует Android-устройство, на котором оно запущено, в сервисе Google Cloud Messaging.
  2. Ваше приложение принимает регистрационный ID, отправленный на устройство сервисом Google Cloud Messaging (разумеется, при успешном выполнении предыдущего шага).
  3. Ваше приложение отправляет регистрационный ID в ваш сервер, где он хранится для дальнейшего использования.
  4. Ваш сервер уведомлений отправляет сообщение в Google Cloud Messaging, указав регистрационный ID устройства в качестве получателя (получателей может быть множество).
  5. Google Cloud Messaging принимает ваше сообщение и отправляет его тем устройствам, ID которых вы указали на предыдущем шаге.
  6. Android-устройство, принимая сообщение, перенаправляет его в ваше приложение, которое отображает уведомление в нужной вам форме.

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

Система оповещения пользователей добавляет возможность создавать отношение один-ко-многим. Таким образом, вы, вместо того, чтобы отправлять уведомления пользователям, указав список устройств на вашем сервере, предоставляете один токен, называющийся “ключом уведомлений” (notification key). Этот ключ предоставляет из себя длинную строку, сгенерированную по запросу сервисом Google Cloud Messaging. Для того, чтобы иметь ключ уведомления для конкретного пользователя, вы должны найти из списка идентификаторов регистрационный ID того устройства, который привязан к этому пользователю. Затем предоставить локально-уникальную строку этого ID, и, наконец, отправить эти данные с помощью HTTP запроса POST в сервис Google Cloud Messaging. В ответ вы получите новый ключ уведомления, привязанный к конкретному устройству. В будущем, когда пользователь добавляет или удаляет устройство, вы можете просто отправлять не всю информацию, а только о внесенных изменениях. В статье я покажу, как все это происходит. Стоит только помнить, что управляет списком устройств сервис Google Cloud Messaging, а не вы.

В дополнение к основным возможностям, система user notification включает возможность “обратной связи” (upstream messaging). Что это означает? Это говорит о том, что при правильной настройке вы можете отправить сообщение, сгенерированное при помощи GCM, обратно к вам на сервер. По сути система upstream messaging работает как XMPP сервер. А это в свою очередь говорит о том, что для того, чтобы ваш сервер принимал такое сообщение от устройств, вам надо настроить локальный XMPP клиент для аутентификации и подключиться к GCM. Однако, если вы всего лишь хотите отправить сообщение с одного устройства на другие, зарегистрированные ключом уведомлений, то библиотека GCM позволит сделать это максимально просто, минуя собственный сервер.

Серверная сторона: управление ключами уведомлений

Мы будем считать, что пользователи могут выбирать: быть подписанным на уведомления или не получать их вообще. Yodel будет принимать команды только через общую очередь Redis. Поэтому я создал быстрый (и не очень красивый) HTTP сервер для тестирования и демонстрации возможностей API сервера уведомлений.

Примечание автора Я не собираюсь вникать в детали XMPP в этом посте, но советую вам следить за моими будущими статьями.

Когда пользователь впервые соглашается получать уведомления (а это 3 шаг из приведенного выше списка), сервер получает регистрационный номер устройства. Разумеется, мы не имеем еще ни одного ключа уведомления для этого девайса. Для того, чтобы его получить, нам следует отправить POST запрос в сервис GCM. Этот POST запрос в формате JSON требует тип операции, имя ключа уведомления (как правило это ID вашего приложения на устройстве пользователя) и список начальных идентификаторов регистрации устройства. Ко всем запросам на получение ключа уведомления мы должны прикладывать следующие HTTP заголовки для правильного анализа данных и проверки на подлинность:

			Content-Type: "application/json"
project_id: "<PROJECT NUMBER>"
Authorization: "key=<SERVER API KEY>"
		

Где PROJECT NUMBER — это номер проекта Google API, а SERVER API KEY — ключ сервера Google Api.

Теперь мы должны выполнить операцию create. Тело HTTP запроса должно иметь следующий вид в JSON:

			request: { 
   "operation": "create",
   "notification_key_name": "<yourUserIdentifier>",
   "registration_ids": ["SFKGKjflskfjQF", "AdfEfdfDf234", etc...]
}
		

После успешного POST запроса вы должны получить в ответ JSON файл, содержащий ключ уведомления:

			{ "notification_key": "yourSuperLongNotificationKey" }
		

Когда вы получите ключ уведомления, вы должны его надежно сохранить. Обратите внимание на то, что если вы потеряете ключ, то заново его сгенерировать уже не получится. Если вы все же попытаетесь получить новый ключ, когда уже потеряли предыдущий, то в ответ получите ошибку №400. Чтобы предотвратить такие случаи, советую вам создать особую структуру для хранения ключа. Еще один вариант – создать такую систему, которая бы позволяла сгенерировать новое имя ключа уведомления, по которому можно будет запросить, собственно, сам ключ.

В случае, если какой-то пользователь устанавливает ваше приложение на новое устройство и подписывается на уведомления, то вам нужно будет связать это самое устройство с уже существующим ключом уведомления. Это делаетcя путем выполнения операции add с использованием того же HTTP endpoint (endpoint – ссылка, по которой возможен доступ к какому то сервису через клиентское приложение).

			request: { 
   "operation": "add",
   "notification_key_name": "<userIdentifier>",
   "notification_key": "<existingNotificationKey>",
   "registration_ids": ["<deviceRegistrationId>"]
}
		

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

Примечание Запрос можно выполнить и без имени ключа уведомления. Тем не менее, явное указания этого поля предотвращает потенциальные несоответствия. К тому же, данный способ рекомендуется Google.

В противоположность предыдущему случаю, может произойти так, что пользователь отписывается от уведомлений на каком–либо устройстве. При таком раскладе вам нужно будет выполнить операцию remove. Построение запроса аналогично предыдущему, за исключением типа операции:

			request: { 
   "operation": "remove",
   "notification_key_name": "<userIdentifier>",
   "notification_key": "<existingNotificationKey>",
   "registration_ids": ["<deviceRegistrationId>"]
}
		

Когда какой-либо запрос удачно завершится (будь то create, add или remove), то ответ будет содержать один ключ уведомления.

А в конце обращу ваше внимание на то, что мы не реализовали операцию remove. Вы не можете явно взять и удалить ключ уведомления. Тем не менее, ключ уведомления будет удален автоматически, если были удалены все связанные с ним регистрационные ID. Но в таком методе основной трудностью является определение, что был удален именно последний ID. Во всех случаях результатом POST запроса является ключ уведомления, даже если он автоматически удаляется в этот же момент. Я решил эту проблему повторением операции add, автоматически переключая ее в операцию create при получении ошибки №400 о несуществующем ключе уведомления. Это, конечно, не самый правильный способ, но довольно эффективный.

Серверная сторона: отправка уведомления

Я не буду тратить наше драгоценное время на объяснение основ отправки уведомлений пользователям. Этот процесс практически не отличается от отправки обычного push-уведомления за исключением парочки особенностей. Официальной документации, обычно, хватает с лихвой. Также вы можете ориентироваться на наш проект yodel-gcm (см. файл sender_base.js).

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

			Content-Type: "application/json"
Authorization: "key=<SERVER API KEY>"
		

Содержимое JSON файла для отправки запроса может выглядеть примерно так:

			{ 
  "data": {
    "message": "You have received a notification!",
    "randomKey": "randomValue"
  },
  "to": "<notification key>"
}
		

Как вы могли заметить, процесс отправки уведомления практически не отличается от отправки push-уведомления. Однако вместо атрибута registration_ids мы используем атрибут to для указания ключа уведомления принимаемого устройства. Обратите внимание, что документация по GCM требует указывать ключ уведомления в атрибуте notification_key. Однако этот атрибут не распознается в GCM endpoint, несмотря на то, что это указано в официальной документации. Да, и в документации бывают ошибки, но я нашел решение на сайте StackOverflow.

Как только информация успешно отправлена в GCM, вы должны получить ответ со статусом 200 и кое-какую дополнительную информацию:

			{
  "success": 2,
  "failure": 0
}
		

Здесь вы можете наблюдать количество устройств, куда успешно отправлены уведомления и куда они совсем не дошли. Если же данные отправились частично, то результат будет примерно таким:

			{
  "success":1,
  "failure":2,
  "failed_registration_ids":[
     "regId1",
     "regId2"
  ]
}
		

В этом примере вы видите, что одно устройство успешно получило уведомление, однако 2 другим устройствам не удалось получить данные. Если бы вы отправляли обычное push-уведомление, то в случае неудач, следовало повторно выполнить отправку данных на те устройства, которые их в прошлый раз не получили. Документация GCM рекомендует просто-напросто повторить последний запрос. Но в этом случае вы отправите уведомление абсолютно всем устройствам, даже тем, которые успешно приняли данные в прошлый раз. Чтобы решить эту проблему, следует создать обычный запрос send, но дополненный атрибутом registration_ids. Значение этого атрибута будет равно массиву идентификаторов устройств, которые не получили уведомлений. Этот массив можно взять из ответа выше.

Клиентская сторона: основы и некоторые плюшки

На стороне клиента никаких значимых изменений делать не нужно, и это хорошо. Это позволяет вам совершенствовать свой сервер без каких-либо потерь среди пользователей. (Если вам нужно освежить свои знания о реализации GCM клиента, вы всегда можете взглянуть на официальную документацию). Как было отмечено ранее, я не буду останавливаться на реализации XMPP клиента. Однако я провел небольшой эксперимент с передачей сообщения по типу устройство-устройство.

Чтобы начать обмен сообщениями между устройствами, вам следует передать ключ уведомления клиенту. В идеале вы должны отправить запрос subscribe, который бы вернул вам ключ уведомления. Затем вы сможете передать сообщение всем устройствам, которые связаны с этим ключом, с помощью такого кода:

			final notificationKey = getNotificationKey(); // не показывать
final GoogleCloudMessaging gcm = 
        GoogleCloudMessaging.getInstance(CONTEXT);
// gcm.send(...) не будет (и не должен) выполняться в главном потоке
new Thread(new Runnable() {
    @ Override
    public void run() {
        try {
            Bundle data = new Bundle();
            // строка "user_command" может быть каким угодно
            data.putString(“user_command”, “dismiss_all”);
            gcm.send(notificationKey, “user_command”, data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();
		

Затем в вашей GCM Intent Service, который отслеживает входящие уведомления, вы можете добавить это:

			...
if(GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) {
    // проверяем команду
    String command = extras.getString(“user_command”);
    if (command != null && command.equals(“dismiss_all”)) {
           // отключаем все уведомления!
           getNotificationManager().cancelAll();
       }
    } else {
       // обработка ошибок
    }
}
...
		

В первой же строчке внутри условия в переменную command я записываю нужную команду, которую принял в атрибуте user_command. Мое приложение на основании этой переменной может реагировать соответствующим образом. Конкретно в этом случае, если я получил команду dismiss_all, то отменяю подписку на все уведомления. Однако переданная команда может быть какой угодно, возможности практически безграничны!

Для более полного ознакомления с обменом сообщениями по типу устройство-устройство вы можете взглянуть на демо-приложение в GitHub.

Миф об автосинхронизированных уведомлениях

Во время конференции Google I/O в 2013 году ведущие представили на всеобщее обозрение удивительную функцию, которая сначала возбудила мой интерес. Заключалась она в том, что автосинхронизированные уведомления удаляются с другого устройства пользователя, если оно было обработано. Например, у пользователя 2 устройства, на оба из них приходят уведомления. Пользователь свайпом удаляет уведомление на одном устройстве, и оно же удаляется и на втором. В официальной документации написано:

Если сообщение было обработано на одном устройстве, то GCM сообщения с другого также будут удалены. Например, если пользователь обработал уведомление календаря на своем девайсе, то оно будет удалено и на других его устройствах.

На момент написания статьи автосинхронизированные уведомления не работают. Но есть выход! Используя обмен сообщениями между устройствами, о котором говорилось выше, вы можете легко отследить обработку уведомления. Как только оно будет удалено пользователем, вам следует отправить всем остальным устройствам запрос на удаление этого уведомления. Реализация этого подхода не потребует много усилий.

Вывод

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

Как я уже говорил на протяжении всей статьи, все исходники открыты. Представляем вам:

yodel – легковесный сервер push-уведомлений для Android и iOS. Написан с использованием Node.js и построен на очереди команд Redis. (Взгляните на ветку feature/usernotifications, чтобы увидеть, как я интегрировал возможность оповещения пользователей, используя yodel-gcm).

yodel-gcm – наш форк популярной библиотеки node-gcm для отправки push-уведомлений.

yodel-demo-http-server – построенный на скорую руку (а потому не самый красивый) HTTP сервер для демонстрации Yodel.

yodel-demo-android – демонстрационное Android-приложение, которое подключается к демонстрационному HTTP серверу.

Перевод статьи “Adventures in Android: User Notifications”

Следите за новыми постами
Следите за новыми постами по любимым темам
17К открытий17К показов