Home

 

 

Push-Nachrichten mit Googles C2DM


Ein Beitrag von Arno BeckerArno Becker

Gab es früher praktisch nur die Möglichkeit, SMS als in der Regel kostenträchtigen Push-Mechanismus für Mobiltelefone einzusetzen, gibt es heute interessante Alternativen. Eine davon ist Googles Cloud to Device Messaging Framework (C2DM), welches auf dem schon vorhandenen Synchronisationsmechanismus des Android-Geräts mit dem Google-Konto aufsetzt.

In den vergangenen Heften wurde schon ausführlich über jWebSocktes berichtet. C2DM ist eine interessante Alternative, da dieses Push-Verfahren auf einer schon bestehenden Verbindung des Android-Geräts mit dem Google-Account aufsetzt. Denn Voraussetzung ist ein eingerichteter Google-Account auf dem Android-Gerät, welches sich darüber fortwährend mit diesem Account synchronisiert.

Man verwendet also eine bestehende Internetverbindung und schont die Systemressourcen, da nicht jede Anwendung eine eigene Internetverbindung offen hält. Dadurch hält der Akku länger, denn die Datenübertragung erfolgt über das GSM-Modul des Android-Geräts, welches viel Strom verbraucht.

Eine Push-Nachricht wird von den Google-Servern, der sogenannten Cloud, direkt an die gewünschten Android-Geräte geschickt. Man benötigt jedoch einen eigenen Server (Anwendungsserver), der die Push-Nachricht initiiert, denn die Google-Server stellen nur die Infrastruktur des Push-Mechanismus zur Verfügung. Die Nachricht selber muss auf dem Anwendungsserver erzeugt werden und zur Verteilung an die Android-Geräte an das C2DM-Framework (die Cloud) geschickt werden.

C2DM realisiert alle Features eines hochverfügbaren Messaging-Systems, wie zum Beispiel eine Nachrichtenqueue und beliebig viele Zustellversuche, falls das Zielgerät nicht aktiv ist.

Überblick

Googles C2DM-Framework erfordert also, dass man einen eigenen Server betreibt, der für das Verschicken der Pushnachrichten über die Cloud zuständig ist. Damit eignet sich C2DM nicht so gut zur Inter-Gerätekommunikation, wie zum Beispiel jWebSockets. Hier leitet der Server die Daten direkt an die Android-Geräte weiter und dient nur als Vermittler. C2DM ist ein serverbasiertes Push-System. Auslöser der Push-Nachricht kann ein beliebiges IT-System sein, also auch ein Android-Gerät. Normalerweise übermittelt man aber keine Daten direkt, da C2DM nur jeweils 1 Kilobyte Daten übertragen kann. Man übermittelt meist eine URI (Uniform Resource Identifier), die dem Android-Gerät mitteilt, wo es welche Daten abholen kann. Dennoch wäre zum Beispiel die Realisierung eines Chat-Programms mit C2DM prinzipiell möglich. Anwendung findet ein Push-System auf Basis von C2DM beispielsweise in Unternehmen, die Updates relevanter Unternehmensdaten sofort auf den Mobilgeräten verfügbar machen wollen. Die Push-Nachricht dient nur dazu, die Anwendung zu wecken, damit sie eine Verbindung zum Unternehmensserver aufbaut und die Daten aktualisiert.

Anmeldeverfahren

Zunächst müssen sich die Android-Geräte an Googles C2DM-Servern registrieren (siehe Abbildung 1). Dies erfolgt rein über Intents. Überhaupt ist für die gesamte Nutzung von C2DM keine zusätzliche API nötig.

Nach erfolgter Registrierung erhält man in der Android-App über einen Broadcast Intent eine eindeutige Registrierungs-Id. Diese muss an den eigenen Server übertragen werden, der später die Push-Nachricht abschickt. Anhand dieser Registration-Id kann der C2DM-Server einem ganz bestimmten Gerät die Nachricht schicken, die er zuvor vom Anwendungsserver erhalten hat.

Nach der Registrierung am C2DM-Server liegen die angemeldeten Geräte auf der Lauer. Schickt man nun vom Anwendungsserver eine Push-Nachricht, die die Registration-Id enthält, an die C2DM-Server, leiten diese die Nachricht in Form eines Intents an das betreffende Android-Gerät weiter. Systemintern wird der Intent in einen Broadcast Intent umgewandelt. Folglich muss man einen Broadcast Receiver implementieren, um die Push-Nachricht auswerten zu können.

