Android 4.0 erweitert die Möglichkeiten des App-Entwicklers bedeutend. Dank neuer Programmierschnittstellen kann er seine Anwendungen mit dem Kalender verknüpfen oder per Wi-Fi Direct drahtlosen Kontakt zu anderen Android-Geräten in der Umgebung aufnehmen.
Unter den Neuerungen in Android 4.0 [1] sind für App-Entwickler insbesondere die neuen APIs von Interesse. Dieser Artikel greift zwei besonders praktische Schnittstellen heraus und zeigt, wie man sie in eigenen Anwendungen verwendet. Es geht um die lang erwartete Schnittstelle zu den Kalenderdaten sowie die drahtlose Datenübertragung zwischen Geräten per Wi-Fi Direct.
Zwei Kalender-APIs
Der Kalender ist eine typische und oft genutzte Smartphone-App. Es liegt nahe, diese Daten über ein API auch für andere Apps bereitzustellen. Mit Android 4.0 ist das zum ersten Mal offiziell möglich. Inoffiziell funktionierte das schon länger, aber die Schnittstellen waren von Google weder dokumentiert noch freigegeben. Die Android-Version 4.0 bietet Entwicklern zwei unterschiedliche Kalenderschnittstellen, die jeweils auf Android-Standardkonzepten basieren: Intents und Content-Provider.
Die Intent-basierte Kalenderschnittstelle lässt sich leichter benutzen, bietet aber nur beschränkte Möglichkeiten. Bestimmte Intents rufen dabei die Kalender-App auf, die die weitere Benutzerführung übernimmt. Listing 1 zeigt ein Beispiel für das Anlegen eines Termins. Es arbeitet mit einem impliziten Intent, den es anhand der Aktion »ACTION_INSERT« und des URI »Events.CONTENT_URI« zur Laufzeit auslöst.
Listing 1
Kalender ausfüllen
01 long begin = new GregorianCalendar(2012, 5, 27, 9, 0).getTimeInMillis(); 02 long end = new GregorianCalendar(2012, 5, 29, 18, 0).getTimeInMillis(); 03 Intent intent = new Intent(Intent.ACTION_INSERT, Events.CONTENT_URI); 04 intent.putExtra(Events.TITLE, "Google I/O") 05 .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, begin) 06 .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, end) 07 .putExtra(Events.EVENT_LOCATION, "Moscone Center West, San Francisco") 08 .putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, true); 09 startActivity(intent);
Intent-üblich geben die Extras weitere Daten mit. Die dabei verwendeten Schlüssel sind in den Klassen »CalendarContract« und »Events« zu finden. Den daraus resultierenden Aufruf der Kalender-App mit bereits ausgefüllten Werten zeigt Abbildung 1. Innerhalb der Kalender-App kann der Nutzer die Werte überprüfen, ändern und speichern oder die Aktion abbrechen. Ein Vorteil dieser Methode ist, dass sie keine zusätzlichen Berechtigungen für eine eigene App erfordert. Darüber hinaus können Intents auch den Kalender oder einzelne Termine anzeigen. Listing 2 öffnet im Kalender die Ansicht eines Zeitpunkts (Abbildung 2).
Listing 2
Zeitpunkt öffnen
01 long time = new GregorianCalendar(2012, 5, 27, 9, 0).getTimeInMillis();
02 Uri uri = Uri.parse("content://com.android.calendar/time/" + time);
03 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
04 startActivity(intent);

