Translate

пятница, 24 февраля 2017 г.

Настройка пуш уведомлении через сервис Firebase для ANDROID и IOS [часть 2]

Доброго времени суток!

В предыдущей части, я рассказывал как сделать настройку пуш уведомления в консоли Firebase, в этой я покажу код на Delphi и серверную часть на PHP

Настройка FMX проекта

ANDROID

1) Настроим проект на прием пушей в Android (Debug/Release сборки)

IDE - Project - Options - Entitlement List - Recieve push notification = true


2) Теперь нам нужен ключ сервера из консоли Firebase (номер 1)


3) Копируем его и вставляем в IDE - Project - Options - Version Info - apiKey


4) В папке с проектом находим AndroidManifest.template.xml, открываем редактором и вставляем строчку 
<service android:name="com.embarcadero.gcm.notifications.GCMIntentService" />
после 
<%receivers%>

5) Проверьте название пакета вашего приложения (7 пункт в предыдущей статье, настройка Android)

6) Готово!

iOS

1) Открываем IDE - Project - Options - Version Info
CFBundleIdentifier вашего проекта должно совпадать с настройкой проекта в Firebase



2) Готово!

Delphi код

1) uses секция
System.PushNotification
{$IFDEF ANDROID}, FMX.PushNotification.Android{$ENDIF}
{$IFDEF IOS}, FMX.PushNotification.IOS{$ENDIF}
2) Объявим константу - это Идентификатор отправителя (см. 2 пункт настройка Android номер 2)
const
  FAndroidServerKey = '63538920422';
3) В private секции формы пишем следующее
  private
    { Private declarations }
    FDeviceID: string;
    FDeviceToken: string;

    FPushService: TPushService;
    FPushServiceConnection: TPushServiceConnection;

    procedure OnReceiveNotificationEvent(Sender: TObject; 
      const ANotification: TPushServiceNotification);
    procedure OnServiceConnectionChange(Sender: TObject; 
      AChange: TPushService.TChanges);

    procedure PushServiceRegister;
3) Реализация методов
update 12.04.2017
procede TFormMain.OnReceiveNotificationEvent(Sender: TObject; 
  const ANotification: TPushServiceNotification);
const
  FCMSignature = 'gcm.notification.body';
  GCMSignature = 'message';
  APNsSignature = 'alert';
var
  aText: string;
  aObj: TJSONValue;
begin
  // это событие срабатывает при открытом приложении
{$IFDEF ANDROID}
  aObj := ANotification.DataObject.GetValue(GCMSignature);
  if aObj <> nil then
    aText := aObj.Value
  else
    aText := ANotification.DataObject.GetValue(FCMSignature).Value;
{$ELSE}
  aObj := ANotification.DataObject.GetValue(APNsSignature);
  if aObj <> nil then
    aText := aObj.Value;
{$ENDIF}
  ShowMessage(aText);
end;

procedure TFormMain.OnServiceConnectionChange(Sender: TObject; 
  AChange: TPushService.TChanges);
begin
  if (TPushService.TChange.DeviceToken in AChange) and 
    Assigned(FPushServiceConnection) then
  begin
    FDeviceID := FPushService.DeviceIDValue[TPushService.TDeviceIDNames.DeviceID];
    FDeviceToken := FPushService.DeviceTokenValue
      [TPushService.TDeviceTokenNames.DeviceToken];
    // тут отправляем в хранилище токенов (на сервер с БД например)
  end;
end;

procedure TFormMain.PushServiceRegister;
begin
  FPushService := nil;
  FPushServiceConnection := nil;

{$IF defined(ANDROID)}
  FPushService := TPushServiceManager.Instance.GetServiceByName<
    (TPushService.TServiceNames.GCM);
  FPushService.AppProps[TPushService.TAppPropNames.GCMAppID] := FAndroidServerKey;
{$ENDIF}
{$IF defined(IOS) AND defined(CPUARM)}
  FPushService := TPushServiceManager.Instance.GetServiceByName
   (TPushService.TServiceNames.APS);
{$ENDIF}
  if Assigned(FPushService) then
  begin
    FPushServiceConnection := TPushServiceConnection.Create(FPushService);
    FPushServiceConnection.OnChange := OnServiceConnectionChange;
    FPushServiceConnection.OnReceiveNotification := OnReceiveNotificationEvent;
    FPushServiceConnection.Active := true;

    FDeviceID := FPushService.DeviceIDValue[TPushService.TDeviceIDNames.DeviceID];
    FDeviceToken := FPushService.DeviceTokenValue
      [TPushService.TDeviceTokenNames.DeviceToken];
    // тут отправляем в хранилище токенов (на сервер с БД например)
  end;