Die Nutzung von C2DM erfordert eine Registrierung über ein Formular auf den Google-Webseiten [1]. Voraussetzung für die C2DM-Registrierung ist ein Google-Account. Dort wird man als erstes nach dem Package der Anwendung gefragt. Hier sollte man den Packagenamen angeben, wie man ihn später bei der Implementierung der Anwendung im -Tag der Android-Anwendung eingeben wird. Für die Beispielanwendung auf der Heft-CD lautet der Packagename beispielsweise de.visionera.pushme.

Nach einigen weiteren Fragen zur Anwendung und deren Nutzungsverhalten von C2DM wird man nach der „Role account email“ gefragt. Diese wird auch als „Google Account ID“ bezeichnet. Hier trägt man die E-Mail-Adresse ein, mit der man sich bei seinem Google-Account anmeldet.

Geeignetes AVD erstellen

Nun benötigt man noch einen Emulator, der mit C2DM arbeiten kann. C2DM gibt es erst seit der Android-Version 2.2. Intern werden die Google-APIs verwendet. Somit muss ein AVD (Android Virtual Device) existieren, welches mindestens „Google APIs (Google Inc.) - API Level 8“ verwendet. Das normale Android-Target ohne Google APIs reicht nicht.

Google-Account eintragen

Der Emulator muss nun noch den Google-Account kennen, damit er sich fortwährend mit ihm synchronisieren kann. Erst dadurch ist er in der Lage, die Push-Nachrichten zu empfangen. Dazu startet man im Emulator das Programm „Settings“. Unter „Accounts & Sync“ kann man über die Schaltfläche „Add account“ einen neuen Google-Account (Typ „Google“) anlegen (Abbildung 2). Nötig sind dazu der Username (E-Mail-Adresse) und das Passwort des Google-Accounts. Es ist ebenfalls möglich, einen Google-Account direkt aus dem Emulator heraus zu erstellen.

Aufbau der Testumgebung

Die Implementierung eines C2DM-basierten Push-Systems besteht aus der Client-Software, also einem Android-Programm, und einer Server-Implementierung. Wir simulieren in diesem Artikel die Kommunikation des Servers mit C2DM mit Hilfe von cURL (Client for URLs). Dies ist ein Programm zum Übertragen von Dateien über das Internet, welches es kostenfrei sowohl für Linux als auch für Windows gibt. Es unterstützt alle gängigen Internetprotokolle (HTTP, HTTPS, FTP etc.) und wird hier genutzt, um per HTTP mit dem C2DM-Server zu kommunizieren.

Die Beispielanwendung „PushMe“, die sich auf der CD zum Heft befindet, simuliert hingegen gleichzeitig den Server, wodurch man zum Testen auf die langwierige Eingabe der cURL-Befehle verzichten kann. Die Anwendung enthält zu Demonstrationszwecken eine einfache Serverimplementierung, die die Anmeldung am C2DM-Server und das Verschicken von Testnachrichten erlaubt. Die Nachrichten werden dann in der gleichen Anwendung empfangen, nehmen aber den Umweg über C2DM. Der betreffende Code kann als Muster für die eigene Serverimplementierung verwendet werden.

Google-Anmeldung

Der in Abbildung 1 gezeigte Schritt 1 für die Anmeldung des Anwendungsservers beim Google-Account lässt sich mit cURL folgendermaßen simulieren:

 curl -k https://www.google.com/accounts/ClientLogin -d Email=Diese E-Mail-Adresse ist vor Spambots geschützt! Zur Anzeige muss JavaScript eingeschaltet sein! -d Passwd=meinPwd -d accountType=HOSTED_OR_GOOGLE -d source=firma-PushMe-1.0 -d service=ac2dm 

Der cURL-Aufruf lässt sich einfach in Java implementieren, indem man einen HTTPS-Post an die URL schickt und als Post-Parameter die mit „-d“ gekennzeichneten Parameter mitschickt. Wir kommen später darauf zurück.

Das Ergebnis sind drei Zeilen mit Tokens, von denen das letzte mit „Auth=“ markiert ist. Es folgt eine lange Kette von Zahlen und Buchstaben. Das Auth-Token („Sender Auth Token“) muss auf dem Server extrahiert werden und wird später mit jeder Push-Nachricht mitgeschickt, damit C2DM weiß, wer die Nachricht schickt.

