Envoyez des notifications push à vos applications Android avec C2DM

le 08/03/2011 par Maxence Walbrou
Tags: Software Engineering

Gardez le contact avec vos utilisateurs, apportez leur de l'information ponctuelle et dynamisez l'utilisation de votre application... sans que celle-ci n'ait besoin d'être en cours d'exécution ! C'est ce que permettent les notifications push sur nos smartphones. Android propose depuis la version 2.2 de l'OS la gestion de ces messages légers par l'utilisation des services C2DM (Cloud To Device Messaging) de Google. L'utilisation de ces services s'avère moins aisée que l'implémentation des notifications proposée par Apple pour l'iPhone. L'objet de cet article est donc de présenter cette mise en place côté téléphone pour arriver à afficher nos messages de manière simple et élégante...

C2DM : présentation et prérequis

Les services C2DM sont proposés par Google en tant que Labs auxquels il faut s'inscrire pour en bénéficier. Ils seront néanmoins disponibles à l'avenir pour tous les développeurs et permettent dès aujourd'hui d'abonner des téléphones aux notifications de votre application.

Fonctionnement tripartite de la solution

Les messages sont transmis et gérés par trois acteurs :

Un s****erveur d'application tiers

C'est un serveur à la charge du développeur qui contient la liste de tous les téléphones mobiles notifiables et leur envoie les messages push via les serveurs C2DM.

Afin d'identifier le serveur d'application tiers qui envoie les messages à notre application, un compte Gmail est utilisé. Ce compte permet au serveur d'application tiers de s'identifier auprès des serveurs C2DM pour envoyer les notifications. Il est également utilisé à l'inscription aux notifications sur l'application. Google préconise de créer un compte gmail dédié à votre application plutôt que d'utiliser un compte personnel.

Les serveurs C2DM

Ces  serveurs sont gérés par Google et transmettent les notifications reçues du serveur d'application tiers aux téléphones. La gestion des conditions d'envoi (le téléphone est bien allumé et capable de recevoir la notification) est effectuée par ces serveurs.

Notre téléphone Android

Ce téléphone héberge notre application qui gère l'abonnement aux notifications ainsi que l'action à effectuer à la réception des messages.

Les services C2DM permettent à tout téléphone en version 2.2 ou plus de s'inscrire et recevoir des notifications push de la part des serveurs Google. Les téléphones en version inférieure (plus de 40% du parc en février 2011) ne pourront donc pas recevoir de notification. Si cette limitation de version est trop impactante pour votre produit, vous devrez trouver une solution spécifique pour les versions inférieures ou considérer des alternatives commerciales à C2DM comme Urban Airship.

Pour pouvoir s'enregistrer aux flux de notifications, les téléphones devront également être identifiés par un compte Google et avoir l'application Android Market d'installée. Si ces conditions sont immédiatement vérifiées sur un téléphone, il s'avère plus délicat de les réunir sur l'émulateur Android fourni avec le SDK qui ne prévoit pas d'utiliser de compte Gmail. Le tutoriel suivant vous permettra d'effectuer les modifications et ajouts d'apk nécessaires (disponibles ici) pour pouvoir tester les notifications sur votre émulateur. Cependant, l'utilisation d'un téléphone à jour reste le moyen le plus simple et rapide de tester l'application !

Abonner son téléphone aux notifications

En théorie...

Notre application doit s'enregistrer auprès des serveurs C2DM pour que ceux-ci puissent lui envoyer les messages reçus de votre serveur d'application tiers. Pour cela, deux informations doivent être envoyées aux serveurs C2DM :

- le sender id : c'est l'adresse gmail dédiée à notre application qui va être utilisée par votre serveur d'application pour l'authentifier aux services google. Dans notre exemple, ça sera "notif.application@gmail.com".

- l'application id : c'est l'identifiant qui permettra aux serveurs C2DM de n'envoyer les messages qu'à notre application Android. Cet application id doit contenir le nom de package (pour notre exemple : "com.octo.notif") qui est l'identifiant unique de notre application sur Android.

A partir de ces informations, les serveurs Google vont générer un jeton, le registration id et le renvoyer à l'application.

Ce jeton nous permettra d'envoyer un message à partir du serveur tiers. Une fois reçu, il doit donc être transmis à notre serveur d'application pour être stocké.

Il faut savoir que les serveurs de Google regénèrent régulièrement les registration id. Notre application devra donc être capable de reconnaitre ultérieurement la réception d'un nouveau jeton et l'envoyer alors au serveur d'application pour remplacer l'ancien.

Une fois cette étape d'abonnement terminée, l'application Android est prête à recevoir des message en push. Voyons comment implémenter ces envois et réceptions d'informations.

Des permissions particulières