Abbildung 1: Intents rufen die Kalender-App mit gewünschten Werten auf. Über das Speichern entscheidet der Anwender.
Zum Bearbeiten eines Events kommen ein URI der Form »content://com.android.calendar/events/Event-ID« sowie die »EDIT« -Aktion zum Einsatz. Die Schwierigkeit liegt allerdings darin, an die richtige Event-ID zu kommen. Das geht mit einem Content-Provider besser.
Per Content-Provider
Im Gegensatz zu den Intents funktionieren Androids Content-Provider für den Kalender unabhängig von der Kalender-App und erlauben direkten Zugriff auf Kalenderdaten. Content-Provider ermöglichen in Android ganz allgemein den Datenaustausch über App-Grenzen hinweg. Sie kommen auch für Kontakte, Mediadaten und Systemeinstellungen zum Einsatz. Ihre Handhabung ähnelt dem Zugriff auf Datenbanken inklusive der Spalten- und Zeilensemantik.
Das Vorgehen ist effizient, weil es nur die gewünschten Felder selektiert. Ist der Entwickler etwa nur an der Startzeit eines Events interessiert, kann er diese gezielt auswählen und erspart sich so das Verarbeiten weiterer Daten wie Terminbeschreibung und Ort. Doch ist die Handhabung ein wenig umständlich, weil es für die Termine keine Java-Objektrepräsentationen gibt. Entsprechende Objektwrapper müsste sich der Programmierer bei Bedarf selbst anlegen.
Das Kalender-Datenmodell ist übersichtlich. Es gibt eine beliebige Anzahl an Kalendern, die jeweils mehrere Termine besitzen dürfen. Ein Termin kann mehrere Teilnehmer haben und ihm können mehrere Erinnerungen zugeordnet sein. Des Weiteren kann es bei sich wiederholenden Terminen mehrere Instanzen des Termins geben. Abbildung 3 zeigt das Datenmodell in einer grafischen Übersicht und benutzt die im API verwendeten Klassennamen.