Was die Parameter des cURL-Aufrufs angeht, so trägt man bei „Email“ und „Passwd“ die Login-Daten des Google-Accounts ein. Der Parameter „source“ dient zum Debuggen und sollte am besten aus dem

Firmennamen, dem Namen der Anwendung und der Version der Anwendung zusammengesetzt werden. Statt „HOSTED_OR_GOOGLE“ bei „accountType“ kann auch „HOSTED“ oder „GOOGLE“ angegeben werden, je nachdem, welchen Typ von Google-Account man besitzt. Bei „GOOGLE“ handelt es sich um den klassischen Google-Account, über den man z.B. auch ein Gmail-Konto einrichtet. Bei „HOSTED“ handelt es sich um Google Apps. Die Google Apps sind auch ein Bestandteil der Google Cloud, aber die Accouts sind voneinander getrennt. Ist man sich nicht sicher, wo man seinen Account angelegt hat, wählt man „ HOSTED_OR_GOOGLE“ und die Cloud versucht die Anmeldung bei beiden Accounttypen.

Der letzte Parameter „service“ gibt den Google-Dienst an, der verwendet werden soll. Bei diesem Pflichtparameter ist „ac2dm“ anzugeben.

App-Registrierung

Schritt 3 in Abbildung 1 führt uns zur Implementierung der Android-App. Das dazu nötige Projekt sollte als „Project Build Target“ mindestens die Google APIs Level 8 erhalten (Android 2.2 + Google APIs). Listing 1 zeigt die C2DM-spezifischen Teile des Android Manifests. Zum einen sind dies besondere Permissions und zum anderen die Deklarationen eines statischen Broadcast Receivers.

Listing 1: Android Manifest für die Nutzung von C2DM.

 <?xml version="1.0" encoding="utf-8"?>
<manifestxmlns:android="http://schemas.android.com/apk/res/android"
      package="de.visionera.lib.c2dm"
      android:versionCode="1"
      android:versionName="1.0">
      
    <uses-sdkandroid:minSdkVersion="8" />
            
    <!-- Permissions für Cloud to Device-Messaging -->
    <permission
        android:name="de.visionera.lib.c2dm.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />
    <uses-permissionandroid:name="de.visionera.lib.c2dm.permission.C2D_MESSAGE" />
    
    <uses-permissionandroid:name="com.google.android.c2dm.permission.RECEIVE" />
        <uses-permissionandroid:name="android.permission.INTERNET" />
    
    <applicationandroid:icon="@drawable/icon"android:label="@string/app_name">

    <activity...>
    …
    </activity>
  
    <receiverandroid:name="de.visionera.lib.c2dm.C2DMReceiver"
            android:permission="com.google.android.c2dm.permission.SEND">
            <!-- Empfange neue Nachrichten -->
            <intent-filter>
                <actionandroid:name="com.google.android.c2dm.intent.RECEIVE" />
                <categoryandroid:name="de.visionera.lib.c2dm" />
            </intent-filter>
            <!-- Empfange Registration-Ids nach erfolgter Registrierung -->
            <intent-filter>
                <actionandroid:name="com.google.android.c2dm.intent.REGISTRATION" />
                <categoryandroid:name="de.visionera.lib.c2dm" />
            </intent-filter>
        </receiver>        

    </application>    

</manifest>

Die erste Permission ist eine selbst deklarierte Permission. Sie ist notwendig um zu verhindern, dass andere Anwendungen die Nachricht abfangen können. Man deklariert die Permission nach dem Muster

packagename.der.anwendung + .permission.C2D_MESSAGE

Anschliessend verwendet man diese Permission. Die INTERNET-Permission ist nötig, um die Registration-Id zum Server zu senden und gehört somit nicht zu den C2DM-spezifischen Permissions. Die RECEIVE-Permission befähigt die Anwendung zum Empfang der Push-Nachrichten vom C2DM-Sever.

Nun benötigt man noch einen statischen Broadcast Receiver [4]. Ihm sollte man die Permission com.google.android.c2dm.permission.SEND mitgeben. Durch sie stellt man sicher, dass nur Nachrichten von den C2DM-Servern empfangen werden können.

Die zwei Intent-Filter dienen zum Empfangen der Push-Nachrichten und zum Empfang der Registration-Id nach erfolgreicher Registrierung. Als gibt man den Packagenamen der Anwendung an, wie man ihn im Manifest-Tag deklariert hat.

C2DM-Kommunikation