Techniquement, votre application va avoir besoin de déclarer certaines permissions dans son fichier manifest pour envoyer des informations aux serveurs C2DM et recevoir le résultat de l'abonnement ainsi que des notifications :

- l'accès à internet :

<uses-permission android:name="android.permission.INTERNET"/>

- le droit de recevoir des messages (abonnement ou notifications) des serveurs C2DM :

<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

Nous allons sécuriser cette réception de messages C2DM pour qu'ils ne soient reçus que par notre seule application "com.octo.notif" :

<permission android:name="com.octo.notif.permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name="com.octo.notif.permission.C2D_MESSAGE" />

S'abonner et se désabonner

Ensuite, pour nous abonner, il suffit de lancer un service d'abonnement identifié par l'intent com.google.android.c2dm.intent.REGISTER en lui spécifiant le sender id et l'application id. Cette dernière information sera retrouvée par un PendingIntent :

Intent registrationIntent = new Intent("com.google.android.c2dm.intent.REGISTER");
registrationIntent.putExtra("app", PendingIntent.getBroadcast(context, 0, new Intent(), 0));
registrationIntent.putExtra("sender", "notif.application@gmail.com");
context.startService(registrationIntent);

De la même manière, la procédure de désabonnement consiste à démarrer le service com.google.android.c2dm.intent.UNREGISTER :

Intent unregistrationIntent = new Intent("com.google.android.c2dm.intent.UNREGISTER");
unregistrationIntent.putExtra("app", PendingIntent.getBroadcast(context, 0, new Intent(), 0));
context.startService(unregistrationIntent);

Gérer la réponse du serveur C2DM

La réponse à l'abonnement ou au désabonnement ainsi que les messages sont envoyés à l'application en tant qu'événements broadcast. Nous allons donc créer la classe C2DMBroadcastReceiver.java implémentant BroadcastReceiver, qui vérifiera quel type de message a été reçu et appellera selon le cas une des méthodes onError(), onRegistration(), onUnregistration() ou onMessageReceived(). Nous pourrons alors créer une classe héritant de C2DMBroadcastReceiver.java qui externalisera ces différents traitements.

public abstract class C2DMBroadcastReceiver extends BroadcastReceiver {

	protected abstract void onError(Context context, String error);
	protected abstract void onRegistration(Context context, String registrationId);
	protected abstract void onUnregistration(Context context);
	protected abstract void onMessageReceived(Context context, Intent intent);

	@Override
	// méthode appelée lors de l'événement broadcast (les infos sont contenues dans l'intent)
	public void onReceive(Context context, Intent intent) {
		if (intent.getAction().equals("com.google.android.c2dm.intent.REGISTRATION")) {
			handleRegistration(context, intent);
		} else if (intent.getAction().equals("com.google.android.c2dm.intent.RECEIVE")) {
			onMessageReceived(context, intent);
		}
	}

	private void handleRegistration(Context context, Intent intent) {
		String error = intent.getStringExtra("error");
		String unregistration = intent.getStringExtra("unregistered");
		String registration = intent.getStringExtra("registration_id");

		if (error != null) {
			onError(context, error);
		} else if (unregistration != null) {
			onUnregistration(context);
		} else if (registration != null) {
			onRegistration(context, registration);
		}
	}
}