Abbildung 3: Das Datenmodell zeigt die Relationen zwischen Kalender, Terminen, Erinnerungen, Teilnehmern und Instanzen. Die Abbildung verwendet die Klassennamen aus dem Kalender-API.
Mit diesem Hintergrundwissen erschließen sich die einzelnen Schritte in Listing 3. Das zusammenhängende Beispiel besteht aus vier Arbeitsschritten: Zuerst ermittelt es die ID des Kalenders, die es anschließend benutzt, um einen Termin anzulegen. Danach listet es alle verfügbaren Termin-Instanzen auf und löscht zuletzt den angelegten Termin wieder.
Listing 3
Kalenderzugriff per Content-Provider
01 // Teil 1: Kalender-ID selektieren
02 // ----------------------------------
03 ContentResolver resolver = getContentResolver();
04 String[] projection = { Calendars._ID, Calendars.CALENDAR_DISPLAY_NAME };
05 Cursor cursor = resolver.query(Calendars.CONTENT_URI, projection, null, null, null);
06 long calenderId;
07 if (cursor.moveToNext()) {
08 calenderId = cursor.getLong(0);
09 String name = cursor.getString(1);
10 Log.i("My", "Kalender ID " + calenderId + ": " + name);
11 } else {
12 throw new RuntimeException("Kein Kalender verfuegbar");
13 }
14
15 // Teil 2: Termin hinzufuegen
16 // ----------------------------------
17 long now = System.currentTimeMillis();
18 long startMillis = now + 60 * 60 * 1000;
19 long endMillis = startMillis + 60 * 60 * 1000;
20
21 ContentValues values = new ContentValues();
22 values.put(Events.CALENDAR_ID, calenderId);
23 values.put(Events.DTSTART, startMillis);
24 values.put(Events.DTEND, endMillis);
25 values.put(Events.EVENT_TIMEZONE, "CET");
26 values.put(Events.TITLE, "App entwickeln");
27 values.put(Events.DESCRIPTION, "Mit Android 4.0 APIs");
28 Uri eventUri = resolver.insert(Events.CONTENT_URI, values);
29 long id = Long.parseLong(eventUri.getLastPathSegment());
30 Log.i("My", "Neues Event mit ID " + id + " hinzugefuegt");
31
32 // Teil 3: Termininstanzen ausgeben
33 // ----------------------------------
34 Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
35 ContentUris.appendId(builder, now);
36 ContentUris.appendId(builder, now + 200l * 24 * 60 * 60 * 1000);
37
38 projection = new String[] { Instances.EVENT_ID, Instances.BEGIN, Instances.TITLE };
39 cursor = resolver.query(builder.build(), projection, null, null, null);
40 while (cursor.moveToNext()) {
41 long eventId = cursor.getLong(0);
42 Date date = new Date(cursor.getLong(1));
43 String title = cursor.getString(2);
44 Log.i("My", "Event ID " + eventId + ": " + title + " (" + date + ")");
45 }
46
47 // Teil 4: Angelegten Termin wieder loeschen
48 // ----------------------------------
49 int deleteCount = getContentResolver().delete(eventUri, null, null);
50 Log.i("My", "Termine geloescht: " + deleteCount);
URIs weisen den Weg
Teil 1 von Listing 3 beschafft sich die ID des ersten Kalenders, den er im System findet. Hier zeigt sich bereits die grundlegende Verwendung des Content-Providers. Statt der bei Datenbanken üblichen Tabellennamen kommen URIs zum Einsatz. Android verwaltet intern ein Mapping, welche URIs zu welchen Content-Providern gehören. Die URIs für System-Content-Provider sind über Konstanten in den APIs fest hinterlegt. Im Falle des Kalenders finden sie sich in der Klasse »android.provider.CalendarContract« [2] sowie in deren inneren Klassen wie »Calendars« , »Events« und »Instances« .
Die Klasse »ContentResolver« ermöglicht den Zugriff auf Content-Provider (Zeile 3). Ein entsprechendes Objekt liefert die Methode »getContentResolver()« , die in jedem Kontext zur Verfügung steht, also beispielsweise über die »Activity« -Klasse. Die »query()« -Methode des »ContentResolver« bekommt den URI übergeben, um den passenden Content-Provider zu adressieren (Zeile 5). Die Variable »projection« gibt zudem an, dass nur bestimmte Werte gefragt sind: Für das Beispiel genügen die Kalender-ID und der Darstellungsname des Kalenders.
Datenbank-Cursor
Das Ergebnis der »query()« -Methode ist ein »Cursor« -Objekt, das viele bereits vom Umgang mit Datenbanken kennen dürften. Mit »moveToNext()« (Zeile 7) springt der Cursor zum jeweils nächsten Datensatz, dessen Werte sich dann nach Spalten erfragen lassen. Im Beispiel ist die ID in Spalte 0 zu finden, da sie im »projection« -Array an erster Stelle steht. Findet das Programm keinen Kalender, wirft dieser Code eine Exception. Produktive Apps sollten hier eine Fehlermeldung anzeigen oder bei fehlender Eindeutigkeit den Nutzer fragen, welchen Kalender er verwenden möchte.
Der zweite Teil von Listing 3 fügt einen neuen Termin hinzu, der in einer Stunde beginnt und 60 Minuten dauert. Dazu sammelt der Code die Werte für den Termin in dem Map-artigen Objekt »ContentValues« (Zeile 21). Die Schlüssel erlaubter Werte sind in der Klasse »Events« als Konstanten zu finden. Die Kalender-ID, der Startzeitpunkt und die Zeitzone sind dabei Pflichtangaben. Bei einmaligen Terminen, wie im vorliegenden Fall, muss der Programmierer auch eine Endzeit angeben. Bei sich wiederholenden Events sind stattdessen Dauer (»DURATION« ) und Wiederholungsrhythmus (»RRULE« oder »RDATE« ) gefordert.
Mit diesen vorbereiteten Daten ruft das Listing die »insert()« -Methode des »ContentResolver« auf (Zeile 28). Diese gibt ein »Uri« -Objekt zurück, mit dem sich der neue Datensatz adressieren lässt. Der letzte Teil des erhaltenen URI entspricht dabei der Event-ID, die das Beispielprogramm in einem Log-Eintrag ausgibt (Zeile 30).
Der dritte Teil des Programmierbeispiels gibt alle Termine der kommenden 200 Tage aus und arbeitet, korrekt ausgedrückt, mit Termin-Instanzen, die Terminwiederholungen berücksichtigen. Jede Wiederholung ist eine weitere Instanz des Termins. Ähnlich wie im ersten Teil findet hier eine Abfrage über den »ContentResolver« statt. Neu ist jedoch, dass der URI an dieser Stelle bereits den Abfragezeitraum beinhalten muss. Die Variable »projection« (Zeile 38) definiert, dass Event-ID, Termin-Titel und -Start gefragt sind. Mit Hilfe der While-Schleife in den Zeilen 40 bis 45 iteriert der Code über den Cursor, solange dieser weitere Datensätze bereitstellt.
Das Codebeispiel schließt im vierten Teil damit ab, dass es den angelegten Termin wieder entfernt. Dazu benötigt es den URI des Termins, den es sich beim Anlegen in Zeile 28 gemerkt hat.
Berechtigungen
Damit das Beispiel auch läuft, müssen die erforderlichen Berechtigung gesetzt sein. Aus Sicherheitsgründen erfordert der Zugriff auf Kalenderdaten entsprechende »uses-permission« -Einträge in der Datei »AndroidManifest.xml« . Für reinen Lesezugriff ist die Berechtigung »android.permission.READ_CALENDAR« erforderlich. Falls das Programm den Kalender auch modifizieren soll, braucht es die Berechtigung »android.permission.WRITE_CALENDAR« .
Der Anwender wird beim Herunterladen der App aus dem Market auf die angeforderten Berechtigungen aufmerksam gemacht, fehlen diese, würde die App eine Exception werfen. Des Weiteren sei noch angemerkt, dass Google empfiehlt, den Zugriff auf Content-Provider asynchron zu machen, also nicht im Hauptthread laufen zu lassen. Das erreicht man zum Beispiel mit »AsyncTask« oder noch besser mit Loadern, die seit Version 3.0 Bestandteil von Android sind.
Im Zusammenhang mit Content-Providern stehen auch Erweiterungen der Kontaktdatenbank. Erstmals kann sich der Anwender selbst mittels eines eigenen Profils in der Kontaktapplikation verwalten. Andere Anwendungen dürfen diese Daten auslesen und sogar ändern, vorausgesetzt sie besitzen die dafür erforderlichen Berechtigungen. Das kleine Codebeispiel in Listing 4 nutzt die neue Klasse »ContactsContract.Profile« zum Auslesen der Profil-Informationen.
Listing 4
Profil auslesen
01 Cursor cursor = resolver.query(Profile.CONTENT_URI, null, null, null, null); 02 DatabaseUtils.dumpCursor(cursor);
P2P mit Wi-Fi Direct
Eine weitere interessante Neuerung in Android 4.0 ist die Unterstützung des Funkstandards Wi-Fi Direct. Damit lassen sich Peer-to-Peer-Verbindungen zwischen Android-Geräten über WLAN aufgebauen. Das funktioniert unabhängig von einer Internetverbindung, einem Server oder Router. Der neue Standard überträgt auch große Datenmengen mit geringer Latenz und über wesentlich weitere Strecken als Bluetooth.
Die zentrale Anlaufstelle des Wi-Fi-Direct-API ist die Klasse »WifiP2pManager« [3] aus dem neuen Package »android.net.wifi.p2p« . Listing 5 setzt eine einfache Klasse namens »MinimalWiFiDirect« um, die mit dem API eine Liste der in Reichweite befindlichen Peers ausgibt. Die »init()« -Methode registriert zunächst die Klasse als »BroadcastReceiver« . Danach muss das Programm Wi-Fi Direct initialisieren. Dazu erzeugt es ein »WifiP2pManager« -Objekt und ruft die Methode »initialize()« auf (Zeile 12). Das zurückgegebene »Channel« -Objekt dient als Handle für weitere API-Aufrufe.
Listing 5
Wi-Fi Direct
01 public class MinimalWiFiDirect extends BroadcastReceiver implements PeerListListener {
02
03 private WifiP2pManager manager;
04 private Channel channel;
05
06 public void init(Context context) {
07 IntentFilter intentFilter = new IntentFilter();
08 intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
09 context.registerReceiver(this, intentFilter);
10
11 manager = (WifiP2pManager) context.getSystemService(Context.WIFI_P2P_SERVICE);
12 channel = manager.initialize(context, context.getMainLooper(), null);
13 manager.discoverPeers(channel, null);
14 Log.i("My", "Warte auf Wi-Fi Direct Broadcasts...");
15 }
16
17 @Override
18 public void onReceive(Context context, Intent intent) {
19 String action = intent.getAction();
20 if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
21 Log.i("My", "Wi-Fi Direct Peers haben sich veraendert");
22 manager.requestPeers(channel, this);
23 }
24 }
25
26 @Override
27 public void onPeersAvailable(WifiP2pDeviceList wifiP2pDeviceList) {
28 Collection<WifiP2pDevice> deviceList = wifiP2pDeviceList.getDeviceList();
29 for (WifiP2pDevice device : deviceList) {
30 Log.i("My", "Wi-Fi Direct Device: " + device.deviceName + ", " + device.deviceAddress);
31 }
32 }
33 }
In Zeile 13 startet »discoverPeers()« die Suche nach Geräten, die sich in Reichweite befinden. Da dies asynchron passiert, empfiehlt es sich, einen »ActionListener« als zweiten Parameter zu übergeben, auf den das Beispiel aber aus Platzgründen verzichtet. Der Listener besteht aus zwei Callback-Methoden, von denen nur eine aufgerufen wird, je nachdem, ob die Suche erfolgreich gestartet ist (»onSuccess« ) oder nicht (»onFailure« ).
Zum Zeitpunkt des Callback ist der Suchvorgang erst gestartet, auf das Ergebnis muss das Programm also anders reagieren. Hier kommt der »BroadcastListener« ins Spiel. Einer der vom System abgeschickten Broadcasts ist »WIFI_P2P_PEERS_CHANGED_ACTION« , er gibt an, dass sich die Liste der gefundenen Geräte geändert hat. In der zum »BroadcastReceiver« gehörenden Methode »onReceive« prüft die App, ob es sich um den entsprechenden Broadcast handelt.
Aber auch damit ist die Aufgabe noch nicht ganz erledigt. Erst nach dem Aufruf von »requestPeers()« wird die Klasse über das Interface »PeerListListener« asynchron aufgerufen. Dieser Listener besteht aus der Methode »onPeersAvailable()« , welche die Beispielklasse direkt implementiert. Nun kann das Programm endlich auf die Liste der verfügbaren Geräte zugreifen.
Datenaustausch
Soll die App mit einem Gerät Daten austauschen, muss sie eine Verbindung aufbauen. Dieser Vorgang ist etwas umfangreich, dieser Artikel soll nur zeigen, wie er im Prinzip funktioniert: Das vorige Codebeispiel hat eine Liste von »WifiP2pDevice« -Objekten erzeugt (Listing 5, Zeile 28). Wählt der Programmierer eines davon aus, kann er mit der darin enthaltenen Adresse ein »WifiP2pConfig« -Objekt erzeugen, das er für die »connect()« -Methode des »WifiP2pManager« benötigt. Diese Methode löst einen weiteren Broadcast aus. Danach kann der »BroadcastReceiver« prüfen, ob das Gerät die Verbindung erfolgreich aufgebaut hat. Genauer gesagt gründet Android eine Peer-to-Peer-Gruppe mit dem anderen Gerät oder erweitert sie um das Gerät.
Mit Hilfe des »WifiP2pManager« und der Methode »requestConnectionInfo()« kann das Programm dann Informationen über die Verbindung holen, die ihm über ein weiteres Callback-Interface und die Klasse »WifiP2pInfo« zugestellt werden. Insbesondere die IP-Adresse des »Owners« der Peer-to-Peer-Gruppe ist interessant. Sie erlaubt es schließlich, die übliche Socketverbindung aufzubauen. Die Verwaltung des Serversockets liegt vollständig in der Hand des Programmierers.
Fragmente
Eine bedeutsame Änderung für die Gestaltung von Apps sind die so genannten Fragmente, die bereits Android 3 zur Unterstützung von Tablets eingeführt hatte. Fragmente dienen dazu, eine Applikation noch modularer aufzuteilen. Neben den Activities steht somit ein weiterer grundlegender Baustein für den Applikationsentwickler zur Verfügung.
Der Vorteil von Fragmenten liegt in ihrer Wiederverwendbarkeit in mehreren Activities. Beispielsweise kann ein Entwickler zwei Fragmente in einer Activity anordnen, wenn der Screen groß genug ist – etwa auf einem Tablet oder dem Google TV. Sie lassen sich aber auch in zwei getrennten Activities benutzen, wenn es sich um ein kleineres Smartphone handelt.
Dieses Vorgehen findet oft bei Listendarstellungen Anwendung, die im zweiten Schritt zu einem ausgewählten Listeneintrag Details darstellen. Mittels Fragmenten können sowohl Liste als auch Details gleichzeitig erscheinen, wenn das Gerät genug Platz bietet (Abbildung 4). Zwar sind Fragmente keine Neuerung von Android 4.0, aber sie werden bedeutsamer, da sie jetzt erstmals Tablets und Smartphones gleichzeitig adressieren. Daher seien sie jedem App-Entwickler ans Herz gelegt [4].