end;

4) Не забудьте вызвать метод PushServiceRegister в одном из событий формы (OnCreate/OnShow)

5) Готово!

C++ Builder код

update 11.04.2017
Не силён в билдере, мне перевели код. Спасибо Kitty
// H файл
#include 
#include 
#include 
//***
private: // User declarations
 String FDeviceID;
 String FDeviceToken;
 TPushService * FPushService;
 TPushServiceConnection * FPushServiceConnection;
 void __fastcall OnReceiveNotificationEvent(TObject *Sender, 
   TPushServiceNotification* const ANotification);
 void __fastcall OnServiceConnectionChange(TObject *Sender, 
   TPushService::TChanges AChange);
 void __fastcall PushServiceRegister();
    void __fastcall RegisterDevice();
 
//CPP файл
#include 
#include 

#if defined(__ANDROID__)
#include 

// Workaround for RSP-17714
namespace Fmx {
 namespace Pushnotification {
   namespace Android {
 _INIT_UNIT(Fmx_Pushnotification_Android);
   }
 }
} // End-of-Workaround

#endif

#if defined(__APPLE__) && (defined(__arm__) || defined(__arm64__))
#include 

namespace Fmx {
 namespace Pushnotification {
  namespace Ios {
 _INIT_UNIT(Fmx_Pushnotification_Ios);
   }
  }
}

#endif

TForm1 *Form1;

const String FAndroidServerKey = L"63538920422";

// ---------------------------------------------------------------------------
void ClearAllNotification()
{
  std::unique_ptr aNotificationCenter(new TNotificationCenter(NULL));
  if(aNotificationCenter->Supported())
   {
 aNotificationCenter->ApplicationIconBadgeNumber = -1;
 aNotificationCenter->CancelAll();
   }
}

// ---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
  ClearAllNotification();
  PushServiceRegister();
}
// ---------------------------------------------------------------------------

bool __fastcall CheckInet()
{
 bool result = false;
 THTTPClient *aHTTP = THTTPClient::Create();
 try {
  try {
   _di_IHTTPResponse aResp = aHTTP->Head("http://google.com");
   result = (aResp->StatusCode < 400);
  }
  catch (const System::Sysutils::Exception &E) {
   result = false;
  }
 }
 __finally {
  delete aHTTP;
 }
 return result;
}

// ---------------------------------------------------------------------------
//*** блок для TTask::Run ***
#if defined(_PLAT_IOS)
    String sPlatform = L"IOS";
#elif defined(__ANDROID__)
    String sPlatform = L"ANDROID";
#else
    String sPlatform = L"";
#endif


class TCppTask : public TCppInterfacedObject
{
 String FDeviceID, FDeviceToken;
 public:
    TCppTask(String AnID, String AToken) : FDeviceID(AnID), FDeviceToken(AToken)
    {
    }
    void __fastcall Invoke()
    {
  String link =
  String().sprintf
    (L"http://rzaripov.kz/pushTest/api.php?method=saveToken&deviceID=%s&deviceToken=%s&platform=%s",
       UTF8String(FDeviceID).c_str(), UTF8String(FDeviceToken).c_str(), UTF8String(sPlatform).c_str());

  std::unique_ptr aHTTP(THTTPClient::Create());
  aHTTP->Get(link);
    }
};

// ---------------------------------------------------------------------------
void __fastcall  TForm1::RegisterDevice()
{
  TTask::Run(_di_TProc(new TCppTask(FDeviceID, FDeviceToken)));
}
//*** конец блока для TTask::Run ***

// ---------------------------------------------------------------------------

