Пара способов отправить уведомления на смартфон со своего сервера

в 14:25, , рубрики: android, Google API, php, домашний сервер, Программирование, Разработка под android, Системы обмена сообщениями, уведомления

В этом туториале я рассмотрю пошагово, как отправлять со своего сервера уведомления на свой (или не свой) смартфон, какие средства для этого понадобятся. Эти способы универсальны и подойдут для любого языка программирования, т.к. напрямую используют API гугла, без использования библиотек. Отправить можно на смартфоны с Android, iOS и в браузеры с поддержкой Push API (на сегодня это Chrome, Firefox и их производные).

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

Немного истории. В начале (с версии андроида 2.2) у гугла для доставки использовалась система C2DM (Android Cloud to Device Messaging), начиная с июня 2012 для этого стали предлагать использовать GCM (Google cloud messaging).

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

Технически, уведомления отправляются с сервера не напрямую в смартфон, а на некий промежуточный сервер, на котором при необходимости хранятся до 4-х недель (настраиваемо), и по возможности отправляются получателю. Т.е. если смартфон находится оффлайн, сервер ждёт. Как только появляется возможность — отправляет.

1. Регистрируемся в Firebase

Для регистрации в Firebase понадобится учётка гугла.

Пара способов отправить уведомления на смартфон со своего сервера - 1

Жмём «Перейти к консоли».

Пара способов отправить уведомления на смартфон со своего сервера - 2

Затем «Добавить проект».

Пара способов отправить уведомления на смартфон со своего сервера - 3

Вводим название проекта. Рекомендую в диапазоне 8-16 символов.
Выбираем страну. Жмём «Создать проект».

2. Настраиваем Firebase

Пара способов отправить уведомления на смартфон со своего сервера - 4

Прокручиваем до блока «Notifications», жмём «Начать».

Вам предложат выбрать приложение, для которого ваши уведомления будут отправляться.

Пара способов отправить уведомления на смартфон со своего сервера - 5

Шаги для Andriod-приложения:

Пара способов отправить уведомления на смартфон со своего сервера - 6

Шаг 1 — Вводим название проекта на Andriod.
Жмём «Зарегистрировать приложение».

Пара способов отправить уведомления на смартфон со своего сервера - 7

Шаг 2 — Жмём «Скачать google-services.com».
Добавляем скачанный файл конфигурации в проект, рядом с файлом build.gradle (тем, который персональный для приложения).
Жмём «Продолжить».

Пара способов отправить уведомления на смартфон со своего сервера - 8

Шаг 3 — Добавляем в проект зависимости.
в файл /build.gradle строчку
classpath 'com.google.gms:google-services:3.1.0'
в файл /<app-module>/build.gradle строчку
apply plugin: 'com.google.gms.google-services'
Тут всё, жмём «Готово».

После настройки приложения, можно сразу протестировать работает ли связь отправив тестовое сообщение (нет нельзя, у нас ещё нет ID клиента, куда слать).

3. Настройка приложения Android на приём уведомлений.

Важное примечание: некоторые оболочки, например MIUI, могут блокировать уведомления, если приложение не запущено или не висит в фоне. Делается это якобы для экономии заряда батареи.

Грубо говоря, отправлять можно два вида уведомлений:
— уведомление по запросу,
— уведомление с полезной нагрузкой.
У них разные способы взаимодействия с приложением.

Уведомление по запросу выведет уведомление в области уведомлений, но только в случае если приложение свёрнуто. При тапе пользователя оно откроет заранее выбранную (при отправке) активити приложения, и передаст бандлом экстра-параметры.

Уведомление с полезной нагрузкой требует наличия в приложении пары служб, в которые и передаётся управление, но на длительность не дольше 10 секунд.

Ниже приведён пример службы, которая отвечает за генерацию ID клиента.

package ru.pyur.loga;

import android.util.Log;

import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.FirebaseInstanceIdService;



public class TestFirebaseInstanceIdService extends FirebaseInstanceIdService {
  public static final String TAG = "TestFbseInstIdSvc";