Da C2DM rein auf Intents basiert, erfolgt auch die Anmeldung des Clients an den C2DM Servern per Intent.

Listing 2: Intent zum Registrieren beim C2DM-Server

 Intent registrationIntent = new Intent("com.google.android.c2dm.intent.REGISTER");
registrationIntent.setPackage("com.google.android.gsf");
        registrationIntent.putExtra("app",
            PendingIntent.getBroadcast(this, 0, new Intent(), 0));
registrationIntent.putExtra("sender", "Diese E-Mail-Adresse ist vor Spambots geschützt! Zur Anzeige muss JavaScript eingeschaltet sein!");
startService(registrationIntent);

Der Code aus Listing 2 kann fast komplett übernommen werden. Die einzige Änderung, die man an diesem Intent macht, ist das „extra“-Attribut „sender“ anzupassen. Hier trägt man die E-Mail-Adresse ein, mit der man sich bei seinem Google-Konto anmeldet.

Der Intent erhält einige Attribute. Zum einen eine Package-Angabe für den Login-Service (ClientLogin, [3]), der Teil der Google-Services ist, die sich auf den Google Servern befinden. Er übernimmt die Anmeldung am Google-Account.

Zusätzlich wird ein PendingIntent gesetzt übergeben. Dieser dient dazu, den Broadcast-Intent auszulösen, den dann der selbst implementierte BroadcastReceiver der Anwendung fängt.

Abschließend wird der Intent genutzt, um einen Service zu starten. Dies ist zunächst verwirrend. Hier handelt es sich um den ClientLogin-Service auf den GoogleServern, nicht um einen Service auf dem Android-Gerät.

Wer sich einloggt, muss sich auch wieder ausloggen. Man sollte folglich dafür sorgen, dass folgender Code aufgerufen wird, wenn die Anwendung längere Zeit nicht mehr gebraucht wird:

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

Beginnen wir bei der Registrierung. Hat man den Intent für die Registrierung abgesetzt, sollte der Broadcast Receiver den Intent com.google.android.c2dm.intent.REGISTRATION erhalten.

Die Methode handleRegistration extrahiert die Registration-Id (Attribut „registration_id“). Diese müsste nun an den eigenen Server geschickt werden, damit dieser später eine Push-Nachricht an genau diese Anwendung schicken kann. Eine Rückmeldung erhält man über „extra“-Atrribute, die im Broadcast Intent übermittelt werden. Auf diese Weise erhält man die Registration-Id, aber auch, falls diese nicht gesetzt ist, eine Fehlermeldung (Attribut „error“). Das Attribut „unregistered“ wird empfangen, nachdem man sich als Android-Client ordnungsgemäß per Intent von den C2DM-Servern abgemeldet hat. Als Wert erhält man den Packagenamen der Anwendung zurück, wie man ihn im Formular der C2DM-Registrierung der Anwendung angegeben hat.

Server-Simulation

Weiter oben haben wir schon gezeigt, wie man sich mittels cURL an den C2DM-Servern anmeldet. Implementiert man einen solchen Server, setzt man per HTTPS einen Post-Request ab und extrahiert aus der Serverantwort das Authentifizierungstoken (der Schlüssel hinter „Auth=“).

Von den Android-Geräten sollte man die Registration-Ids erhalten haben. Die Id und das Auth-Token werden nun zusammen mit der zu sendenden Nachricht an Google geschickt

curl -k --header "Authorization: GoogleLogin auth=DQAAA...m2801szg" "https://android.apis.google.com/c2dm/send" -d registration_id=APA91bG...PSs1hc -d "data.message=Hallo Android!" -d collapse_key=new

Zum Vergleich zeigt Listing 4, wie man das Verschicken einer Nachricht serverseitig implementieren würde.

Listing 4: Server-Code zum Verschicken einer C2DM-Nachricht

 final HttpClient client = new DefaultHttpClient();        
final String postURL = "https://android.apis.google.com/c2dm/send";        
final HttpPost post = new HttpPost(postURL);
final List<NameValuePair> nvps = new ArrayList<NameValuePair>();
        
// registration_id der Clientanwendung
nvps.add(new BasicNameValuePair("registration_id", mRegistrationId));
nvps.add(new BasicNameValuePair("collapse_key", "new"));

// Nachrichteninhalt
nvps.add(new BasicNameValuePair("data.message", „Hallo Android!“));
        

