Les notifications sur Mobile

Avec Delphi pour mobile, il est possible d'exploiter Google Cloud Messaging (GCM) sous Android et Apple Push Notification service (APNS) sous iOS pour recevoir des notifications.

Pour cela, il suffit de placer sur la fiche un composant TPushEvents et un provider. Delphi propose en standard TParseProvider et TKinveyProvider mais nous allons voir dans ce billet comment créer votre propre provider afin de gérer vous même les notifications.

Mais d'abord, comment ça fonctionne ?

En premier lieu, laissez moi vous rappeler comment fonctionnent les systèmes de push. Ils sont similaires sur les deux plateformes, à savoir, le smartphone s'enregistre auprès de Google ou Apple pour obtenir un token unique qui identifie votre application sur le téléphone. Votre mobile communique alors ce token à un provider qui le conserve soigneusement pour pouvoir ensuite vous envoyer une notification. La notification se fait donc depuis ce provider qui adresse un requête à Google ou Apple en indiquant le destinataire du message grâce à son token. Ensuite c'est Google/Apple qui se charge de faire remonter la notification sur le mobile.

Tout cela semble assez, simple, alors pourquoi passer par Parse ou Kinvey ? si vous ne voulez pas exploiter les autres services de ces prestataires et que vous avez un site web à votre disposition, je ne vois aucune raison valable de ne pas gérer cela vous même :)

Première étape, l'obtention du Token

La premier point va consister à créer un nouveau composant provider pour notre application Delphi sur mobile.

Comme point de départ, nous avons la propriété TPushEvent.Provider...mais contre toute attente, cette propriété est un simple TComponent ! Depuis Delphi 1 nous avons l'habitude de lier des composants comme un TDataSet et un TDataSource, une TDBGrid et une TDataSource, ... et pour ceux qui se sont déjà essayé à la création de composant, vous savez que c'est très simple à mettre en oeuvre puisque l'inspecteur d'objets retrouve tout seul les composants adpatés à la propriété. On peut en déduire qu'il existe un éditeur de propriété spécifique à TPushEvent.Provider afin qu'il ne propose que certains composants et non tous les TComponent.

Bref, cela ne nous dit pas comment créer notre provider...j'ai donc creusé dans le code de source REST.Backend.Parse*.pas pour comprendre ce qu'il en est.

La classe TExecutePushProvider

Le composant que l'on va placer sur la fiche sera TExecutePushProvider dont voici le code

interface
type
  TExecutePushProvider = class(TComponent, IBackendProvider)
  private
    FProjectNumber: string;
  public
  // IBackendProvider
    function GetProviderID: string;
  published
    property ProjectNumber: string read FProjectNumber write FProjectNumber;
  end;

implementation

const
  PROVIDER_ID = 'EXECUTE_SARL_PUSH';

{ TExecutePushProvider }

function TExecutePushProvider.GetProviderID: string;
begin
  Result := PROVIDER_ID;
end;

Oui ! c'est tout ! L'interface IBackendProvider ne contient qu'une seule méthode qui doit retourner un ID unique.

Il faut savoir que APNs n'a besoin d'aucun paramètre, l'application iOS étant déjà signée avec un certificat propre à l'application, Apple peut directement délivrer un token spécifique à cette application. Chez Google il faudra simplement renseigner le numéro de projet, c'est donc l'unique propriété du composant...qui ne possède aucune méthode l'exploitant !

Sous la surface

Regardons maintenant ce qu'il se cache sous la surface, et notamment dans la partie initialization.

var
  Factory: TExecutePushDeviceServiceFactory;
initialization
  TBackendProviders.Instance.Register(PROVIDER_ID, 'Execute Push Provider');
  Factory := TExecutePushDeviceServiceFactory.Create;
  Factory.Register;

On retrouve ici l'ID de notre provider qui est enregistré dans TBackendProviders. Le lien entre cet ID et notre composant, se fera via une Factory.

{  TExecutePushDeviceServiceFactory }
// the Factory return a IBackendService from a IBackendProvider
type
  TExecutePushDeviceServiceFactory = class(TProviderServiceFactory)
  protected
    function CreateService(const AProvider: IBackendProvider; const IID: TGUID): IBackendService; override;
  public
    constructor Create;
  end;

constructor TExecutePushDeviceServiceFactory.Create;
begin
  inherited Create(PROVIDER_ID, UNIT_NAME);   // Do not localize
end;