Créons ensuite la classe de traitement NotificationsReceiver.java où nous pourrons gérer les résultats comme nous le souhaitons (affichage d'un message d'erreur, nouvel essai d'abonnement, envoi par webservice du registration id, sauvegarde de celui-ci...)  :

public class NotificationsReceiver extends C2DMBroadcastReceiver {

	@Override
	protected void onError(Context context, String error) {
		// traitement de l'erreur
	}

	@Override
	protected void onRegistration(Context context, String registrationId) {
		// envoi du registrationId au serveur d'application
	}

	@Override
	protected void onUnregistration(Context context) {
		// traitement du désabonnement
	}

	@Override
	protected void onMessageReceived(Context context, Intent intent) {
		// traitement du message reçu
	}
}

Pour que cette classe soit utilisée lorsqu'un événement broadcast est détecté, il ne reste qu'à mettre à jour le fichier manifest. Nous rajoutons au même niveau que les activités un receiver capable de traiter les messages C2DM uniquement :


<receiver android:name=".NotificationsReceiver" android:permission="com.google.android.c2dm.permission.SEND">
     <!-- Recevoir le registration id -->
     <intent-filter>
          <action android:name="com.google.android.c2dm.intent.REGISTRATION"></action>
          <category android:name="com.octo.notif"></category>
     </intent-filter>
     <!-- Recevoir un message -->
     <intent-filter>
          <action android:name="com.google.android.c2dm.intent.RECEIVE"></action>
          <category android:name="com.octo.notif"></category>
     </intent-filter>
 </receiver>

Voilà ! Notre application est prête à s'abonner et recevoir des notifications ! Google propose une implémentation plus complète de ce système dans l'exemple ChromeToPhone qui vous permettra de voir comment retenter un abonnement après une erreur, comment garder le registration id en mémoire, etc.

Afficher les notifications reçues

Facebook utilise le push par exemple pour nous signaler qu'un nouveau message est disponible ou qu'une personne souhaite devenir notre ami... L'application redirige alors l'utilisateur vers la page permettant d'afficher l'information dans son intégralité. Implémentons un comportement similaire : lorsque notre application reçoit une notification push, nous souhaitons que celle-ci apparaisse dans la barre de notification du téléphone et que lorsque nous cliquons sur son résumé,  nous arrivions par exemple sur la première page de notre application.

Cette action se définit dans notre méthode onMessageReceived() :

@Override
	protected void onMessageReceived(Context context, Intent intent) {

		String message = intent.getStringExtra("message"); // data.message contient le texte de la notification
		String title = "OctoWorldCup notif";
		int iconId = R.drawable.notif_image;

		// création de la notification :
		NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
		Notification notification = new Notification(iconId, message, System.currentTimeMillis()); 

		// création de l'activité à démarrer lors du clic :
		Intent notifIntent = new Intent(context.getApplicationContext(), MainActivity.class);
		PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notifIntent, 0);

		// affichage de la notification dans le menu déroulant :
		notification.setLatestEventInfo(context, title, message, contentIntent);
		notification.flags |= Notification.FLAG_AUTO_CANCEL; // la notification disparaitra une fois cliquée

		// lancement de la notification :
		notificationManager.notify(1, notification);
	}

Pour aller plus loin dans la personnalisation de la barre de notification, vous pouvez vous baser sur la documentation ici.

Envoyer les notifications push

Il ne reste plus qu'à tester l'envoi d'une notification. Cela consiste côté serveur d'application à s'authentifier auprès des services Google https://www.google.com/accounts/ClientLogin avec le compte dédié ("notif.application@gmail.com"). Une fois identifié, pour chaque registration id, nous pouvons envoyer notre message par une requête POST à l'url https://android.apis.google.com/c2dm/send :

La façon d'implémenter l'envoi de message dépend de votre serveur et des technologies que vous souhaitez utiliser. Ce tutoriel vous propose une implémentation en Python.

Afin de tester rapidement notre fonctionnement, voici un script shell qui vous permettra d'envoyer simplement un message vers un mobile identifié par son registration id :

#!/bin/sh

# to use : ./authentAndSendMessage google_account_mail google_account_password registration_id "message"

# parameters
email=$1
password=$2
registration_id=$3
message=$4

# authentication
echo '--> Authenticate to google services'
authentication_result=`curl -s https://www.google.com/accounts/ClientLogin \
	-d Email=$email \
	-d "Passwd=$password" \
	-d accountType=GOOGLE \
 	-d service=ac2dm`

# if authentication ok
if [[ $authentication_result == *Auth=* ]]
then
  	echo 'Authentication successful'
	authentication=`echo $authentication_result | awk '{split($0,array,"Auth="); print array[2]}'`
	echo 'Authentication token extracted'

	echo '--> Sending message "'$message'"'
	# send message
	curl --header "Authorization: GoogleLogin auth=$authentication" "https://android.apis.google.com/c2dm/send" \
	 	-d registration_id=$registration_id \
		-d "data.message=$message" \
		-d collapse_key=1

	echo 'finished'
fi

Dans ce script, on peut voir dans la requête d'envoi POST que le texte de la notification est passé en paramètre dans la variable data.message. Nous aurions pu ajouter d'autres informations avec d'autres clés data.variable pour transmettre plus d'informations.

Dans cette même requête, une autre propriété est intéressante : collapse_key. Cette clé est un identifiant que l'on donne aux messages de nature similaire. Ainsi, si le téléphone est éteint et que plusieurs messages avec le même identifiant s'accumulent sur le serveur C2DM, seul le dernier sera envoyé lorsque l'appareil aura été rallumé. Il convient donc de choisir avec soin ces identifiants.

Conclusion : C2DM pour nos applications de demain

Si les services C2DM ne permettent aujourd'hui que de toucher 60 % des smartphones du parc mondial Android, l'arrivée massive de tablettes en version 3, ainsi que la possibilité de mettre à jour son téléphone en 2.2 offerte par la quasi totalité des constructeurs/opérateurs laisse présager que plus de 80 % des appareils seront compatibles avant la fin de l'année 2011. Soyez donc prêts !