  @Override
  public void onTokenRefresh() {
    String refreshedToken = FirebaseInstanceId.getInstance().getToken();
    Log.d(TAG, "Refreshed token: " + refreshedToken);

    //~sendRegistrationToServer(refreshedToken);
  }

}

И пример кода службы, принимающей сообщения. Приложение должно быть запущено, или висеть в фоне, иначе не гарантируется приём сообщений. Некоторые оболочки, например MIUI, в целях экономии, режут всё подряд, в том числе привелегии фоновых служб.

package ru.pyur.loga;

import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

import static ru.pyur.loga.AcMain.context;



public class TestFirebaseMessagingService extends FirebaseMessagingService {
  public static final String TAG = "TestFbseMsgngSvc";

  @Override
  public void onMessageReceived(RemoteMessage remoteMessage) {
    Log.d(TAG, "From: " + remoteMessage.getFrom());

    if (remoteMessage.getData().size() > 0) {
      Log.d(TAG, "Message data payload: " + remoteMessage.getData());

      String val1 = remoteMessage.getData().get("val1");
      String val2 = remoteMessage.getData().get("val2");
      String val3 = remoteMessage.getData().get("val3");
      int color = (1<<16)|(1<<8)|(0);
      ShowNotification(val1, val2, color);
    }

    if (remoteMessage.getNotification() != null) {
      Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
    }
  }


  @Override
  public void onDeletedMessages() {
    // In some situations, FCM may not deliver a message. This occurs when there are too many messages (>100) pending for your app on a particular device
    // at the time it connects or if the device hasn't connected to FCM in more than one month. In these cases, you may receive a callback
    // to FirebaseMessagingService.onDeletedMessages() When the app instance receives this callback, it should perform a full sync with your app server.
    // If you haven't sent a message to the app on that device within the last 4 weeks, FCM won't call onDeletedMessages().
  }


  void ShowNotification(String title, String text, int color) {
    NotificationCompat.Builder mNotify = new NotificationCompat.Builder(context, "");
    mNotify.setLights(color, 100, 200);
    mNotify.setSmallIcon(R.drawable.service_icon);
    mNotify.setContentTitle(title);
    mNotify.setContentText(text);
    mNotify.setDefaults(Notification.DEFAULT_SOUND);

    NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

    int mId = 1001;
    try { mNotificationManager.notify(mId, mNotify.build()); }
    catch (Exception e) { e.printStackTrace(); }
  }


}

не забудьте прописать службы в манифесте.

<service
  android:name=".TestFirebaseMessagingService">
  <intent-filter>
    <action android:name="com.google.firebase.MESSAGING_EVENT"/>
  </intent-filter>
</service>


<service
  android:name=".TestFirebaseInstanceIdService">
  <intent-filter>
    <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
  </intent-filter>
</service>

ID клиента генерируется на устройстве, но вы сами выбираете способ доставки этого ID к себе на сервер.

Вот теперь можно протестировать, отправив тестовое сообщение из консоли.

Пара способов отправить уведомления на смартфон со своего сервера - 9

Пара способов отправить уведомления на смартфон со своего сервера - 10

4. Отправляем уведомление со своего сервера

Существует несколько способов обмена данными с сервером Firebase. Мы рассмотрим два способа обмена по протоколу HTTP.

Протокол первого поколения — Legacy HTTP

Пара способов отправить уведомления на смартфон со своего сервера - 11

Понадобится ключ. Жмём на гайку, выбираем «Настройки проекта».

Пара способов отправить уведомления на смартфон со своего сервера - 12

Вкладка «Cloud Messaging».
Копируем «Устаревший ключ сервера».

<?php
  // ------------------------ test fcm send. legacy ------------------------ //

$socket = @fsockopen('ssl://fcm.googleapis.com', 443, $errno, $errstr, 10);

if (!$socket)  die('error: remote host is unreachable.');


  // ---- уведомление для трея ---- //
$payload = '{
  "to" : "cGAFgPJGf-s:APA91bF**...**aEVM17c9peqZ",
  "notification" : {
    "title" : "Моё первое сообщение",
    "body" : "(Legacy API) Привет!",
    "sound": "default"
  }
}';
// или
  // ---- уведомление для службы ---- //