function TExecutePushDeviceServiceFactory.CreateService(const AProvider: IBackendProvider;
  const IID: TGUID): IBackendService;
begin
  if not (AProvider is TExecutePushProvider) then
    raise Exception.Create('wrong Provider, expected TExecutePushProvider');
  Result := TExecutePushDeviceService.Create(AProvider as TExecutePushProvider);
end;

Là encore notre ID utilisé dans le Constructor de la Factory permet de faire le lien avec le reste. Quand Delphi lui passe en paramètre un IBackendProvider de type TExecutePushProvider, il lui retourne un IBackendProvider...et c'est parti pour une nouvelle classe.

{ TExecutePushDeviceService }
 // the BackendService return a IBackendPushDeviceApi
type
  TExecutePushDeviceService = class(TInterfacedObject, IBackendService, IBackendPushDeviceService)
  private
    FProvider     : TExecutePushProvider;
    FPushDeviceApi: TExecutePushDeviceAPI;
  public
    constructor Create(AProvider: TExecutePushProvider);
    function GetPushDeviceAPI: IBackendPushDeviceApi;
    function CreatePushDeviceApi: IBackendPushDeviceApi;
  end;

constructor TExecutePushDeviceService.Create(AProvider: TExecutePushProvider);
begin
  inherited Create;
  FProvider := AProvider;
end;

function TExecutePushDeviceService.GetPushDeviceAPI: IBackendPushDeviceApi;
begin
  if FPushDeviceApi = nil then
  begin
    FPushDeviceApi := TExecutePushDeviceApi.Create;
  {$IFDEF ANDROID}
    FPushDeviceApi.FGCMAppID := FProvider.ProjectNumber;
  {$ENDIF}
    FPushDeviceApi._AddRef;
  end;
  Result := FPushDeviceAPI;
end;

function TExecutePushDeviceService.CreatePushDeviceApi: IBackendPushDeviceApi;
var
  Api: TExecutePushDeviceApi;
begin
  Api := TExecutePushDeviceApi.Create;
{$IFDEF ANDROID}
  Api.FGCMAppID := FProvider.FProjectNumber;
{$ENDIF}
  Result := Api;
end;

Ce nouvel objet ne sert pas à grand chose en tant que tel, mais il récupère le ProjectNumber quand on est sous Android...et retourne un IBackendPushDeviceApi

{ TExecutePushDeviceAPI }
// finally, the PushDeviceAPI request the system PushService, and, on Android, set it's ProjectNumber as GCMAppID
type
  TExecutePushDeviceAPI = class(TInterfacedObject, IBackendPushDeviceApi)
  private
  {$IFDEF ANDROID}
    FGCMAppID: string;
  {$ENDIF}
  public
    function GetPushService: TPushService; // May raise exception
    function HasPushService: Boolean;
    procedure RegisterDevice(AOnRegistered: TDeviceRegisteredAtProviderEvent);
    procedure UnregisterDevice;
  end;

function TExecutePushDeviceAPI.GetPushService: TPushService; // May raise exception
begin
{$IFNDEF PUSH}
  Result := nil;
{$ENDIF}
{$IFDEF ANDROID}
  Result := TPushServiceManager.Instance.GetServiceByName(TPushService.TServiceNames.GCM);
{$ENDIF}
{$IFDEF IOS}
  Result := TPushServiceManager.Instance.GetServiceByName(TPushService.TServiceNames.APS);
{$ENDIF}
  if (Result = nil) then
    raise Exception.Create('PushService not available');
  if Result.Status <> TPushService.TStatus.Started then
  begin
  {$IFDEF ANDROID}
    Result.AppProps[TPushService.TAppPropNames.GCMAppID] := FGCMAppID;
  {$ENDIF}
  end;
end;

function TExecutePushDeviceAPI.HasPushService: Boolean;
begin
{$IFNDEF PUSH}
  Result := False;
{$ENDIF}
{$IFDEF ANDROID}
  Result := FGCMAppID <> '';
{$ENDIF}
{$IFDEF IOS}
  Result := True;
{$ENDIF}
end;

procedure TExecutePushDeviceAPI.RegisterDevice(AOnRegistered: TDeviceRegisteredAtProviderEvent);
//var
//  Service: TPushService;
begin
// --> it's possible to insert here the Token registration to a custom website
//  Service := GetPushService;
//  ADeviceToken := APushService.DeviceTokenValue[TPushService.TDeviceTokenNames.DeviceToken];
  if Assigned(AOnRegistered) then
    AOnRegistered(GetPushService);
