Mobile Applikationen –
also Apps – werden oft erst wirklich interessant, wenn ein
Daten-Austausch mit einer Server Applikation erfolgt. Das übliche
Request-Response-Modell sieht dann so aus, dass der Client – in
diesem Fall die App – eine Anfrage (Request) an den Server sendet
und sofort synchron die Antwort des Servers (Response) bekommt.
In vielen Fällen ist
das völlig ausreichend. Wie sieht es aber aus, wenn interessante
Dinge auf dem Server passieren und der Client darüber informiert
werden muss?
Sieht man die
„Hol-Schuld“ beim Client, würde der Client „pollen“, also in
regelmäßigen Zeitabständen beim Server nachfragen, ob es was Neues
gibt. Die Nachteile liegen auf der Hand: ist das Zeitintervall zu
klein, findet in zu vielen Fällen ein unnötige Kommunikation statt,
ist das Zeitintervall zu groß ist die Information unter Umständen
„nicht ausreichend aktuell“.
Es muss also eine
Möglichkeit geben, die mobilen Geräte seitens des Servers
anzusprechen. Letztendlich kann die Erreichbarkeit der Geräte nur
unter Mithilfe der Anbieter der Mobiltelefon-Systeme gewährleistet
werden, da nur diese die entsprechenden Information besitzen. Die
Optionen, das Gerät per Anruf oder SMS zu benachrichtigen, sind in
diesem Fall nicht fein-granular genug, da sie das gesamte Gerät und
nicht eine spezielle App erreichen. Daher stellen die Anbieter
sogenannte „Push“-Dienste zur Verfügung, die es erlauben die
Apps servergesteuert zu erreichen – die „Bring-Schuld“ liegt in
diesem Fall also beim Server. Für iPhones stellt selbstverständlich
Apple den entsprechenden Service zur Verfügung, für Android ist das
als „Google Cloud Messaging“ (GCM) Bestandteil der sogenannten
„Google Play Services“. Darunter sind weitere Schnittstellen zu
Google-Diensten zusammengefasst, wie z.B. Google Maps, Google+ oder
auch für die Abwicklung von Bezahlvorgängen (In-App-Payment).
Die App muss sich dazu
als potenzieller Empfänger von Push-Nachrichten registrieren (1, 2)
und die entsprechenden Verbindungsdaten der Server-Komponente zur
Verfügung stellen (3). Mit der Identifikation des Clients, kann der
Server ein für den Client interessantes Ereignis (4) dem
Push-Service mitteilen (5). Dieser ist dann dafür verantwortlich,
dieses zeitnah dem Mobilgerät weiterzuleiten (6).
Um das Thema einfachst
möglich zu demonstrieren, werde ich hier eine App vorstellen, die
mit ihren Verbindungsdaten den GCM-Service direkt anspricht. Das
Beispiel ist eigentlich nur zu Demonstrationszwecken sinnvoll,
jedenfalls fällt mir keine Anwendung ein, für die es sinnvoll wäre,
dass eine App sich selbst über GCM benachrichtigen sollte.
Allenfalls die Benachrichtigung der App auf einem anderen Device wäre
eine vorstellbare Möglichkeit. Diese wäre dann aber aus
architektonischer Sicht letztendlich nur ein Ersatz der
Server-Komponente im oberen Fall.
Für weitere
Informationen gibt es bei Google es ein sehr brauchbares „Getting
Started“-Tutorial
(http://developer.android.com/google/gcm/index.html).
Ich will mich aber auf die reine Android-App beschränken.
Vorbereitung: Google-Console
In der Google Cloud
Console „https://cloud.google.com/console“
ist ein (neues) Projekt anzulegen. Für dieses muss unter „APIs &;
auth / APIs“ das „Google Cloud Messaging for Android“ auf „ON“
geschaltet sein. Die „Project Number“ kann man sich schon mal
merken (im Sinne von kopieren), die benötigen wir später noch.
Zur Absicherung des
Zugriffs auf den GCM-Service benötigt man einen ServerKey für das
Projekt. Falls dieser noch nicht vorhanden ist (zu finden bei
„Registered apps“ als „ServerKey“), muss er erzeugt werden.
In der Google Cloud Console ist das relativ kompliziert, da man einen
SHA-Fingerprint benötigt – beschrieben im Tutorial. Noch gibt es
allerdings die „alte“ Google Apis Console
(„https://code.google.com/apis/console/“), unter der ein
ServerKey per Knopfdruck generiert werden kann (dort unter „API
Access“, „Create new Server key“). Die Google Apis Console
versucht, auf die Google Cloud Console weiterzuleiten. Noch – Stand
November 2013 – ist es jedoch möglich, per „Dismiss“ die alte
Console aufzurufen. Falls das irgendwann nicht mehr möglich ist,
muss wohl der im Tutorial beschriebene Weg verwendet werden.
Der ServerKey wird
später dem GCM übergeben und kann als Text herauskopiert werden.
Der Key sollte aktiv sein und „Any IP address is allowed“ sollte
angezeigt werden.
Vorbereitung: Entwicklungsumgebung
Das GCM ist ein
Bestandteil der „Google Play Services“. Diese müssen im „Android
SDK Manager“ installiert sein. Zu finden sind sie unter dem Ordner
„Extras“.
Um unter Eclipse auf
die Library zuzugreifen, muss sie in den Workspace importiert werden.
Dazu über den „File > Import“ Dialog „Android > Existing
Android Code Into Workspace“ auswählen und dort das Root
Verzeichnis und „Copy Projects Into Workspace“ wählen. Das
Root-Verzeichnis für das Google Play Library Project ist
„/extras/google/google_play_services/libproject/google-play-services_lib“.
Zum Vorgehen bei anderen Umgebungen als Eclipse: siehe GCM-Tutorial.
Für die zu erstellende
App wird ein Android-Projekt angelegt, dass die Google Play Library
verwendet. Dafür in den Properties des neuen Projekts im Punkt
„Android“ unter „Library“ das „google-play-services_lib“
Projekt hinzufügen.
Vorbereitung: Architektur
Wir erstellen eine App,
die nur aus einer Activity besteht. Diese enthält nur den Button, um
das Senden des Push auszulösen. Zum Empfangen der Push-Nachricht
muss ein BroadcastReceiver implementiert werden.
Im Manifest-File müssen
einige permissions definiert werden:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission
android:name="de.kluck.push.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="de.kluck.push.permission.C2D_MESSAGE" />
Für die neueste
Version der Google Play Library muss innerhalb des application-Tags
folgender Eintrag vorhanden sein:
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
Activity 1. Anlauf – ganz einfach
Es sind zwei Schritte unbedingt notwendig: das
Registrieren der App am GCM und das eigentlich Auslösen der
Push-Nachricht.
Im einfachsten Fall verwenden wir eine Activity
mit 2 Buttons, die genau diese Operationen auslösen. Wir verwenden
den Button mit der Id „registerButton“ zum Registrieren und Holen
der RegistrationId. Die RegistrationId legt letztendlich fest, für
welche App das Device angesprochen werden soll. Da es sich um einen
externen Service-Aufruf handelt, muss er in einem AsyncTask erfolgen.
Dazu wird die PROJECT_NUMBER benötigt. Das ist diejenige, die in der
Google Cloud Console angezeigt wird – siehe oben. Das Device
registriert sich also für genau dieses Projekt:
String PROJECT_NUMBER = "...";
Context context;
String regid;
...
context = getApplicationContext();
View registerButton = findViewById(R.id.registerButton);
registerButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new AsyncTask() {
@Override
protected String doInBackground(String... arg0) {
String regId = null;
try {
String projectNumber = arg0[0];
GoogleCloudMessaging gcm =
GoogleCloudMessaging.getInstance(context);
regId = gcm.register(projectNumber);
} catch (IOException e) {
Log.e(TAG, "Error on register", e);
}
return regId;
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
regid = result;
}
}.execute(PROJECT_NUMBER);
}
});
Als Auslöser der „eigentlichen“
Funktionalität verwenden wir einen Button mit der Id „pushButton“.
Der Aufruf erfolgt auch hier in einem AsyncTask, dem die gerade
erworbene RegistrationId des Devices ebenso übergeben wird, wie der
API-Key der Anwendung – wie oben beschrieben aus der Google Cloud
bzw. Google Apis Console zu beziehen. Letzterer wird dann als
Authorization Key an die HttpConnection gehängt. Dadurch soll
sichergestellt werden, dass nur berechtige Zugriffe erfolgen. Die
Registration Id ist Bestandteil des Contents des Requests. Neben der
hier verwendeten Möglichkeit, die Nutzdaten in einem JSON-String zu
verpacken, gibt es auch die Möglichkeit, Daten direkt in der URL
mitzugeben. Das ist aber nicht ganz so flexibel, insbesondere kann
nur eine einzelne RegistrationId angesprochen werden. Mit anderen
Worten: es kann durchaus durch Angabe einer Liste von
RegistrationsIds eine ganze Reihe von Devices parallel angesprochen
werden! Der „data“-Teil des JSON-Objekts ist nicht beliebig
flexibel, letztlich ist es nur eine Liste von Key-Value-Paaren.
View pushButton = findViewById(R.id.pushButton);
pushButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new AsyncTask() {
@Override
protected Void doInBackground(String... arg0) {
try {
String apiKey = arg0[0];
String regId = arg0[1];
URL url =
new URL("https://android.googleapis.com/gcm/send");
HttpURLConnection connection =
(HttpURLConnection)url.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Authorization",
"key=" + apiKey);
connection.setRequestProperty("Content-Type",
"application/json");
OutputStreamWriter writer =
new OutputStreamWriter(
connection.getOutputStream());
writer.write("{\"registration_ids\" : [\"" + regId +
"\"],\"data\" : { \"action\" : \"push\"}}");
writer.close();
int responseCode = connection.getResponseCode();
Log.i(TAG, "responseCode: " + responseCode);
} catch (Exception e) {
Log.e(TAG, "Error on service call", e);
}
return null;
}
}.execute(API_KEY, regid);
}
});
}
BroadcastReceiver
Der BroadcastReceiver
zum Empfangen der Push-Nachricht wird im Manifest-File mit der
entsprechenden Berechtigung und dem passenden Intent-Filter
definiert:
<receiver
android:name="de.kluck.push.GcmBroadcastReceiver"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="de.kluck.push" />
</intent-filter>
</receiver>
Die Implementierung
soll für dieses Beispiel einfach nur den „action“-Betandteil des
Intent-Extras „toasten“:
public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Result: " +
intent.getStringExtra("action"), Toast.LENGTH_SHORT).show();
setResultCode(Activity.RESULT_OK);
}
}
Die oben angesprochenen
Key-Value-Paare des JSON-“data“-Bestandteil des HTTP-Requests
finden sich tatsächlich hier in den „Extras“ des Intents wieder.
Man hat so also die Möglichkeit, Daten vom Server zum Device zu
transportieren. Umfang und Struktur sind zwar eingeschränkt, aber es
besteht ja immer noch die Möglichkeit, beispielsweise eine Id zu
übergeben und die kompletten Daten über den „normalen“
Request-Response-Weg zu holen.
Activity 2. Anlauf – etwas sauberer, bitte
An dem obigen Beispiel stören zwei Dinge: zum
einen werden die Google Play Services von der App als vorhanden
vorausgesetzt. Da es sich um einen zu installierenden Service
handelt, sollte die Existenz geprüft werden, bevor er verwendet
wird. Zum anderen muss die Registrierung der App explizit per
Knopfdruck erfolgen. Bei einer richtigen Anwendung sollte das
transparent im Hintergrund stattfinden und da es sich um einen
externen Aufruf handelt, auch nicht öfter als unbedingt notwendig.
Um das Vorhandensein der GooglePlayServices auf
dem Device zu prüfen, steht ein GooglePlayServicesUtil zur
Verfügung, was praktischerweise auch gleich Methoden zur Ausgabe der
entsprechenden Fehler-Dialoge enthält:
private static final String TAG = "MainActivity";
private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
private boolean checkPlayServices() {
int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (resultCode != ConnectionResult.SUCCESS) {
if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
GooglePlayServicesUtil.getErrorDialog(resultCode, this,
PLAY_SERVICES_RESOLUTION_REQUEST).show();
} else {
Log.i(TAG, "This device is not supported.");
finish();
}
return false;
}
return true;
}
Nur wenn dieser Test erfolgreich ist, kann über
das GoogleCloudMessaging die RegistrationId des Devices geholt
werden:
private static final String PROPERTY_REG_ID = "registration_id";
private static final String PROPERTY_APP_VERSION = "appVersion";
String PROJECT_NUMBER = "...";
String API_KEY = "...";
GoogleCloudMessaging gcm;
Context context;
String regid;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ...
context = getApplicationContext();
// Check device for Play Services APK
if (checkPlayServices()) {
// Proceed with GCM registration.
gcm = GoogleCloudMessaging.getInstance(this);
regid = getRegistrationId(context);
if (regid.isEmpty()) {
registerInBackground();
}
} else {
Log.i(TAG, "No valid Google Play Services APK found.");
}
}
Angelehnt an das GCM-Tutorial und mit dem
Hintergrund, dass der Zugriff auf den GCM-Service nicht bei jedem
Start der App erfolgen soll, wird hier die RegistrationId in den
SharedPreferences der App gespeichert und nur neu geholt, wenn
notwendig. Nämlich dann, wenn sich die App signifikant geändert
hat.
private String getRegistrationId(Context context) {
final SharedPreferences prefs = getGCMPreferences(context);
String registrationId = prefs.getString(PROPERTY_REG_ID, "");
if (registrationId.isEmpty()) {
Log.i(TAG, "Registration not found.");
return "";
}
int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION,
Integer.MIN_VALUE);
int currentVersion = getAppVersion(context);
if (registeredVersion != currentVersion) {
Log.i(TAG, "App version changed.");
return "";
}
return registrationId;
}
private SharedPreferences getGCMPreferences(Context context) {
return getSharedPreferences(MainActivity.class.getSimpleName(),
Context.MODE_PRIVATE);
}
private static int getAppVersion(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionCode;
} catch (NameNotFoundException e) {
throw new RuntimeException("Could not get package name: " + e);
}
}
Nur dann, wenn keine (aktuelle) RegistrationId in
den SharedPreferences hinterlegt ist, muss der Service aufgerufen
werden – auch hier natürlich über einen asynchronen Aufruf.
private void registerInBackground() {
new AsyncTask() {
@Override
protected String doInBackground(Void... params) {
String msg = "";
try {
if (gcm == null) {
gcm = GoogleCloudMessaging.getInstance(context);
}
regid = gcm.register(PROJECT_NUMBER);
msg = "Device registered, registration ID=" + regid;
storeRegistrationId(context, regid);
} catch (IOException ex) {
msg = "Error :" + ex.getMessage();
}
return msg;
}
@Override
protected void onPostExecute(String msg) {
Log.i(TAG, msg);
}
}.execute(null, null, null);
}
private void storeRegistrationId(Context context, String regId) {
final SharedPreferences prefs = getGCMPreferences(context);
int appVersion = getAppVersion(context);
Log.i(TAG, "Saving regId on app version " + appVersion);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PROPERTY_REG_ID, regId);
editor.putInt(PROPERTY_APP_VERSION, appVersion);
editor.commit();
}
Das war dann auch „schon“ der notwendige
Verwaltung-Overhead. Um ganz sauber zu sein, sollte man auch noch in
der onResume-Methode der Activity die PlayServices abchecken, um
sicher zu stellen, dass sie nicht zwischenzeitlich deinstalliert
wurden:
@Override
protected void onResume() {
super.onResume();
checkPlayServices();
}
Test der App
Die App ist in der vorgestellten Form nicht im
Emulator lauffähig und muss daher auf einem realen Gerät getestet
werden. Dort sollte nach einem Druck auf den Button relativ schnell
der Toast mit der Nachricht erscheinen – ganz unspektakulär.
Fazit
Wie bereits gesagt,
dieses Beispiel ist weltfremd durch den Verzicht auf eine
Server-Komponente. Ich hoffe aber, damit in aller Kürze demonstriert
zu haben, was seitens der Android-App notwendig ist, um den
Push-Dienst zu verwenden. Die relevanten Teile der Server-Komponente
wären auch nur das Ansprechen des HTTP-Dienstes, was inhaltlich
völlig analog zum hier dargestellten Zugriff innerhalb der App
erfolgen würde.
Keine Kommentare:
Kommentar veröffentlichen