void __fastcall  TForm1::PushServiceRegister()
{
  bool result = CheckInet();
 if (result == true)
 {
  FPushService = nullptr;
  FPushServiceConnection = nullptr;

 #if defined(__ANDROID__)
  FPushService = TPushServiceManager::Instance->GetServiceByName
    (TPushService_TServiceNames_GCM);
  if(FPushService)
  FPushService->AppProps
    [TPushService_TAppPropNames_GCMAppID] = FAndroidServerKey;
 #endif
 #if defined(__APPLE__) && (defined(__arm__) || defined(__arm64__))
  FPushService = TPushServiceManager::Instance->GetServiceByName
    (TPushService_TServiceNames_APS);
 #endif

 if(FPushService)
   {
    FPushServiceConnection = new TPushServiceConnection(FPushService);
    FPushServiceConnection->OnChange = &OnServiceConnectionChange;
    FPushServiceConnection->OnReceiveNotification = &OnReceiveNotificationEvent;
    FPushServiceConnection->Active = true;

    FDeviceID = FPushService->DeviceIDValue[TPushService_TDeviceIDNames_DeviceID];
    FDeviceToken = FPushService->DeviceTokenValue
    [TPushService_TDeviceTokenNames_DeviceToken];

    if(FDeviceID != "" && FDeviceToken != "")
   {
    RegisterDevice();
   }

   }

 }//if (result == true)
}

// ---------------------------------------------------------------------------
void __fastcall TForm1::OnServiceConnectionChange(TObject *Sender, 
  TPushService::TChanges AChange)
{
 if (AChange.Contains(TPushService::TChange::DeviceToken) && (FPushServiceConnection))
 {
  FDeviceID = FPushService->DeviceIDValue[TPushService_TDeviceIDNames_DeviceID];
  FDeviceToken = FPushService->DeviceTokenValue
    [TPushService_TDeviceTokenNames_DeviceToken];

  if(FDeviceID != "" && FDeviceToken != "")
   {
    RegisterDevice();
   }

 }
}
// ---------------------------------------------------------------------------

void __fastcall TForm1::OnReceiveNotificationEvent(TObject *Sender,
 TPushServiceNotification* const ANotification)
{

 const String FCMSignature = L"gcm.notification.body";
 const String GCMSignature = L"message";
 const String APNsSignature = L"alert";
 String aText = "";
 TJSONValue * aObj;

 #if defined(__ANDROID__)
  aObj = ANotification->DataObject->GetValue(GCMSignature);
  if(aObj != NULL)
     {
   aText = aObj->Value();
     }
     else
     {
      aText = ANotification->DataObject->GetValue(FCMSignature)->Value();
              }
 #endif

 #if defined(__APPLE__) && (defined(__arm__) || defined(__arm64__))
   aObj = ANotification->DataObject->GetValue(APNsSignature);
   if (aObj != NULL)
    {
  aText = aObj->Value();
    }
     #endif

 ShowMessage(aText);

}
// ---------------------------------------------------------------------------
ВАЖНО для тех кто пишет на C++ Builder

PHP код

Тут сначала нужно описание сделать: 
Firebase объединил APNs + GCM, поэтому токены которые выдаются из FPushService для iOS не валидны для FCM, но у Google есть специальный инструмент для этого, у Android все отлично, токен валиден

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

Эта функция как раз и делает преобразование токена APNs в FCM
$appName - этот параметр должен содержать название пакета iOS приложения
$tokens - токены APNs
$server_key - ключ сервера (настройка Android 2 пункт)

function apns2fcmToken($appName, $tokens, $server_key) {
    $url = 'https://iid.googleapis.com/iid/v1:batchImport';
    $headers = array('Authorization: key=' . $server_key,
      'Content-Type: application/json');

    if (is_array($tokens))
      $token_arr = $tokens;
    else
      $token_arr = array($tokens);    
 
    $fields = array('application' => $appName, 'sandbox' => false, 
      'apns_tokens' => $token_arr);
 
    $ch = curl_init();
    curl_setopt_array($ch, array(
            CURLOPT_URL => $url,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYHOST => 0,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_POSTFIELDS => json_encode($fields)
    ));
    $result = curl_exec($ch);
    curl_close($ch);
    if ($result === false) return false;
    $json = json_decode($result, true);
    $rows = [];
    for ($n = 0; $n < count($json['results']); $n++){
      if ($json['results'][$n]['status'] == "OK")
        $rows[$n] = $json['results'][$n]['registration_token'];
    }
    return $rows;
}
Эта функция делает отправку пушей на выбранные токены
$title - заголовок уведомления
$text - текст уведомления
$tokens - массив токенов FCM
$server_key - ключ сервера (настройка Android 2 пункт)