$payload = '{
  "to" : "cGAFgPJGf-s:APA91bF**...**aEVM17c9peqZ",
  "data":{
    "val1" : "Моё первое сообщение",
    "val2" : "(Legacy API) Привет!",
    "val3" : "какие-то дополнительные данные"
  }
}';


$send  = '';
$send .= 'POST /fcm/send HTTP/1.1'."rn";
$send .= 'Host: fcm.googleapis.com'."rn";
$send .= 'Connection: close'."rn";
$send .= 'Content-Type: application/json'."rn";
$send .= 'Authorization: key=AIzaSy***************************IPSnjk'."rn";
$send .= 'Content-Length: '.strlen($payload)."rn";
$send .= "rn";

$send .=$payload;

$result = fwrite($socket, $send);


$receive = '';
while (!feof($socket))  $receive .= fread($socket, 8192);

fclose($socket);

echo '<pre>'.$receive.'</pre>';

?>

Здесь в поле «to» надо подставить ID клиента. В http заголовок «Authorization: key=» подставить «Устаревший ключ сервера».

Протокол второго поколения — (Modern) HTTP v1.

(источник: developers.google.com/identity/protocols/OAuth2ServiceAccount)
Не спрашивайте, почему вторая версия протокола называется V1, видимо первая считалась бетой и носила нулевой номер.
Я не углублялся в подробности, но так понимаю этот протокол более универсальный и имеет более широкие возможности, чем просто отправка уведомлений.

<?php
  // ------------------------ test fcm send. modern ------------------------ //

  // -- шаг 1. вычисляем JWT -- //
$JWT_header = base64_encode('{"alg":"RS256","typ":"JWT"}');

$issue_time = time();

$JWT_claim_set = base64_encode(
'{"iss":"firebase-adminsdk-mvxyi@<your-project>.iam.gserviceaccount.com",'.
 '"scope":"https://www.googleapis.com/auth/firebase.messaging",'.
 '"aud":"https://www.googleapis.com/oauth2/v4/token",'.
 '"exp":'.($issue_time + 3600).','.
 '"iat":'.$issue_time.'}');
  // см. примечание

$private_key = '
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwR1biSUCv4J4W
****************************************************************
****************************************************************
...
****************************************************************
teTJImCT6sg7go7toh2ODfaPmeI0nA/LwSjzWs0b8gdIYPT5fAsvfQiND0vu/M3V
7C/z/SmIKeIcfOYrcbWQwTs=
-----END PRIVATE KEY-----
';

$data = $JWT_header.'.'.$JWT_claim_set;
$binary_signature = '';

openssl_sign($data, $binary_signature, $private_key, 'SHA256');

$JWT_signature = base64_encode($binary_signature);


$JWT = $JWT_header.'.'.$JWT_claim_set.'.'.$JWT_signature;



  // -- шаг 2. авторизируемся и получаем токен -- //

$socket = @fsockopen('ssl://www.googleapis.com', 443, $errno, $errstr, 10);

if (!$socket)  die('error: remote host is unreachable.');


$payload = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion='.rawurlencode($JWT);

$send  = '';
$send .= 'POST /oauth2/v4/token HTTP/1.1'."rn";
$send .= 'Host: www.googleapis.com'."rn";
$send .= 'Connection: close'."rn";
$send .= 'Content-Type: application/x-www-form-urlencoded'."rn";
$send .= 'Content-Length: '.strlen($payload)."rn";
$send .= "rn";

$send .= $payload;


$result = fwrite($socket, $send);

$receive = '';
while (!feof($socket))  $receive .= fread($socket, 8192);

fclose($socket);

echo '<pre>'.$receive.'</pre>';



  // -- parse answer JSON (lame) -- //

$line = explode("rn", $receive);
if ($line[0] != 'HTTP/1.1 200 OK')  die($line[0]);

$pos = FALSE;
if (($pos = strpos($receive, "rnrn", 0)) !== FALSE ) {
  if (($pos = strpos($receive, "{", $pos+4)) !== FALSE ) {
    if (($pose = strpos($receive, "}", $pos+1)) !== FALSE ) {
      $post = substr($receive, $pos, ($pose - $pos+1) );
      $aw = json_decode($post, TRUE);
      $access_token = $aw['access_token'];
      }
    else die('} not found.');
    }
  else die('{ not found.');
  }