// Autorisierunstoken muss als Header gesetzt werden.
post.addHeader("Authorization", "GoogleLogin auth=" + mAuthToken);
try {
    UrlEncodedFormEntity ent = new UrlEncodedFormEntity(nvps, HTTP.UTF_8);
    post.setEntity(ent);
} catch (UnsupportedEncodingException e1) {
    e1.printStackTrace();
}
        
//wenn man eine Message-Id zurück bekommt,
//wurde die Nachricht erfolgreich verschickt.
try {
    response = client.execute(post);            
    Log.i(TAG, response.getStatusLine().toString());
 
    HttpEntity entity = response.getEntity();
 
    if (entity != null) {
        InputStream instream = entity.getContent();
        String result = convertStreamToString(instream);
        Log.i(TAG, result);
        instream.close();
    }
} catch (Exception e) {
    e.printStackTrace();
}

Im Code wird die Registrierungs-Id (mRegistrationId) und das Auth-Token (mAuthToken) als gegeben vorausgesetzt. Beachten muss man, dass das Auth-Token nicht als Post-Parameter, sondern im Header übergeben wird.

Es ist durchaus möglich, mehrere Nachrichten in einem Request zu übergeben. Der Parameter „data.message“ ist nicht vorbestimmt. Zwingend notwendig ist nur, dass der Parametername mit „data.“ beginnt. Nun übergibt man so viele Einzelnachrichten mittels selbstdefinierter Parameter (z.B. data.msg_1, data.msg_2 etc), wie man möchte. Beachten muss man jedoch die Obergrenze von 1 Kilobyte. Diese Grenze gilt für die Summer der Länge aller Einzelnachrichten.

C2DM speichert die Nachrichten, falls das oder die Geräte nicht angemeldet sind. Bei der nächsten Anmeldung des Geräts bei den C2DM Servern werden die Nachrichten dann zugestellt. In der Zwischenzeit könnte der Server aber viele Nachrichten ausgeliefert haben, die sich auf den C2DM-Servern stauen.

Um dies zu verhindern und nur die aktuellste Nachricht an das Android-Gerät auszuliefern, gibt es den Parameter „collapse_key“. Er bündelt Nachrichten zu Nachrichten gleichen Typs, wenn sein Wert immer gleich ist. Jede Änderung des Werts eröffnet einen neuen Typ von Nachrichten, von denen jeweils nur die letzte zugestellt wird, wenn das Gerät zwischenzeitlich offline war.

Wissenswertes...

Man sollte sich nicht darauf verlassen, dass ein Android-Gerät, welches einmal angemeldet wurde, immer eine Verbindung zum Server besitzt. Die Registration-Id kann ungültig werden und wird von den C2DM-Servern neu verschickt. Der Broadcast Receiver empfängt dann erneut einen REGISTRATION Intent und dafür sorgen, dass der Anwendungsserver so schnell wie möglich die neue Id erhält, da zwischenzeitlich an die alten Registration-Id gesendete Nachrichten ins Leere gehen.

Der hier vorgestellte Code dient folglich nur dem Verständnis und ist keineswegs vollständig. Besser ist es, sich die von Google bereitgestellten Beispiele „JumpNote“ [5] und „Google Chrome to Phone Extension“ [6] anzuschauen.

Auf der CD zum Heft befindet sich das Beispiel „PushMe“. Es enthält eine Implementierung, die den hier vorgestellten Mechanismus clientseitig abstrahiert. In der Praxis sollte man diese Klassen in sein Programm einbinden (sie sind Open Source). Dann vereinfacht sich die Programmierung fast auf die Implementierung des Broadcast Receivers und man muss sich nicht um Sonderfälle und Exception Handling kümmern. Beachten sollte man in jedem Fall, dass Google nicht garantiert, dass die Push-Nachrichten zugestellt werden. Da man praktisch mit einer Black-Box arbeitet, ist diese Einschränkung in vielen Anwendungsfälle nicht akzeptabel. Daher sollte man, bevor man sich für C2DM entscheidet prüfen, ob man eine Zustellgarantie (oder zumindest eine Fehlermeldung) braucht.

Fazit

Google bietet mit C2DM ein interessantes Push-System an, welches mit wenig Aufwand verwendet werden kann. Ohne die Zustellgarantie ist es jedoch für viele Anwendungsfälle nicht geeignet. Hinzu kommt, dass es sich noch im „Labs“-Status befindet und eine dauerhafte Unterstützung und eine Abwärtskompabilität nicht gewährleistet ist.