function pushSend($title, $text, $tokens, $server_key) {
    $url = 'https://fcm.googleapis.com/fcm/send';
    $headers = array('Authorization: key=' . $server_key, 
     'Content-Type: application/json');
 
    if (is_array($tokens))
      $fields['registration_ids'] = $tokens;
    else
      $fields['registration_ids'] = array($tokens);
 
    $fields['priority'] = 'high';
    $fields['notification'] = array('body' => $text, 'title' => $title);
    $fields['data'] = array('message' => $text, 'title' => $title);
 
    $ch = curl_init();
    curl_setopt_array($ch, array(
            CURLOPT_URL => $url,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYHOST => 0,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_POSTFIELDS => json_encode($fields)
    ));
    $result = curl_exec($ch);
    curl_close($ch);
    return $result; //# проверять === false
}
Вот такой очень простой код у нас получился, стоит отметить что можно добавлять полезную нагрузку в виде полей, чтобы в приложении обрабатывать и выполнять дальнейшие команды. 

update 11.04.2017
Как правильно сохранять токен в БД?

Создаём нашу табличку чтобы хранить токены
CREATE TABLE PushTokens (
  id int (11) AUTO_INCREMENT,
  deviceID varchar(255) NOT NULL,
  deviceToken varchar(511) NOT NULL,
  platform varchar(11) NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT unq_deviceID UNIQUE (`deviceID`(100))
);
В таблице поле deviceID сделано уникальным, чтобы не допускать дублирование токенов

1) Когда мы с приложения отправляем на сервер токен устройства, мы должны еще отправить платформу на которой запущено приложение

2) Сохраняем полученный deviceToken, deviceID, platform в нашу таблицу PushTokens

update 11.04.2017
SQL запрос будет следующим
$sql =  "INSERT INTO PushTokens (`deviceToken`, `deviceID`, `platform`) VALUE ('$deviceToken', '$deviceID', '$platform') ON DUPLICATE KEY UPDATE `deviceToken` = '$deviceToken'";

3) При отправке пуш уведомления на девайсы, нужно сделать следующее:
  • Выборка токенов из БД платформы Android в массив
  • Выборка токенов из БД платформы iOS в массив, НО! токены APNs не валидны для FCM их нужно преобразовать через функцию apns2fcmToken
  • Сделать слияние двух массивов через array_merge($tokens_android, $tokens_ios_fcm)
  • И полученный массив отправить в функцию pushSend одним из параметров
update 11.04.2017
ВАЖНО! За один раз можно отправить только на 1000 токенов, поэтому если зарегистрированных девайсов у вас много, нужно будет разделить на несколько этапов

Почему при отправке уведомления из консоли Firebase мы не видим текст и заголовок в центре уведомлении на Android?
Это особенность FMX, в нативной реализации проверяется наличие полей message и title, а с консоли они не приходят (но можно заполнить расширенные настройки и указать эти поля)

update 11.04.2017


Как сделать многострочный текст в уведомлениях на Android?
Решение для Seattle/Berlin

update 11.04.2017
Выложил проект на GitHub

UPDATE: для iOS 10+ рабочая связка Delphi Berlin Up2+xCode8.1
На этом всё! Удачи Вам!

Google Play

