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