Abbildung 4: Fragmente erlauben es, gleichzeitig Liste (links) und Detailansicht (rechts) darzustellen.
Handy oder Emulator
Am besten lassen sich die Codebeispiele dieses Artikels auf einem Android-4.0-Gerät wie dem Galaxy Nexus testen. Wer den Emulator des SDK nutzen möchte, sollte ein paar Dinge beachten: Das Kalenderbeispiel benötigt ein Konto, doch normale Google-Accounts funktionieren nicht direkt im Emulator. Im Internet finden sich jedoch Anleitungen, wie man Konten über die Adresse »m.google.com« als Exchange-Konto hinzufügen kann [5]. Die neuen APIs für Wi-Fi Direct, Bluetooth und NFC (Android Beam) unterstützt der Emulator in der jetzigen Version nicht.
Android 4.0 bietet noch etliche weitere Neuerungen: Insgesamt kommen 90 neue Klassen hinzu und 192 Klassen haben sich geändert [6]. Google entwickelt Android in hoher Geschwindigkeit weiter und deckt immer mehr Anwendungsfälle ab. Gleichzeitig bleiben einschneidende Änderungen aus, was man als Zeichen einer zunehmenden Reife und Stabilisierung der APIs interpretieren kann. Entwickler haben jedenfalls die Wahl, ob sie mit den neuen APIs sofort innovative Apps bauen möchten oder erst auf eine weitere Verbreitung von Android-4.0-Geräten warten möchten. (mhu)
Infos
- Neuerungen für Entwickler in Android 4: http://developer.android.com/sdk/android-4.0-highlights.html
- »CalendarContract« : http://developer.android.com/reference/android/provider/CalendarContract.html
- »WifiP2pManager« : http://developer.android.com/reference/android/net/wifi/p2p/WifiP2pManager.html
- Fragmente: http://developer.android.com/reference/android/app/Fragment.html
- Testkonten für den Emulator: http://stackoverflow.com/questions/7959847/how-to-use-and-test-the-android-4-0-calendar-api-in-the-emulator/8017783
- Neue und geänderte Klassen: http://developer.android.com/sdk/api_diff/14/changes.html
- Listings zum Artikel: https://www.linux-magazin.de/static/listings/magazin/2012/02/android-entwicklung/