end;

procedure TExecutePushDeviceAPI.UnregisterDevice;
begin
end;

On arrive au bout du tunnel ! TExecutePushDeviceAPI exploite tout simplement les fonctions systèmes pour obtenir une instance de GCM ou APNs, il renseigne le ProjectNumber ...et c'est tout !

On notera qu'il est possible d'intercepté le Token dans la méthode RegisterDevice, mais vous pouvez tout aussi bien le faire dans l’événement TPushEvents.OnDeviceTokenReceived !

voici par exemple comment envoyer le token et quelques informations sur le mobile à un script PHP
procedure TForm1.PushEvents1DeviceTokenReceived(Sender: TObject);
var
  SL : TStringList;
begin
  SL := TStringList.Create;
  try
    SL.Add('system=' + TOSVersion.ToString);
    SL.Add('token=' + PushEvents1.DeviceToken);
    SL.Add('device=' + PushEvents1.DeviceID);
    idHTTP1.Post('http://mywebsite/push/register.php', SL);
  finally
    SL.Free;
  end;
end;

Envoyer un message via APNs

L'émission d'une notification nécessite l'obtention d'un certificat APNs pour votre application, c'est pénible à faire mais c'est très bien expliqué ici.

Voici un code PHP pour adresser un message au Token réceptionné:

<?php 
function push_apns($token$text$debug false) {
    global 
$debug_certificat$release_certificat;
    
    
$url = ($debug 'ssl://gateway.sandbox.push.apple.com:2195' 'ssl://gateway.push.apple.com:2195');
    
    
$cert = ($debug $debug_certificat $release_certificat); // cert.pem
    
    
$streamContext stream_context_create();
    
stream_context_set_option($streamContext'ssl''local_cert'$cert);
    
$apns stream_socket_client($url$error$errorString60STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT$streamContext);
  
    if (!
$apns) {
        return 
"can't connect";
    }
  
    
$payload = array(
        
'aps' => array('alert' => $text),
    );
    
$payload json_encode($payload);
    
    
$notification chr(0).pack('n'32).pack('H*'$token).pack('n',strlen($payload)).$payload;
    
$err fwrite($apns$notificationstrlen($notification));
    if (!
$err)
      return 
"write error";
    
fclose($apns);
    
    return 
"OK";
}

Notez qu'il existe une nouvelle API plus complète (qui retourne des messages d'erreur !) mais elle s'appuie sur HTTP/2 qui n'est pas supporté par les versions PHP que j'utilise.

Envoyer un message via GCM


Pour GCM c'est beaucoup plus simple à mettre en oeuvre, il suffit de créer une clé d'API pour votre application sur la console Google.

<?php 
function push_gcm($token$text) {
    
$key 'XXX'// Server API Key in your Google console
    
$url 'https://gcm-http.googleapis.com/gcm/send';
    
$post = array(
      
'to'   => $token,
      
'data' => array('message' => $text),
    );
    
$headers = array(
      
'Authorization: key=' .$key,
      
'Content-Type: application/json',
    );
    
$ch curl_init();
    
curl_setopt($chCURLOPT_URL$url);
    
curl_setopt($chCURLOPT_POSTtrue);
    
curl_setopt($chCURLOPT_HTTPHEADER$headers);
    
curl_setopt($chCURLOPT_RETURNTRANSFERtrue);
    
curl_setopt($chCURLOPT_SSL_VERIFYPEERfalse);
    
curl_setopt($chCURLOPT_POSTFIELDSjson_encode($post));
    
$result curl_exec($ch);
    
    if (
curl_errno($ch))
        
$result .= "<br>error ".curl_errno($ch);
    
curl_close($ch);
    
    return 
$result;
}

la réponse est au format JSON et vous permet de savoir si le message a bien été délivré.

Conclusion

Il faudrait creuser d'avantage les sources de Delphi pour trouver comment sont exploités GCM et APNs dans le code, car il y a sans doute moyen de supprimer tout cet empilement de Factory et autre Interfaces pour n'avoir finalement qu'un seul composant TPushNotifications recevant directement les informations systèmes.

Notez aussi que j'ai laissé de côté la partie OSX et Windows Mobile dont je n'ai pas besoin actuellement.

Vous pouvez télécharger le code Execute.PushProvider.pas
Date de dernière modification : 10/02/2016