23 комментария :

  1. Вроде статья не старая, но делали для iPhone уведомления, токены с iOs работают без apns2fcmToken и уведомления приходят.
    Правда есть странность в гугле, что какой токен ему не подсунь в pushSend возвращается в success: количество , даже если заведомо несуществующий токен написать.

    ОтветитьУдалить
    Ответы
    1. не подтвержаю, приходит failure с количеством недействительных токенов. даже если токен был валиден и срок его годности прошел, все равно придёт ошибка

      Удалить
    2. Да точно, методом тыка проверил, но если а начале токена, до двоеточий, что-то написать, то токен все равно проходит. например токен "dlMLBXkg6lg:APA91bGPF3", и если его изменить на "111dlMLBXkg6lg:APA91bGPF3" то он также сработает, видимо эта часть до двоеточий для чего-то другого нужна ))

      Удалить
  2. В своем проекте столкнулся со сложностями.
    Исторически сложилось, что мое приложение для iOS и Android-платформ имеет разные идентификаторы пакетов (com.yourapp.PLATFORM).
    В моем случае не получилось на FireBase собрать их в один пакет: сообщения до устройств Android доходили без проблем; iOS ни в какую не отрабатывал (несмотря на успешные сообщения об отправке со стороны Firebase {"multicast_id":7125252680403741812,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1490294673113408%91729e8c91729e8c"}]}
    ). Проблема с отправкой была не только при использовании php-скрипта, но и при попытках отправки через консоль Firebase. Даже при посыле на все iOS-устройства (без указания каких-либо конкретных токенов).
    Проблема в конце-концов решилась заведением под iOS отдельного проекта в Firebase, получении своего Server key.

    ОтветитьУдалить
  3. Sorry, not working. ADeviceID comes up correctly, but AdeviceToken comes up blank. I use Delphi 10.2 Tokyo. I have this message to try get the Token: raised exception class EJNIException with message 'java.io.IOException: MAIN_THREAD'.

    ОтветитьУдалить
  4. Equal, in Delphi 10.1 Berlin not working. The Firebase's APIs has been changed?

    ОтветитьУдалить
    Ответы
    1. Android platform?
      To get a token, you need:
      internet
      sender ID from firebase console
      correctly configured project

      Удалить
  5. Этот комментарий был удален автором.

    ОтветитьУдалить
  6. The DeviceToken variable is empty, when the operating system is IOS.

    Android works perfectly.

    Would you help me?

    ОтветитьУдалить
  7. Большое спасибо за столь обстоятельный труд! Первый раз вижу, что решение представлено не только на паскале, но и С++, что делает картину полностью завершенной и полезной для полной RAD. У меня в планах разработка для туристической фирмы, там пользователей больше чем 1000. Не могли бы Вы модернизировать Ваш сервер, чтобы обойти ограничение на 1000 устройств? Это бы окончательно дополнило картину статьи. Спасибо.

    ОтветитьУдалить
    Ответы
    1. Спасибо!
      Решение на билдере мне перевели)
      Пока нету времени модернизировать пуш-сервер, но решение простое:
      1) узнать кол-во зарегистрированных устройств, поделить на 1000, чтобы узнать кол-во повторов для цикла
      2) используя limit и offset в mysql запросе получаем "пачки" токенов
      3) выполняем преобразования (для iOS) и отправляем пользователям

      Удалить
  8. Здравствуйте.
    Вы усовершенствовали свой php сервер методом pushSendOver1000.
    Почему-то если заменить вызов sendPush методом pushSendOver1000 то получаю сообщение:
    status "ERROR"
    text "Такой метод не найден"
    С чем это может быть связано?
    push.php заменил новым. Спасибо.

    ОтветитьУдалить
    Ответы
    1. Доброго времени суток!
      Был Pull Request с изменениями от другого разработчика, я его просто одобрил, но не тестил.
      Только что обновился еще раз, проверьте

      Удалить
  9. Здравствуйте.
    Почему то в новой версии этот вызов показывает, что метод не не найден:
    http://мой сайт/pushTest/api.php?method=pushSendOver1000&title=Заголовок&text=Текст

    ОтветитьУдалить
  10. Вроде разобрался... Надо делать по прежнему вызов sendPush, а не pushSendOver1000...

    ОтветитьУдалить
    Ответы
    1. да все верно, обработка запроса не поменялось.
      method - это просто название переменой, по которой скрипт понимает что нужно сделать и какой функционал использовать

      Удалить
  11. Спасибо за отличную статьи и полнофункциональный php сервер!!!

    ОтветитьУдалить
    Ответы
    1. Пользуйтесь на здоровье, но хорошо бы подтянуть знания по php, чтобы в будущем его модернизировать и улучшить

      Удалить
    2. как принять пуши с сервера на delphi fmx 10.1 отправку токена сделал на сервер,сервер шлет мне пуши

      Удалить
  12. Tenho problemas para receber o Token no Dispositivo iOS.
    Fiz o certeificado .p12 , logo depois colocar o IdentifyBundler como na explicação mas não consigo o retorno.Alguma forma de auxilio.
    Uso Tokio 10.2.3

    ОтветитьУдалить
  13. как принять пуши с сервера на delphi fmx 10.1 отправку токена сделал на сервер,сервер шлет мне пуши

    ОтветитьУдалить