else die('rnrn not found.');



    // -- шаг 3. отправляем запрос на Firebase сервер -- //

$socket = @fsockopen('ssl://fcm.googleapis.com', 443, $errno, $errstr, 10);

if (!$socket)  die('error: remote host is unreachable.');


$payload = '{
  "message":{
    "token" : "cGAFgPJGf-s:APA91bF**...**aEVM17c9peqZ",
    "notification" : {
      "title" : "Заголовок сообщения",
      "body" : "(Modern API) Моё первое сообщение через Firebase!"
      }
   }
}';
// или
  $payload = '{
"message": {
  "token" : "cGAFgPJGf-s:APA91bF**...**aEVM17c9peqZ",
  "data":{
    "val1" : "Заголовок сообщения",
    "val2" : "(Modern API) Моё первое сообщение через Firebase!",
    "val3" : "дополнительные данные"
    }
  }
}';


$send  = '';
$send .= 'POST /v1/projects/pyur-test-id/messages:send HTTP/1.1'."rn";
$send .= 'Host: fcm.googleapis.com'."rn";
$send .= 'Connection: close'."rn";
$send .= 'Content-Type: application/json'."rn";
$send .= 'Authorization: Bearer '.$access_token."rn";
$send .= 'Content-Length: '.strlen($payload)."rn";
$send .= "rn";

$send .=$payload;


$result = fwrite($socket, $send);

$receive = '';
while (!feof($socket))  $receive .= fread($socket, 8192);

fclose($socket);


echo '<pre>'.$receive.'</pre>';

?>

Пара способов отправить уведомления на смартфон со своего сервера - 13

по адресу console.firebase.google.com/project/poject-id/settings/serviceaccounts/adminsdk надо скопировать «Сервисный аккаунт Firebase» и подставить в переменную "$JWT_claim_set", в поле «iss».

Жмём «Создание закрытого ключа»

Пара способов отправить уведомления на смартфон со своего сервера - 14

Создаём ключ, сохраняем, никому не показываем. В скачанном файле будет содержаться «Закрытый ключ», его подставляем в переменную "$private_key".

Хинт: токен, полученный в шагах 1 и 2 можно и нужно кешировать в локальном временном хранилище, например файле, или базе данных. И только по истечении времени (по умолчанию один час), запрашивать у сервера авторизации следующий токен.

Пара способов отправить уведомления на смартфон со своего сервера - 15

Важно! Перед использованием Modern Http API необходимо явно разрешить его использование здесь: console.developers.google.com/apis/library/fcm.googleapis.com/?project=your-project

Бонус, дополнительные параметры для уведомлений:

sound — либо «default», либо имя ресурса в приложении. Должен располагаться в "/res/raw/". Формат MP3, AAC или ещё чего подходящее.
icon — меняет иконку уведомления. Должна храниться в «drawable» приложения. Если отсутствует, FCM будет использовать иконку приложения (указанную как «launcher icon» в манифесте приложения).
tag — Следует использовать для группировки однотипных уведомлений. Новые уведомления будут выводиться поверх уже имеющихся с таким же тегом.
color — цвет иконки, задаётся как "#rrggbb" (у меня в MIUI не заработало)
click_action — запускаемое активити, при нажатии пользователем на уведомлении.

Заключение

В будущем API вероятно будет изменяться, объявляться depricated и т.п. Поэтому сегодня думаю стоит делать сразу на протоколе HTTP v1.

Мне будет интересно почитать в комментариях оригинальные способы применения уведомлений, помимо новых сообщений из вконтактика. К примеру у меня настроен мониторинг вентиляторов ардуиной, и если они остановятся, отправляется уведомление.

Да, я в курсе, что существует Zabbix и т.п., но тема статьи — домашние сервера, и прочие умные дома. Считаю системы корпоративного класса перебором в любительских поделках.

Автор: Юрий Пресняков

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js