Googles Android: Direktausdruck

Aus Wikibooks


Druckversion des Buches Googles Android
  • Dieses Buch umfasst derzeit etwa 87 DIN-A4-Seiten einschließlich Bilder (Stand: Aktuell).
  • Wenn Sie dieses Buch drucken oder die Druckvorschau Ihres Browsers verwenden, ist diese Notiz nicht sichtbar.
  • Zum Drucken klicken Sie in der linken Menüleiste im Abschnitt „Drucken/exportieren“ auf Als PDF herunterladen.
  • Mehr Informationen über Druckversionen siehe Hilfe:Fertigstellen/ PDF-Versionen.
  • Hinweise:
    • Für einen reinen Text-Ausdruck kann man die Bilder-Darstellung im Browser deaktivieren:
      • Internet-Explorer: Extras > Internetoptionen > Erweitert > Bilder anzeigen (Häkchen entfernen und mit OK bestätigen)
      • Mozilla Firefox: Extras > Einstellungen > Inhalt > Grafiken laden (Häkchen entfernen und mit OK bestätigen)
      • Opera: Ansicht > Bilder > Keine Bilder
    • Texte, die in Klappboxen stehen, werden nicht immer ausgedruckt (abhängig von der Definition). Auf jeden Fall müssen sie ausgeklappt sein, wenn sie gedruckt werden sollen.
    • Die Funktion „Als PDF herunterladen“ kann zu Darstellungsfehlern führen.


Vorwort


Einleitung


Eine Übersicht zu Android

Zurück zu Googles Android



Faszination Smartphone

Smartphones begeistern auch wenig technikaffine Menschen seit einigen Jahren. Die Kombination aus Telefonie, GPS, Internet und Multimedia-Inhalten bietet Entwicklern mit Phantasie ein breites Spektrum an Anwendungsmöglichkeiten. Kaum ein Tag vergeht, an dem nicht eine neue originelle App auf einem der virtuellen Marktplätze angeboten wird.

Derzeit ist Android, gefolgt von Apples iOS, das meistverwendete Betriebssystem auf Smartphones und Tablet-Computern. Da Android anders als vergleichbare Betriebsysteme offen ist, kann es auch auf andere Geräte portiert werden. In diesem Buch lernen Sie vieles zum Betriebssystem Android und Möglichkeiten zur Anwendungsentwicklung.

Linux Inside

Obwohl Android auf Linux 2.6 basiert, gibt es doch einige ganz erhebliche Unterschiede zu den klassischen Linux-Systemen. Für Systemaufrufe wird nicht wie sonst die Bibliothek glibc, sondern die androideigene Entwicklung bionic verwendet. Für diesen – auf den ersten Blick ungewöhnlichen Schritt – gibt es gute Gründe:

  • Lizenz: glibc nutzt die GPL (GNU General Public License), bionic dagegen die BSD-Lizenz.

GPL erfordert das so genannte Copyleft und somit, dass jede Software, die glibc nutzt, selbst unter die GPL zu stellen ist. Diese Anforderung schränkt die kommerziellen Anpassungen von Android stark ein. Selbst wenn kommerzielle Interessen keine primäre Rolle spielen, kann man aus dem Quellcode unter Umständen auf interne Details des Gerätes schließen, die nicht öffentlich gemacht werden sollen.

  • Größe: Die glibc benötigt etwa 400 kByte je Prozess, bionic dagegen gerade mal die Hälfte.

Ein Vorteil, der natürlich besonders auf mobilen Endgeräten mit ihren begrenzten Ressourcen zum Tragen kommt.

Darüber hinaus gilt bionic und insbesondere ihre Thread-Bibliothek als sehr schnell.

Java

Apps für Android werden in Java entwickelt. Das hat den großen Vorteil, dass so eine große Gruppe von Entwicklern nach einer Einarbeitung in das javabasierte Android-SDK schnell in der Lage ist, für die Plattform zu entwickeln. Mit Hilfe des Android NDK (Native Development Kit) kann aber auch nativer Code, also Code der mit etwa mit Hilfe eines C- oder C++-Compilers übersetzt wurde, integriert werden. Die Entwicklungsumgebung für Android Apps findet man auch in der folgenden Abbildung:

Wir sehen, dass die Java-Quellen in der Entwicklungsumgebung geschrieben und wie gewohnt in das class-Format als Java-Bytecode übersetzt werden. Anders als bei der Arbeit mit dem Java-SDK werden die class-Dateien dann in das so genannte dex-Format (Dalvik Executable) übersetzt. Eine dex-Datei kann dabei mehrere class-Dateien umfassen. Es handelt sich aber nicht nur um die Zusammenfassung mehrerer Dateien zu einer einzigen, sondern wirklich um die Übersetzung in neuen Programm-Code. Doch dazu gleich mehr. Die dex-Datei wird mit weiteren Dateien, wie etwa dem Manifest, zur eigentlichen App zusammengefasst, deren Datei die Endung apk erhält. Diese apk-Datei wird dann auf dem Gerät installiert.

Der dex-Code aus der App wird dort von der Dalvik Virtual Machine (DVM) ausgeführt. Die DVM wurde von Dan Bornstein bei Google entwickelt und eignet sich besonders als Laufzeitumgebung für Low-End-Geräte. Sie basiert auf einer frei verfügbaren Java-Umgebung namens Apache Harmony und zeichnet sich durch einen geringen Bedarf an Arbeitsspeicher und Strom aus. Da die DVM keinen Java-Byte-Code ausführt, ist sie keine JVM und fällt somit nicht unter Lizenzbestimmungen von Oracle.

Neben dem lizenzrechtlichen Vorteil ergibt sich aber auch ein erheblicher Performance-Unterschied: Die JVM basiert auf dem Maschinenmodell der Kellermaschine. Konkrete Computer sind aber Registermaschinen. Die Bytecode-Anweisungen, die für eine Kellermaschine entwickelt wurden, müssen also zur Laufzeit in Anweisungen für Registermaschinen umgesetzt werden. In der Mitte der 1990-er Jahre interpretierten die frühen JVM-Implementierungen den Bytecode und benötigten dazu viel Zeit, da die Anweisungen ja auf eine komplett andere Architektur umgesetzt werden mussten. Heute wird Bytecode mit Hilfe eines Just-In-Time-Compilers (JIT-Compiler) zur Laufzeit direkt in den Maschinencode der Zielplattform übersetzt. Das ist zwar schneller als die Interpretation und benötigt – anders als bei leistungsstarken Servern – doch einige der auf mobilen Endgeräten knappen Ressourcen. Bei der Entwicklung unter Android wird der class-Code bereits auf der Entwicklungsmaschine in dex-Code übersetzt. Dieser dex-Code basiert auf Registermaschinen und kann von der DVM auf dem Endgerät zügig interpretiert werden. Unter anderem versprach man sich von diesem Ansatz auch, keinen JIT-Compiler mehr zu benötigen, doch enthält die DVM seit Android 2.2 einen JIT-Compiler, was insbesondere bei dürftig ausgestatteten Geräten zu spürbaren Leistungssteigerungen geführt hat.

Die Sandbox

Zunächst erscheint es wieder merkwürdig, dass Android verschwenderisch mit System-Ressourcen umgeht. Jede App hat nämlich

  • einen eigenen Prozess
  • eine eigene DVM
  • einen eigenen Heap
  • ein eigenes Verzeichnis im Dateisystem
  • einen eigenen Betriebssystem-User

Dadurch, dass wir jeder App diese eigene Umgebung zugestehen, erhalten wir mehr Sicherheit. Die strikte Trennung der Prozesse bewirkt, dass verschiedene Apps nicht auf den Arbeitsspeicher anderer Apps zugreifen können. Das Verzeichnis der Anwendung ist ebenfalls gegen fremde Zugriffe geschützt. Apps dürfen nur Ressourcen anderer Apps nutzen, wenn sie dazu berechtigt sind. Berechtigungen werden explizit in einer eigenen Datei, dem Manifest der App, angefordert und bereitgestellt.

Tatsächlich können eigene DVMs sogar ohne großen Aufwand bereitgestellt werden. Es wird auf den expliziten Neustart einer DVM verzichtet. Stattdessen gibt es einen Systemprozess namens zygote, der mit Hilfe der Unix-Anweisung fork dafür sorgt, dass jede App einen eigenen DVM-Prozess hat. Außerdem sorgt zygote dafür, dass möglichst viele Ressourcen geteilt werden. Dies sorgt für einen raschen Start und eine ressourcenschonenden Betrieb der DVM. Wer mehr über die Interna von Android wissen will sei auf einen Übersichtsartikel verwiesen.

TODO Referenz auf Fokusreport


Da Android mehrere Apps gleichzeitig betreiben kann, sind Engpässe im Hauptspeicher möglich, wenn zu viele Apps gestartet werden. Die Apps werden in einem Stapel verwaltet.

Das oberste Programm ist dabei sichtbar und aktiv. Mit dem Back-Button des Android-Gerätes wird eine Anwendung vom Stack aktiviert und die vormals aktive Anwendung auf den Stack gelegt. Sollte es zu Speicherengpässen kommen, kann Android Apps, die weiter unter auf dem Stapel liegen, beenden.

Das Manifest

Wir haben bereits gesehen, dass Android-Apps eine Manifest-Datei enthalten. Das Manifest ist ein XML-Dokument, in dem verschiedene Eigenschaften der App publiziert sind. Unter anderem sind dort auch die Rechte verzeichnet und somit die Ressourcen, die die App nutzen will. Bei der Installation wird der Anwender gefragt, ob er bereit ist, der App diese Rechte einzuräumen. Im folgenden Beispiel wird deklariert, dass die App GPS-Zugriff braucht:

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

Komponenten

Wegen des Sandbox-Prinzips kann keine App die öffentlichen Klassen einer anderen App nutzen. Auf den ersten Blick schränkt dies die Wiederverwendbarkeit unseres Codes stark ein. Tatsächlich bedient sich Android einer wesentlich robusteren Software-Architektur als der einfachen Verwendung von Java-Klassen. Die Klassenstruktur einer App ist vollständig gekapselt und somit für andere Apps eine Blackbox. Jede App kann aber Komponenten publizieren, die dann von anderen Apps nutzbar sind. Wir lösen uns so von den Implementierungsdetails unserer Java-Klassen und haben insgesamt weniger Abhängigkeiten. Der Entwurf und die Implementierung einer App können sich jetzt komplett ändern, solange nur die Schnittstelle der Komponenten erhalten bleibt.

Ein erheblicher Teil dieses Buches widmet sich der Entwicklung von Komponenten. Damit sind insbesondere gemeint

  • Activities
  • Services
  • Broadcast-Receiver
  • Content-Provider

Jeder dieser Komponentenarten ist jeweils mindestens ein Kapitel gewidmet. Wir geben daher an dieser Stelle nur einen kurzen Überblick

Activities

Activity ist die Java-Klasse die zu jeder Form von GUI gehört. Die Activity enthält die Logik, die zu einer GUI gehört. Ein bekanntes Beispiel ist die Dialer-Activity aus der folgenden Abbildung.

Services

Komplexe Aufgaben werden nicht im Hauptthread einer View ausgeführt. Bei Zeitüberschreitung einer Anwenderaktion kann Android die ganze App mit einer „Application Not Responding“-Meldung wegen Zeitüberschreitung abbrechen. Potenziell zeitaufwändige Operationen wie etwa Netzwerkzugriffe werden daher von Services in Threads oder eigenen DVMs bearbeitet. Nebenläufigkeit spielt bei Android eine bedeutende Rolle.

Broadcast-Receiver

Auf dem Gerät treten Ereignisse ein, die über einen so genannten Broadcast gemeldet werden. Beispiele für Broadcasts sind

  • das Telefon klingelt
  • eine SMS trifft ein
  • die Akkuleistung lässt nach

Ausserdem können Apps auch eigene Broadcasts versenden. Mit Hilfe eines Broadcast-Receivers kann eine App Broadcasts mit Ereignissen „abonnieren“, die für die App wichtig sind.

Content-Provider

Content-Provider bieten ihren Nutzern eine SQL-ähnliche Syntax als Schnittstelle für Daten. Wie der Provider diese Schnittstelle implementiert und ob überhaupt eine Datenbank zum Einsatz kommt, ist dem Nutzer des Providers dabei nicht bekannt. Im folgenden Beispiel verschaffen wir uns Zugriff auf alle Lieder, die auf unserem Telefon gespeichert sind und die das Wort ‚Yesterday‘ in ihrem Titel tragen.

  Uri from = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
  String artist = MediaStore.Audio.AudioColumns.ARTIST;
  String stream = MediaStore.Audio.Media.DATA;
  String title = MediaStore.Audio.Media.TITLE;
  String where = title + " like '%Yesterday%'";
  Cursor songs = getContentResolver().query(from,
    new String[] { artist, stream, title },
    where, null, null);

Einen ersten Eindruck über den Aufbau eines Content-Providers liefert die folgende Abbildung


Wir sehen, dass die Komponenten einer App voneinander unabhängig sind. Wie sie ihre Schnittstellen publizieren, lernen wir noch in einem späteren Kapitel. Wir bezeichnen Komponenten auch als lose gekoppelt (loosely coupled). Obwohl sich alles lokal auf einem kleinen mobilen Gerät abspielt, hat die Architektur einer App in Android Ähnlichkeit mit einem verteilten System.

Einzelnachweis


Einrichten der Programmierumgebung

Zurück zu Googles Android


Um Programme für Android zu entwickeln, benötigt man neben dem Android  SDK auch eine entsprechende  IDE. Die Entwickler von Android selbst empfehlen für diesen Zweck die Verwendung von  Eclipse. Es gibt aber auch die von  Google entwickelte IDE mit dem Namen  Android Studio. Sie können sich entscheiden welche IDE sie benutzen, denn wir werden ihnen Tutorials zu beiden IDEs machen.

  • Unterstützte Plattformen:
  • Android Studio: Windows, Mac OSX
  • Eclipse: Windows, Mac OSX, Linux

Damit der Einstieg in die Android-Entwicklung so angenehm wie möglich ist, gibt es an dieser Stelle Anleitung dazu, wie man Eclipse, das Android Studio, Java und das Android SDK auf seinem System einrichtet.

Java installieren

1. Stellen Sie sicher, dass Sie eine aktuelle Java-Version auf Ihrem Rechner installiert haben!

  • Windows: Installieren Sie das Java JDK (Java Development Kit) in Version 5 oder 6 (6 wird empfohlen!).
  • Mac OS X: Da Java auf dieser Plattform standardmäßig installiert ist, sollten Sie die Softwareaktualisierung starten, um sicher die neueste Version zu besitzen.

Eclipse installieren

2. Installieren Sie die Eclipse IDE in Version 3.5 (Galileo), 3.6 (Helios) oder 4.2 (Juno). Eclipse „Classic“ ist für die Android-Programmierung völlig ausreichend und wird auch für alle hier erstellten Beispielprogramme genutzt. Je nach Ihren Präferenzen und Erfahrungen können Sie aber auch eine der folgenden Versionen benutzen:

  • Eclipse IDE for Java EE Developers
  • Eclipse IDE for Java Developers oder
  • Eclipse for RCP/Plug-in Developers.

3. Laden Sie das Android SDK Starter Package entsprechend Ihrem Betriebssystem herunter. Hinterlegen Sie das ZIP-File an einem sinnigen Verzeichnis und entpacken es dort.

4. Installieren Sie das ADT-Plugin für Eclipse. Öffnen Sie dazu in der Menüleiste 'Help' –> 'Install New Software' (Abbildung 1) und fügen Sie über 'Add…' ein neues Package hinzu (Abbildung 2). Als Namen verwenden Sie etwas sinniges wie „Android“ oder „ADT Plugin“ (aber das bleibt ihn überlassen). Für die Quelle geben Sie, wie in Abbildung 3, https://dl-ssl.google.com/android/eclipse/ oder http://dl-ssl.google.com/android/eclipse/ an (früher wurde die Quelle über 'https' oftmals nicht gefunden, über 'http' sollte das aber kein Problem sein).

5. Jetzt müssen Sie Eclipse zeigen, wo es das in Schritt 3. heruntergeladen SDK finden kann. Geben Sie dazu den Pfad in den Android-Einstellungen an (Abbildung 4).

  • Windows: Window –> Preferences –> Android –> Pfad hinzufügen –> Apply
  • Mac OS X: Eclipse –> Preferences –> Android –> Pfad hinzufügen –> Apply

6. Zu guter Letzt müssen Sie jetzt noch über den 'Android SDK and AVD Manager' (Abbildung 5) die Android-Versionen auswählen, für die Sie entwickeln wollen. Wir arbeiten mit Android 2.2 (alias Version 8 oder Froyo). Klicken Sie dazu in der Symbolleiste auf das Androidsymbol und gehen auf 'Available Packages'. Für den Anfang genügt es, wenn Sie folgende Packages installieren (siehe Abbildung 6):

  • SDK Platform Android 2.2, API 8
  • Documentation for Android SDK, API 8 und
  • Android SDK Tool

Grundlegende Komponenten einer Android-Anwendung

Zurück zu Googles Android


Bevor wir jedoch anfangen, eigene Android-Anwendungen zu schreiben, möchte ich zunächst einen sehr kurzen Abriss über die Bestandteile geben, welche die Grundstruktur einer jeden Android-Anwendung ausmachen. Diese hier vorgestellten und auch viele weitere Elemente werden in späteren Kapitel näher betrachtet und mit eigenen Beispielen erklärt.

Activities

Meistens besteht eine Anwendung aus einer Verknüpfung mehrerer Activities, zwischen denen navigiert werden kann. Dabei kann man eine Activity mit der Nutzeroberfläche gleichsetzen, da sie auf die Eingaben des Nutzer reagiert und gegebenenfalls Ausgaben erzeugt, sprich sie übernimmt einen Großteil der Logik einer Anwendung. Für jede neue Nutzeroberfläche muss daher auch eine neue Activity erstellt werden. Damit auf Eingaben reagiert werden kann, wird jeder Activity eine View zugeordnet, welche die Informationen über die Bildinhalte besitzt.

Views

Views sind Elemente, die den Anwendern an der Oberfläche präsentiert werden. Eine grafische Darstellung ist in Android ein Baum von Views, das heißt ein oberstes View-Element enthält wahlweise andere Views. Diese können zum Beispiel Textfelder, Buttons, Bilder und viele mehr sein. Hauptsächlich werden alle grafischen Oberflächen in XML-Layoutfiles beschrieben, in denen die oben genannten Baumstruktur zum tragen kommt. Diese Layoutfiles werden zur Laufzeit einer Activity an diese gebunden und ihr Inhalt auf dem Bildschirm dargestellt. Finden kann man die Layoutfiles in den Application Ressources.

Android-Manifest XML

Das Manifest-File ist der Dreh- und Angelpunkt einer jeden Android-Anwendung. Sämtliche Metadaten einer Anwendung werden in XML-Syntax in diesem File hinterlegt. Solche Metadaten sind beispielsweise:

  • Zugriffsrechte der Anwendung wie Zugang zum Internet, Schreiben/Lesen auf die SD-Karte, Auslesen von SMS und viele mehr
  • Registrieren aller Activities der Anwendung und wie sich diese Verhalten
  • Informationen und Restriktionen über unterstützte Android-Versionen
  • und viele mehr …

Application Ressources

Neben den Java-Klassendateien, dem Manifest oder den Viewdateien etc., kommt es nicht selten vor, dass eine Android-Anwendung Dateien wie Bild-, Audio- und Stringdateien verwendet. Diese Ressourcen werden in den Application Ressources hinterlegt, von wo aus sie an jeder Stelle einer Anwendung erreichbar sind. Dateien die hier immer zu finden sein werden, sind die oben genannten Layoutfiles.

Ein Déjà-vu – Hello World

Zurück zu Googles Android


Nun, da wir die Entwicklungsumgebung installiert und konfiguriert haben und wissen, aus welchen Basiskomponenten eine Android-Anwendung besteht, wollen wir unsere erste eigene Anwendung erstellen. Und nichts würde sich besser dafür eignen, als eine „Hello World“-Anwendung zu schreiben.


1. Zunächst müssen wir dafür in Eclipse ein neues Android-Projekt erstellen. Wie in Abbildung 1 gezeigt, geht das mit Hilfe von 'File –> New –> Project' oder über die Schaltfläche in der Symbolleiste. Indem sich öffnenden Dialog suchen wir uns unter Android das Android-Projekt aus und klicken auf 'Next'.


2. Jetzt müssen wir einige Voreinstellungen für unsere Anwendung vornehmen:

  • Der Projektname: Naja, eigentlich selbsterklärend, aber unter diesem Namen erstellen wir unser Projekt und können es unter diesem auch in unserem Verzeichnis finden.
  • Das Build-Target: Hier geben wir an, unter welcher Android-Version unsere Anwendung entwickelt wird. Dies kann später in den Projekteinstellungen geändert werden.
  • Der Anwendungsname Der Name unserer Anwendung unter dem wir die App auch auf dem Android-Gerät finden.
  • Der Package-Name: Alle Java-Klassen werden in einem gemeinsamen Package zusammengefasst.
  • Optional besteht die Möglichkeit eine 'Start'-Activity zu erstellen, die beim Programmstart als erstes aufgerufen wird. Da wir mit interaktiven Apps arbeiten wollen, empfiehlt sich die Angabe eines Namens für die Start-Activity.
  • Optional: ist auch die Angabe einer minimalen SDK Version.

Damit kann angegeben werden, welche Version das Betriebssystems ein Endgerät besitzen muss, damit es die Anwendung ausführen kann. Das ist an dieser Stelle aber noch nicht von Bedeutung.

Wie in dieser Anwendung die einzelnen Felder belegt sind, sehen wir in Abbildung 3.

Mit einem Click auf 'Finish' wird das Projekt erstellt.


3. Nachdem das Projekt erstellt wurde, kann, darf und sollte ein kurzer Blick in die Verzeichnisstruktur der Anwendung riskiert werden. Wie in der Abb. 4 zu sehen ist, kann man alle aus dem Kapitel Grundlegende Komponenten einer Android-Anwendung genannten Komponenten hier finden:

  • Activity: StartActivity.java
  • Layout-Datei: main.xml
  • Manifest: AndroidManifest.xml

Im Grunde genommen haben wir bereits jetzt eine lauffähige Anwendung vor uns liegen. Um zu zeigen, dass dem auch so ist, werden wir das Programm einmal ausführen … doch Stopp!

Um eine Android-Anwendung auszuführen, benötigen wir mindestens eins von zwei Dingen:

  • Wir haben entweder die Möglichkeit unser Programm direkt auf einem androidfähigen Gerät zu starten, das mindestens die Betriebssystemversion unterstützt, in der wir das Programm geschrieben haben,
  • Wir nutzen einen im SDK mitgelieferten als „Android Virtual Device“ (AVD) bezeichneten Emulator. Auch wenn Sie bereits ein Android-Smartphone besitzen, werden wir nun so ein AVD einrichten.


4. Dazu öffnen wir wieder den „Android SDK und AVD Manager“ (siehe Abbildung 5). Wir sollten uns bereits jetzt im Dialog für 'Virtual Devices' befinden (wenn dem nicht so ist, bitte dorthin wechseln). Mit einem Klick auf 'New' legen wir ein neues AVD an und können es konfigurieren. Momentan ist es für uns nur wichtig, dem AVD im Konfigurationsdialog einen Namen und seine Betriebssystemversion mitzuteilen. Die anderen Eigenschaften werden wir in einem späteren Kapitel behandeln. Mit einem Click auf 'Create AVD' besitzen nun unser eigenes virtuelles Entwicklungsgerät.


5. Wir haben unsere Anwendung und unser AVD. Also bringen wir den Stein ins Rollen: Mit Rechtsklick auf das Projekt –> Run As –> Android Application (siehe Abb. 7) wird das Programm gestartet. Da wir zurzeit nur ein virtuelles Device nutzen, wird die Anwendung automatisch auf diesem ausgeführt. An dieser Stelle sei gesagt, dass, egal wie schnell ihr Computer ist, das Hochfahren eines AVD immer eine gefühlte Ewigkeit dauert! (In dieser Zeit können Sie sich ruhig einen Kaffee holen :) ). Irgendwann sollte ein Fenster erscheinen, das wie in Abbildung 8 aussieht.

Sobald das Device hochgefahren ist, erscheint in den meisten Fällen zugleich auch die von uns gestartete Anwendung, wie es auch in Abb. 9 der Fall ist. Wie zu sehen ist, steht auf dem Bildschirm schon ein kleiner Satz, der sich aus dem Projektnamen und dem Namen unserer Start-Activity zusammensetzt. Jede „Start“-Activity, die wir beim Erstellen eines neuen Android-Projektes mit erstellen lassen ist so aufgebaut. Sicherlich (und auch hoffentlich), stellt sich Ihnen jetzt die Frage wie dieser kleine Satz auf den Bildschirm kommt. Da bietet es sich an, einmal den Zusammenhang und die Zusammenarbeit zwischen Activity, Layouts und Ressourcen zu betrachten.


  • StartActivity.java:
package com.jami.hw;
 
import android.app.Activity;
import android.os.Bundle;
 
public class StartActivity extends Activity {
     /** Called when the activity is first created. */
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }
}

Die Activity in diesem Beispiel macht zwar noch nicht viel her, die wichtigsten Funktionen sind dennoch zu sehen. In jeder Activity kommt die Methode onCreate vor, die jedes Mal dann aufgerufen wird, wenn eine Activity neu „erschaffen“ wird. Es gibt häufig auch die Situation, dass eine bereits bestehende Activity neu aufgerufen wird. In diesem Fall wird nicht die onCreate- sondern die onRestart- bzw. onStart-Methode genutzt. Aber zum Lifecycle einer Activity und aller damit einhergehenden Methoden kommen wir noch im Kapitel [[../_Activities|Activities]] zu sprechen. Wie gesagt wird onCreate nur beim Initialisieren der Activity genutzt. Dabei sollte hier die Zuweisung eines Layouts, sprich der UI der Activity, erfolgen. Dies wird durch die Methode setContentView erreicht, der als Parameter eine Layout-Ressource mitgegeben wird. In unserem Beispiel ist dies die Ressourcen-Datei main.xml, die sich im Ordner res/layout befindet. Da unsere Anwendung nur einen einfachen Text ausgeben soll, wird passiert in dieser Activity nichts weiter.


  • main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

<TextView  
    android:layout_width="fill_parent"
      android:layout_height="wrap_content"
    android:text="@string/hello"/>
 
</LinearLayout>

Diese XML-Datei beschreibt, wie die Oberfläche einer Activity aufgebaut ist. Als „Rahmen“ benötigt man dazu als erstes ein Grundlayout. Wir fangen mit dem einfachen linearen Layout an. Welche weiteren Typen es gibt und worin sich diese Layouts unterscheiden, wird in einem späteren Kapitel behandelt. In diese Layouts werden dann, je nachdem was man darstellen möchte, weitere View-Elemente eingefügt. In unserem Fall möchten wir einen Text darstellen, also verwenden wir hier eine View vom Typ TextView. Es gibt auch noch weitere Elemente, die zum Beispiel zur Darstellung von Buttons, Eingabefeldern oder Bildern genutzt werden. Aber auch diese werden explizit in einem eigenen Kapitel behandelt. Um den Text zu setzen, der später auf dem Bildschirm angezeigt werden soll, muss die TextView auch wissen, welchen Text er anzuzeigen hat. Dafür gibt es zwei Möglichkeiten:

  • Man kann dem Attribut android:text der TextView den anzuzeigenden Text direkt übergeben oder
  • Man kann ihm eine Referenz auf einen Eintrag in einer Ressourcen-Datei mitgeben.

In diesem Beispiel erhält die TextView eine Referenz auf den String namens 'hello', wie er in der Datei strings.xml definiert wurde. Wenn man den String direkt übergeben will, setzt man das text-Attribut in der Layout-Defintion wie folgt:

android:text="Hello World, StartActivity!"


  • strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="hello">Hello World, StartActivity!</string>
    <string name="app_name">HelloWorld</string>
</resources>

Im Grunde genommen ist die Datei strings.xml nichts weiter als eine Ansammlung von Strings, die durch diese Ressourcen-Datei überall in der Anwendung verfügbar sind.


Ich gratuliere Ihnen! Sie haben Ihr erstes Android-Programm geschrieben, auch wenn Sie an dieser Stelle nicht wirklich etwas programmiert haben … aber das werden wir schon bald ändern.


Ich (nicht der Autor) habe gerade mal (23. 8. 2023) versucht, das Beispiel mit einem aktuellen Android zum Laufen zu bekommen - es hat geklappt. Wenn man mit Eclipse arbeitet, muss man wahrscheinlich gar nichts ändern. Ich bevorzuge aber die Nutzung von Befehlen in der Konsole. Da geht es wie folgt:

Die benötigten Dateien sind:

./AndroidManifest.xml ./res/layout/main.xml ./res/values/strings.xml ./src/com/jami/hw/StartActivity.java

Außer der ersten sind diese oben abgebildet. Die erste lautet wie folgt:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.jami.hw">
    <uses-sdk android:targetSdkVersion="33" />
    <application android:label="HelloWorld">
        <activity android:name=".StartActivity"
                  android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Hier muss man evtl. die sdk-Version anpassen.

Mit folgenden Befehlen wird das ganze dann kompiliert (der Pfad zu android.jar muss ggfs. angepasst werden):

Datei R.java erzeugen:

aapt package -M AndroidManifest.xml -I ~/.android_sdk/platforms/android-33/android.jar -S res/ -v -m -J src/

Java-Dateien kompilieren:

javac -source 9 -target 9 -d obj/ -cp ~/.android_sdk/platforms/android-33/android.jar -sourcepath src/ src/com/jami/hw/*.java

Class-Dateien in dex-Format umwandeln (früher ging das mit dx, jetzt mit d8):

d8 --lib ~/.android_sdk/platforms/android-33/android.jar --output bin/ obj/com/jami/hw/*.class

In apk-Datei packen. Diese ist noch nicht signiert.

aapt package -M AndroidManifest.xml -I ~/.android_sdk/platforms/android-33/android.jar -S res/ -v -F bin/HelloWorld.unsigned.apk bin

Die Daten in der Datei müssen auf 4 Bytes aligniert werden:

zipalign -f -p -v 4 bin/HelloWorld.unsigned.apk bin/HelloWorld.aligned.apk

Dann muss das ganze signiert werden, dafür ist ein keystore notwendig, siehe unten.

apksigner sign -verbose -ks HelloWorld.keystore --out bin/HelloWorld.signed.apk bin/HelloWorld.aligned.apk

Und zuletzt wird die App auf dem Smartphone installiert. (Dafür muss sich das Smartphone im Debug-Modus befinden. Wie das geht ist für jedes Smartphone anders -> Im Netz suchen...)

adb install bin/HelloWorld.signed.apk

Das war's (wenn alle Schritte geklappt haben...)

Zum Signieren wird ein keystore benötigt. Denn kann man mit folgendem Befehl erstellen:

keytool -genkey -validity 10000 -dname "CN=AndroidDebug, O=Android, C=US" -keystore HelloWorld.keystore -storepass android -keypass android -alias androiddebugkey -keyalg RSA -keysize 2048

Beim Signieren wird man nach dem Passwort gefragt. Das lautet "android". Kann man aber natürlich auch was anderes wählen...


Androids Innenleben


Activities

Zurück zu Googles Android


Der Vielfalt der Programmiersprachen und Frameworks entsprechend gibt es viele Möglichkeiten, um graphische Benutzerschnittstellen (GUIs) zu entwickeln. Wenn wir in Java mit den Swing-Klassen arbeiten, definieren wir beispielsweise Container-Klassen, in die wir dann Steuerelemente (Widgets) wie Buttons oder Eingabefelder einfügen. Die Steuerelemente können dabei selbst Container sein, die andere Steuerelemente enthalten. Auf diese Weise ergibt sich eine hierarchische Struktur. Im .NET-Framework gibt es seit der Version 3.0 die Windows Presentation Foundation (WPF). Hier können die Steuerelemente in XML-Dokumenten hierarchisch strukturiert werden. Die eigentliche Logik, die die Interaktion mit dem Anwender steuert, wird dann in zusätzlichem Programmcode definiert. Auf diese Weise erhält man eine Trennung zwischen Präsentation und Anwendungslogik.

Activities brauchen Views

Die Android-API enthält nicht die Swing-Klassen aus dem Java-SDK. In Android-Anwendungen werden GUIs nach einem ähnlichen Konzept wie in der WPF definiert.

Die Darstellung der GUI wird in XML-Dateien durchgeführt; das hierarchische XML-Format trägt dabei der hierarchischen Struktur einer GUI besonders gut Rechnung. In einer Android Anwendung können wir auf diese GUI in Form von Objekten vom Typ View zugreifen. Diese View-Objekte reflektieren die Struktur und die Namensgebung, wie sie in der XML-Datei hinterlegt sind. Ein View haben wir in der Datei main.xml bereits im vorherigen Kapitel definiert.

Die eigentliche Anwendungslogik wird dann in Objekten vom Typ Activity hinterlegt. Eine Activity ist ein Objekt, das direkt an der Benutzerfront arbeitet:

  • Sie ist mit einer View verbunden und kann so die Buttons, Eingabefelder, Checkboxen und was es sonst noch so in der GUI gibt steuern.
  • Wenn ein Anwender sich entschließt mit der GUI zu interagieren, werden Events ausgelöst, auf die die Activity dann geeignet reagieren kann.

Grundsätzlich muss einer Activity nicht unbedingt eine View zugeordnet sein, doch verliert sie ohne View eigentlich ihre Existenzberechtigung. Die Zuordnung geschieht mit der Methode setContentView. Diese Vorgehensweise haben wir auch bereits im vorhergehenden [[../_Ein Déjà-vu – Hello World|Kapitel]] gesehen. Die Standardanwendung, die automatisch mit jedem Android-Projekt erzeugt wird, enthält eine Klasse, die von Activity erbt:

public class StartActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  }
}

Wir werden im Laufe des Kapitels noch die Bedeutung der Methode onCreate kennenlernen. Uns interessiert zunächst nur ihr Inhalt: Im wesentlichen enthält sie den Aufruf von setContentView. Damit wird der Activity die View zugewiesen, die in main.xml definiert wurde. Die View ist dabei eine Ressource ähnlich wie ein Text aus der Datei strings.xml (siehe auch [[../_Ein Déjà-vu – Hello World|Ein Déjà-vu – Hello World]]). Wir wollen hier Objekte vom Typ Activity[1] genauer unter die Lupe nehmen, die Beschreibung von Views in XML-Dateien ist Gegenstand eines eigenen Kapitels. Da GUIs so ganz ohne Views ziemlich langweilig sind, werfen wir noch einmal einen Blick auf die Standard-View aus dem vorherigen Kapitel. Dort ist eine TextView enthalten, die für die Darstellung eines einfachen Textes verantwortlich ist:

<TextView  
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:text="@string/hello"
/>

Zwar gibt es auch eine korrespondierende Java-Klasse namens TextView,[2] doch haben wir keine Möglichkeit an die Objektdarstellung der View zu kommen. Daher können wir den Text der View nicht programmgesteuert in unserer onCreate-Methode[3] anpassen. Um mit dem Objekt zu arbeiten, das zu dieser View gehört, müssen wir der View einen Namen geben:

<TextView  
  android:id="@+id/simpletv"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
/>

Ähnlich wie bei den Texten im letzten Kapitel sorgt das Android-Plugin der Eclipse-IDE dafür, dass wir jetzt in der Klasse R.id ein ganzzahliges Attribut simpletv haben, das unsere TextView eindeutig identifiziert. Zwar ist das Textfeld noch leer, doch können wir die View mit ihrer id und der Methode findViewById[4] im Universum der Objekte unserer Android-Anwendung wiederfinden:

TextView message = (TextView) findViewById(R.id.simpletv);

Die Methoden der Klasse TextView – eine Unterklasse des allgemeinen Typs View[5] – verwenden wir jetzt für die Steuerung. Den Text setzen wir wie folgt:

message.setText("Hello Android");

Die Interaktion mit dem Anwender

Zur Übung wollen wir zu unserer View noch ein Eingabefeld (Typ EditText)[6] und einen Button (Typ Button)[7] hinzufügen. Die Steuerelemente bekommen die id word bzw. upper; der Button soll programmgesteuert mit dem Text „To Upper“ beschriftet werden. Wer etwa Geduld hat und experimentierfreudig ist, kann sich etwa mit Hilfe der Dokumentation zur API an dieser kleine Aufgabe versuchen.

Wer ungeduldiger ist, fügt in main.xml vor der Definition von firsttv die folgenden Zeilen ein:

<EditText
  android:id="@+id/word"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:inputType="text"
/>
<Button
  android:id="@+id/upper"
  android:layout_height="wrap_content"
  android:layout_width="wrap_content"
/>

Der zugehörige Java-Code sieht wie folgt aus:

TextView view=(TextView)findViewById(R.id.firsttv);
EditText edit=(EditText)findViewById(R.id.word);
Button  upper = (Button)findViewById(R.id.upper);
upper.setText("To Upper");

Die App sollte im Emulator ungefähr so aussehen:

Wenn wir diese App starten, tut sich natürlich nichts, wenn wir den Button anklicken. Sein Click-Event wird zwar ausgelöst, läuft aber ins Leere. Wie bei der ereignisgesteuerten Programmierung üblich, können wir unserem Button einen Listener zuordnen. Dazu enthält der Typ View ein Interface namens OnClickListener[8] mit einer Methode onClick.[9] Dieses Interface implementieren wir mit einer anonymen inneren Klasse. Anonyme Klassen werden in Android-Anwendungen oft gebraucht und wer sie noch nicht kennt, sollte das schnell nachholen.

private TextView view;
private EditText edit;
private Button upper;

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  view = (TextView) findViewById(R.id.simpletv);
  edit = (EditText) findViewById(R.id.word);
  upper = (Button) findViewById(R.id.upper);
  upper.setText("To Upper");
  upper.setOnClickListener(new View.OnClickListener() {
    public void onClick(View v) {
      String result=edit.getText().toString().toUpperCase();
      view.setText(result);
    }
  });
}

Die Organisation des Codes hat sich etwas geändert: Die Views werden nicht mehr in lokalen Variablen, sondern in Attributen gehalten.

So können wir sie in allen Methoden unserer Klasse StartActivity nutzen. Wirklich neu sind nur die letzten Zeilen der Methode. Wir sehen, dass ein Objekt vom Typ OnClickListener anonym erzeugt und dem Button mit Hilfe seiner Methode setOnClickListener[10] zugeordnet wird.

Die Funktionalität ist einfach: Der Inhalt des Eingabefeldes wird ausgelesen und in ein Objekt vom Typ String transformiert. Dieser Text wird dann in Großbuchstaben umgewandelt und der Variablen result zugewiesen. Anschließend wird der Inhalt von result in unserer TextView dargestellt.

Das ist sicher keine spannende Anwendung, doch haben wir so bereits einige wesentliche Aspekte bei der Arbeit mit Activities kennen gelernt.

  • Die Zuordnung einer View.
  • Der Zugriff auf die Objekte, die in der View enthalten sind.
  • Die Arbeit mit Widgets, die alle durch jeweils eine View repräsentiert werden.
  • Die Verarbeitung von Anwendereingaben mit Hilfe von Listenern.

> Activities sind meldepflichtig <

Zwar werden wir die Manifest-Datei, die ja zu jeder Android-Anwendung gehört noch in einem eigenen [[../_Das Manifest|Kapitel]] kennen lernen, doch sollten wir bereits jetzt wissen, dass dort auch alle Activities unserer Anwendung verzeichnet sein müssen. Wenn wir einen Blick auf die Datei AndroidManifest.xml werfen, sehen wir, dass Eclipse das in unserem Fall bereits erledigt hat:

 <application android:icon="@drawable/icon" android:label="@string/app_name">
   <activity android:name=".StartActivity"
             android:label="@string/app_name">
     <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
     </intent-filter>
   </activity>
 </application>

Ein Teil der Angaben ist selbsterklärend, einen anderen Teil wie intent-filter lernen wir noch kennen, wenn es um [[../_Das Manifest|Manifeste]] oder [[../_Intents oder "Ich hätte gern den Zucker"|Intents]] geht.

Menüs

Der meiste Code, der in einer Activitiy anfällt, hat zwar mit Views zu tun, doch gibt es auch Eigenschaften einer Activity, die unabhängig von Views sind. So können wir in einer Activity beispielsweise Code hinterlegen, der ausgeführt wird, wenn die Akkuleistung unseres Gerätes nachlässt. Ebenso können Ereignisse abgefangen werden, die ausgelöst werden, wenn bestimmte Tasten des Gerätes gedrückt werden.

Exemplarisch behandeln wir den Fall, dass ein Anwender die Menü-Taste seines Gerätes drückt. In diesem Fall wird das so genannte Kontext-Menü angezeigt, sofern wir eines definiert haben. Dafür ist die Methode onCreateOptionsMenu[11] zuständig, die wir wie folgt überschreiben wollen:

private static final int BOLD_MENU = Menu.FIRST;
@Override
public boolean onCreateOptionsMenu(Menu menu) {
  menu.add(Menu.NONE, BOLD_MENU, Menu.NONE, "Large Font");
  return super.onCreateOptionsMenu(menu);
}

Der Name der Methode add[12] deutet ja schon sehr darauf hin, dass wir hier einen Menüeintrag hinzufügen. Er wird mit dem Text „Large Font“ beschriftet und soll später die Möglichkeit bieten, den Inhalt der TextView vergrößert darzustellen. Der erste Parameter von add kann genutzt werden, um Einträge zu gruppieren, der dritte um den Eintrag in eine Reihenfolge einzuordnen. Wer sich dafür interessiert, sollte mal einige Einträge hinzufügen und diese Parameter etwa variieren. Wir haben es uns hier leicht gemacht: Gruppierung und Reihenfolge sind uns in diesem Beispiel egal. Die id des ersten (und einzigen) Menüeintrages merken wir uns in der Konstanten BOLD_MENU.

Den zum Menü gehörenden Code definieren wir, indem wir die Methode onOptionsItemSelected[13] überschreiben. Typischerweise werden hier in einer switch/case-Anweisung die einzelnen Menüeinträge abgeklappert:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
  switch (item.getItemId()) {
    case BOLD_MENU:
      view.setTextSize(21);
    default:
  }
  return super.onOptionsItemSelected(item);
}

In unserem Fall ist das natürlich sehr einfach gehalten: Wenn ein Anwender den Menüpunkt „Large Font“ auswählt wird die Größe des Textes einfach auf 21 erhöht. Nachdem wir das einmal ausprobiert haben, schadet es sicher nicht dieses rudimentäre Menü unter Verwendung der API-Dokumentation des Typen Menu selbstständig zu erweitern.

Toast

Views, wie wir sie hier kennengelernt haben, sind mit Abstand die gebräuchlichsten Instrumente, um mit Anwendern zu interagieren. Wir lernen jetzt, dass wir unabhängig von XML-Dateien etwa kleine Textfelder anzeigen können.

Tatsächlich bedürfen diese Objekte vom Typ Toast[14] keiner Layoutdateien, sie brauchen noch nicht einmal eine Activitiy. Um ein Ergebnis zu sehen, reicht es, wenn wir den folgenden Code etwa an das Ende unserer onCreate-Methode schreiben.

Toast toast=Toast.makeText(this, "in onCreate", Toast.LENGTH_LONG);
toast.show();

Der erste Parameter, den die Factory-Methode makeText[15] braucht, ist vom Typ Context[16]; einem der Basistypen von Activity. Da aber auch Typen wie Service oder BroadcastReceiver von Context abgeleitet sind, ist der Einsatz einer Activity nicht zwingend erforderlich.

Für den dritten Parameter gibt es die beiden Möglichkeiten

  • Toast.LENGTH_LONG[17] und
  • Toast.LENGTH_SHORT[18]

für die Anzeigedauer der Textnachricht; angezeigt wird der Text aber erst nach Aufruf der Methode show.

Das Leben einer Activity

Wir benutzen den Typ Toast jetzt, um einige grundlegende Erfahrungen mit Activities in Android zu sammeln. Jede Activity hat einen der folgenden Zustände:

  • aktiv und im Vordergrund: Dies ist der Zustand, in dem wir unsere Beispiel-Activity immer erlebt haben.
  • passiv: Die Activity ist nicht mehr sichtbar und eine andere Anwendung steht im Vordergrund. Dies passiert etwa, wenn wir in unserer Anwendung mit mehreren Activities arbeiten und zwischen diesen hin und her navigieren. Um mit mehreren Activities zu arbeiten benötigen wir noch mehr Wissen über Intents, das wir aber erst in einem eigenen [[../_Intents oder "Ich hätte gern den Zucker"|Kapitel]] erwerben werden.
  • aktiv, aber teilweise überdeckt: Zu den beiden extremen Zuständen 'aktiv' und 'passiv' gibt es noch ein Zwischenstadium. Die Activity steht nicht im Vordergrund und ist noch sichtbar, sie wird aber teilweise durch eine andere Activity überdeckt.

Activities wechseln in typischen Android-Use-Cases häufig ihren Zustand. Immer, wenn eine Zustandsänderung eintritt, wird mindestens eine der so genannten Lifecycle-Methoden aufgerufen.

Die erste dieser Methoden kennen wir bereits: Sie wird bei der Erzeugung der Activity aufgerufen; die Methoden onStart und onRestart immer dann, wenn eine nicht sichtbare Activity in den Vordergrund wechselt. Wechselt die Activity in den Hintergrund wird ihre onPause-Methode ausgeführt und onStop zusätzlich dann, wenn sie nicht mehr sichtbar ist. Bei jedem Wechsel in den Vordergrund wird onResume aufgerufen. Wenn die Activitiy zerstört wird, wird onDestroy ausgelöst. Es ist sehr interessant diesen Zustandswechsel zu beobachten. Dazu überschreiben wir in unserer Klasse StartActivity jede der sieben Lifecycle-Methoden so, dass sie einen Toast anzeigt.

Für die Methode onCreate haben wir das schon gemacht. Dabei dürfen wir nicht vergessen, dass die überschriebenen Lifecycle-Methoden immer auch ihre Implementierung in der Basisklasse aufrufen muss. Diesem Umstand haben wir beim Überschreiben von onCreate bereits mit der Zeile

super.onCreate(savedInstanceState);

Rechnung getragen.

Wenn wir diese App laufen lassen, sehen wir beispielsweise auch, dass auch nach dem Betätigen der Return-Taste am Gerät, die Activity angehalten wird, und somit onPause und onStop aufgerufen werden. Die Activity wird aber nicht beendet, sondern läuft im Hintergrund weiter. Auf den Aufruf von onDestroy warten wir im Normalfall vergebens. Wenn das System allerdings eine Engpass mit seinem Speicher hat, kann es passieren, dass eine Activity über die Klinge springen muss.

Zunächst werden natürlich passive Activities zerstört und dann erst teilweise sichtbare; in keinem Fall wird aber die Activity im Vordergrund kassiert. Dieses Verfahren hält möglichst viele Activities am Leben und erspart uns so deren häufige Erzeugung und somit einige Laufzeit. Nur Activities, die zerstört werden, müssen neu erzeugt werden und durchlaufen dann wieder onCreate.

Zustand retten

Da bei der Zerstörung eines Objektes auch sein ganzer Zustand verloren geht, kann es durchaus sinnvoll sein, diesen Zustand so abzuspeichern, dass er im Fall der Vernichtung beim nächsten Aufruf von onCreate wiederhergestellt werden kann.

Dafür bietet die Android-API den Typ SharedPreferences:[26] In onPause können mit diesem Typ Werte einiger wichtiger Attribute abgelegt und in onCreate wieder ausgelesen werden. Am Namen SharedPreferences erkennt man ja bereits, dass dieser Typ eigentlich zum Speichern von Einstellungen gemacht ist.

Wir wollen uns seine Funktionsweise aber in einem anderen Zusammenhang klarmachen: Wenn ein Anwender in unserer Beispielanwendung einen Text eingibt, soll der Text erhalten bleiben und nach dem Wiederbeleben der Activity wieder angezeigt werden, selbst wenn die Activity zerstört oder das Gerät abgeschaltet wird. Wir vereinbaren dazu ein private Attribut

private SharedPreferences prefs;

Das Attribut initialisieren wir in der onCreate-Methode . Dann schauen wir nach, ob in prefs ein Text aus dem früheren Leben der Activity erhalten ist. Diesen Text weisen wir dann dem Eingabefeld edit zu:

edit = (EditText) findViewById(R.id.word);
prefs = getSharedPreferences("StartActivityPrefs", MODE_PRIVATE );
String userText = prefs.getString("userText","");
edit.setText(userText);

Die Activity ist immer dann zerstörungsgefährdet, wenn die Methode onPause aufgerufen wird. Daher ist diese Methode auch der richtige Ort um alles zu retten, was wichtig ist:

@Override
public void onPause(){    
  super.onPause();
  SharedPreferences.Editor editor = prefs.edit();
  editor.putString("userText", edit.getText().toString());
    editor.commit();
}

Dieser kurze Streifzug durch die Klasse Activity gibt uns einige Eindrücke über die Möglichkeiten dieses Typs. Selbstverständlich hat Activity noch weitere Fähigkeiten, die man ruhig einmal mit Hilfe der zugehörigen API-Dokumentation ausprobieren sollte.

Einzelnachweise

  1. http://developer.android.com/reference/android/app/Activity.html
  2. http://developer.android.com/reference/android/widget/TextView.html
  3. http://developer.android.com/reference/android/app/Activity.html#onCreate(android.os.Bundle)
  4. http://developer.android.com/reference/android/app/Activity.html#findViewById(int)
  5. http://developer.android.com/reference/android/view/View.html
  6. http://developer.android.com/reference/android/widget/EditText.html
  7. http://developer.android.com/reference/android/widget/Button.html
  8. http://developer.android.com/reference/android/view/View.OnClickListener.html
  9. http://developer.android.com/reference/android/view/View.OnClickListener.html#onClick(android.view.View)
  10. http://developer.android.com/reference/android/view/View.html#setOnClickListener(android.view.View.OnClickListener)
  11. http://developer.android.com/reference/android/app/Activity.html#onCreateOptionsMenu(android.view.Menu)
  12. http://developer.android.com/reference/android/view/Menu.html#add(int,%20int,%20int,%20java.lang.CharSequence)
  13. http://developer.android.com/reference/android/app/Activity.html#onOptionsItemSelected(android.view.MenuItem)
  14. http://developer.android.com/reference/android/widget/Toast.html
  15. http://developer.android.com/reference/android/widget/Toast.html#makeText(android.content.Context,%20java.lang.CharSequence,%20int)
  16. http://developer.android.com/reference/android/content/Context.html
  17. http://developer.android.com/reference/android/widget/Toast.html#LENGTH_LONG
  18. http://developer.android.com/reference/android/widget/Toast.html#LENGTH_SHORT
  19. http://developer.android.com/reference/android/app/Activity.html#onCreate(android.os.Bundle)
  20. http://developer.android.com/reference/android/app/Activity.html#onStart()
  21. http://developer.android.com/reference/android/app/Activity.html#onRestart()
  22. http://developer.android.com/reference/android/app/Activity.html#onResume()
  23. http://developer.android.com/reference/android/app/Activity.html#onPause()
  24. http://developer.android.com/reference/android/app/Activity.html#onStop()
  25. http://developer.android.com/reference/android/app/Activity.html#onDestroy()
  26. http://developer.android.com/reference/android/content/SharedPreferences.html

Intents oder "Ich hätte gern den Zucker"

Zurück zu Googles Android


Allgemein

Mittlerweile kennen wir Activities, ihren Aufbau und ihren Lebenszyklus. Aber mit einer Activity alleine kommt man in den meisten Anwendung eher nicht aus. Also, wie kommen wir nun von einer Activity zur nächsten? Die Antwort ist das Konzept von „Intents“. Kurz gesagt stellen Intents einen indirekten Nachrichtendienst dar, mit dem es möglich ist, Activities und Services aufzurufen und Broadcast-Receiver über Ereignisse zu informieren. Aber warum nun „indirekt“? Stellen Sie sich folgendes Szenario vor:

„Sie haben bei sich zu Hause eine Vorratskammer, in der Sie Gewürze, Mehl, Haushaltswaren usw. lagern. Nun brauchen Sie z. B. ein Paket Zucker. Was machen Sie? Sie gehen einfach in die Kammer und holen sich den Zucker. Genauso funktioniert das Android-Betriebssystem bzw. Intents nicht. Im Falle von Android wartet vor der Vorratskammer ein Butler, dem Sie sagen was Sie möchten. Dieser holt Ihnen den Zucker und übergibt Ihnen diesen.“

Was heißt das für uns? Wenn wir auf Kernelemente von Android (den Zucker) zugreifen wollen, müssen wir ein Intent-Objekt erstellen (den Butler), dieses mit Informationen befüllen (bitten den Zucker zu holen) und dieses an den Kontext weitergeben (in die Vorratskammer schicken).

Die gerade genannten Kernelemente sind

  • die bereits bekannten [[../_Activities|Activities]],
  • [[../_Services|Services]],
  • [[../_BroadcastReceiver|Broadcast-Receiver]] und
  • [[../_ContentProvider|Content-Provider]]

Der Kontext versucht, mit den im Intent enthaltenen Informationen, die gewünschte (explizite) oder eine passende (implizite) Komponente zu finden und dem Absender des Intents zukommen zu lassen. Von den drei möglichen Typen, die an einem Intent teilnehmen können, kennen wir bisher nur Activities. In den beiden folgenden Abschnitten werden wir uns anhand einiger beispielhafter Activities den Unterschied zwischen expliziten und impliziten Intents klarmachen. Damit haben wir Intents grundsätzlich verstanden. Aspekte von Intents, die für Services und Broadcast-Receiver spezifisch sind, besprechen wir in den jeweiligen Spezialkapiteln.

Die Android-Projekte, die wir im Kapitel [[../_Activities|Activities]] entwickelt haben, bestehen aus nur einer Activity. So etwas kommt auch in der Praxis vor, doch umfassen viele Apps mehrere Activities. In diesem Kapitel umfasst unser Projekt zwei Activities. Um diese Komponenten miteinander zu verbinden brauchen wir Intents.

Doch zunächst legen wir ein neues Android-Projekt an. Wir nennen es IntentDemo und lassen uns von Eclipse auch gleich eine Activity erzeugen, die wir PrimaryActivity nennen wollen.

Explizite Intents

Wir duplizieren den Java-Code, der zu PrimaryActivity gehört, und nennen die Klasse SecondaryActivity. Die erste Activity verzeichnet Eclipse für uns im Manifest, bei der zweiten müssen wir selbst Hand anlegen. Der application-Teil des Manifestes sollte also etwa so aussehen:

  <application android:icon="@drawable/icon" android:label="@string/app_name">
    <activity android:name=".PrimaryActivity"
              android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity android:name=".SecondaryActivity"
              android:label="@string/app_name">
    </activity>
  </application>

Da es in diesem Kapitel wirklich nur um die Wirkungsweise von Intents geht, brauchen wir kein kunstvolles Layout. Durch umbenennen und kopieren des Standard-Layouts verschaffen wir uns zwei Dateien primary.xml und secondary.xml. Die zweite lassen wir wie sie ist, die erste ergänzen wir noch um einen Button:

  <Button android:id="@+id/next"
          android:layout_height="wrap_content"
          android:layout_width="wrap_content"
          android:text="Next"/>

In die onCreate-Methoden der beiden Activity-Klassen fügen wir noch – so wie wir es [[../_Activities|gelernt]] haben – Code ein, um die zugehörige Views zu laden. Den Listener für den Button definieren wir zwar, lassen ihn aber zunächst wirkungslos:

PrimaryActivity.onCreate

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.primary);
    Button next = (Button) findViewById(R.id.next);
    next.setOnClickListener(new View.OnClickListener() {
      public void onClick(View v) {
      }
    });
  }

SecondaryActivity.onCreate

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.secondary);
  }


Die beiden sehr einfachen Activities sehen wie folgt aus:


Beim Klick auf den mit „Next“ beschrifteten Button soll PrimaryActivity in den Hintergrund und SecondaryActivity in den Vordergrund wechseln. Und genau dafür brauchen wir Intents.

Wir definieren ein Objekt vom Typ Intent[1] und teilen dem Kontext mit, dass SecondaryActivity den Vordergrund übernehmen soll.

Den folgenden Code setzen wir also an das Ende von PrimaryActivity.onCreate:

  final Intent intent=new Intent(this, de.wikibooks.android.SecondaryActivity.class);
    next.setOnClickListener(new View.OnClickListener() {
      public void onClick(View v) {
        startActivity(intent);
 }
  });

Wir beobachten, dass wir beim Erzeugen des Intents den Packagenamen und den kompletten Klassennamen der Ziel-Activity mitgegeben. Die Methode startActivity[2] erbt die Klasse Activity von Context. Sie macht das, was ihr Name verspricht und bringt uns zur SecondaryActivity.

Wenn wir die App starten und „Next“ auslösen, wird anstandslos zur SecondaryActivity gewechselt. Diese Art von Intent nennt sich explizit, weil wir unserem Butler sehr präzise gesagt haben, was wir brauchen. Also nicht nur einfach „Zucker“, sondern „Alnatura Rohrohrzucker“.

Diese Vorgehensweise findet man auch in anderen Java-Frameworks: es gibt Methoden, denen man den Namen einer Klasse voll qualifiziert übergibt und die sich dann um die Objekterzeugung kümmern. Und genau diese enge Koppelung von Klassen hat sich bei Änderungen als unflexibel erwiesen. Nachteile, die auf der Hand liegen, sind:

  • Wenn wir in eine andere als die SecondaryActivity verzweigen wollen, müssen wir den Java-Code wieder anfassen.
  • Wir können nur Activities aus unserer eigenen Anwendung ansteuern.
  • Die Activities (und natürlich die Services und Broadcast-Receiver) unserer Anwendung können nicht von anderen Anwendungen genutzt werden.

Dass es auch anders geht, sehen wir im folgenden Abschnitt.

Implizite Intents nutzen

Bei impliziten Intents werden Ross und Reiter nicht genannt, sondern nur beschrieben. Wenn wir in der Klasse PrimaryActivity die folgende Intent-Definition

  final Intent intent=new Intent(this, de.wikibooks.android.SecondaryActivity.class);

austauschen durch

  final Intent intent=new Intent(Intent.ACTION_DIAL);

dann landen wir, wenn wir den Next-Button betätigen, in der Dial-Activity des Android-Systems.


Wir kennen nicht den Namen der Klasse, der zu der im Intent hinterlegten Activity gehört. Das brauchen wir auch nicht, da hinterlegt ist, dass sie auch über die sogenannte Action abgerufen werden kann.

Der Vorteil sollte klar sein: Wenn die Dialer-Activity durch einen anderen Dialer ausgetauscht werden soll, muss einfach nur die Zuordnung der Aktion zur Activity geändert werden. Wir werden noch sehen, dass dazu einfache Eingriffe in die Manifest Datei reichen. Es ist also nur eine einzige Änderung nötig, um alle ACTION_DIAL-Intents[3] auf den neuen Dialer umzulenken.

Hinsichtlich der Zuordnung gelten die folgenden Regeln:

  • Jede Action kann keiner oder mehreren Activities zugeordnet sein
  • Jede Activity kann mehreren Aktionen zugeordnet sein.

Die Aktionen sind dabei einfache Texte, von denen sehr viele – wie auch ACTION_DIAL – bereits als Konstante im Typ Intent definiert wurden. Wenn es zu einer Aktion keine passende Activity gibt, meldet das System einen Fehler. Das passiert etwa, wenn wir die Intent-Definition

  final Intent intent=new Intent(Intent.ACTION_DIAL);

durch

  final Intent intent=new Intent("ACTION_FEHLER");

ersetzen: Es gibt keine Activities mit der Aktion ACTION_FEHLER. Stehen mehrere Activities zur Auswahl überlässt Android dem Anwender die Auswahl. So gibt es beispielsweise zur Action ACTION_VIEW[4] viele Activities.

Wenn wir unseren Intent wie folgt definieren

  final Intent intent=new Intent(Intent.ACTION_VIEW);

bekommen wir ein Angebot an Activities:


Je nach Anwendung kann es wichtig sein, vor dem Start des Intents zu wissen, ob das Ziel wirklich eindeutig beschrieben ist. Dazu können wir aber den Kontext befragen:

  boolean isUnique(intent){
    return 1==getPackageManager().queryIntentActivities(intent, PackageManager.GET_INTENT_FILTERS);
  }

Wie schon gesagt, kann es zu einer Action mehrere Activities geben. Um hier spezifischer zu werden, können wir bei der Definition der Intents noch eine URI übergeben. Beispiel für URIs sind die bekannten URLs wie

http://de.wikibooks.org/

aber auch weniger gängige Ausprägungen wie

tel:(+49) 7723 9200

  • An dem Teil der URI, der vor dem Doppelpunkt steht, kann der Kontext auch erkennen, welche der ACTION_VIEW-Activities benötigt wird.
  • Der Teil, der dem Doppelpunkt folgt, kann dann als Daten verwendet werden.

Beim Start des Intents

  final Intent intent=new Intent(Intent.ACTION_VIEW, Uri.parse("http://de.wikibooks.org/"));

wird also die Web-Browser-Activity angezeigt und die Seite „de.wikibooks.de“ geladen. Dagegen wird mit

  final Intent intent=new Intent(Intent.ACTION_VIEW, Uri.parse("tel:(+49) 7723 9200"));

die Dialer-Activity gestartet und gleich die Nummer „(+49) 7723 920 0“ gewählt. Da es sehr viele Aktionen gibt, können sie nicht alle aufgezählt werden. Mehr Informationen und Beispiele gibt es in der API-Dokumentation zum Typ Intent oder auch auf der Seite www.openintents.org.


Neben der Angabe einer Aktion können wir mit Hilfe der Methode addCategory[5] auch eine oder mehrere Kategorien angeben, denen die Activity zugeordnet sind. Auf diese Weise können wir die Anzahl der in Frage kommenden Ziele weiter einschränken und auch an unsere Anforderungen anpassen.

  • So kennzeichnet Intent.CATEGORY_LAUNCHER[6] eine Activity, die auch die Start-Activity einer App ist;
  • Die Kategorie Intent.CATEGORY_TAB[7] identifiziert dagegen solche Activities, die als Teil einer Activity einen eigenen Reiter bekommen kann.

Actions und Intent-Filter

Natürlich wollen wir implizite Intents nicht nur benutzen, sondern auch unsere eigenen Komponenten über implizite Intents nutzbar machen. Um etwa unsere SecondaryActivity auch impliziten Intents zur Verfügung zu stellen, reicht eine einzige Änderung im Manifest:

  <activity android:name=".SecondaryActivity"
            android:label="@string/app_name">
    <intent-filter>
      <action android:name="de.wikibooks.android.SECONDARY" />
      <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
  </activity>

Neu ist hier das Tag <intent-filter>. Hier wird die Beschreibung der Activity durchgeführt. Wir kennzeichnen sie hier mit unserer eigenen Aktion „de.wikibooks.android.SECONDARY“, es wäre aber kein Problem gewesen, dafür eine der Aktionen aus der Klasse Intent zu verwenden.

Zu jedem Intent-Filter muss es mindestens eine Aktion und mindestens eine Kategorie geben. Mehrere sind aber durchaus möglich. Als Kategorie wird in den meisten Fällen bedeutungsfrei Intent.CATEGORY_DEFAULT[8] gewählt. Bereits jetzt können alle Android-Anwendungen (und nicht nur unsere eigene) mit impliziten Intents der Form

  final Intent intent=new Intent("de.wikibooks.android.SECONDARY")

zur SecondaryActivity gelangen.

Wir überzeugen uns jetzt davon, dass unsere Activities auch „von außen“ erreichbar sind. Im Kapitel [[../_Activities|Activities]] haben wir uns mit einem Android-Standard-Projekt beschäftigt und der (von Eclipse erzeugten) Activity den Namen StartActivity gegeben. In diesem Projekt gibt es auch ein Manifest. Dort wurde StartActivity bereits automatisch verzeichnet. Erst jetzt verstehen wir den Manifest-Eintrag

  <activity android:name=".StartActivity"
            android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>

vollständig: Die Activity hat die Kategorie CATEGORY_LAUNCHER[9] und ist über die Action ACTION_MAIN[10] implizit erreichbar.

Implizite Intents gehören zu den gewöhnungsbedürftigsten Komponenten in der Android-Programmierung. Wenn man ihre Vorteile aber einmal verstanden hat, mag man sie nicht mehr missen.

Datenübergabe mit Intents

Dass wir jetzt zwischen einzelnen Activities wechseln können, ist schon ein Fortschritt. Noch mehr Möglichkeiten erhalten wir, wenn wir es schaffen, dass der Ziel-Activity Daten aus der Quell-Activity übergeben werden. Wir können uns vorstellen, dass eine Datenübergabe auch für andere Komponenten wie Services oder Broadcast-Receiver interessant sein kann.

Die Datenübergabe erarbeiten wir uns wieder an einer einfachen Anwendung mit zwei Activities, die wir bei Bedarf modifizieren und auch für andere Komponenten ganz ähnlich verwenden können. Bei der App aus dem [[../_Activities|Activities]] gibt der Anwender einen Text ein, der dann in Großbuchstaben umgewandelt wird. Diese Anwendung verteilen wir jetzt auf PrimaryActivity und SecondaryActivity. Die erste übernimmt die Eingabe und sendet den Text per Klick auf einen Button an die zweite Activity. In die Layout-Datei primary.xml fügen wir noch ein Eingabefeld ein:

  <?xml version="1.0" encoding="utf-8"?>
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:orientation="vertical"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">
    <EditText android:id="@+id/word"
               android:layout_width="fill_parent"
               android:layout_height="wrap_content"
               android:inputType="text"/>
    <Button android:id="@+id/next"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="Next"/>
  </LinearLayout>

Die Activities sehen jetzt wie folgt aus:

Wenn der Anwender den Button anklickt, wird der eingegebene Text abgelegt:

public void onClick(View v) {
secondary.putExtra("word", word.getText().toString());
startActivity(secondary);
}

Jeder Intent verfügt über ein Verzeichnis von Key-Value-Paaren, die auch mit an die Zielkomponente versendet werden. Dieses Verzeichnis hat den Typ Bundle[11] und wird auch als Extras bezeichnet.

Im Beispiel fügen wir den Extras mit der Methode putExtra[12] das Paar hinzu, dass aus dem Schlüssel "word", sowie dem Text besteht, den der Anwender eingegeben hat.

Außer diesem einen Paar könnten wir Bedarf noch mehr Informationen mitgeben. Diese werden dann in SecondaryActivity mit Hilfe der Methode getStringExtra[13] aus der Klasse Intent eingesammelt:

  String word=getIntent().getStringExtra("word");
  TextView view = (TextView) findViewById(R.id.tv);
  view.setText(word.toUpperCase());

wobei tv, die id der TextView ist, die wir in secondary.xml vereinbart haben. Den zu Intent, zu der die SecondaryActivity gehört, finden wir mit getIntent. Aus den Extras des Intents können wir dann unter Angabe des Schlüssels "word" auch wieder den zugehörigen Wert auslesen.

Pending-Intent

Ein PendingIntent[14] Objekt ähnelt einem Intent mit einer Ausnahme. Das Objekt, das durch ein Pending Intent aktiviert wird, läuft unter den Rechten des Erzeugers der Pending Intent. Das ist sogar dann noch der Fall, wenn zum Beispiel die erzeugende Activity oder eben der erzeugende BroadcastReceiver nicht mehr existiert. Das ist offenbar ein interessantes Mittel für einen Receiver. Es gibt eine Reihe von statischer Factory-Methoden zur Erzeugung von PendingIntent-Objekten. Bsp.: getActivity(), getBroadcast(), getService(). Dieser Methoden erzeugen ein Pending Intent Objekt das bei dessen Nutzung jeweils eine Activity, einen Broadcast oder einen Service erzeugt – mit den Rechten des Erzeugers des Pending Intents! Man sollte sehr vorsichtig damit umgehen.

Einzelnachweise

  1. http://developer.android.com/reference/android/content/Intent.html
  2. http://developer.android.com/reference/android/content/Context.html#startActivity(android.content.Intent)
  3. http://developer.android.com/reference/android/content/Intent.html#ACTION_DIAL
  4. http://developer.android.com/reference/android/content/Intent.html#ACTION_VIEW
  5. http://developer.android.com/reference/android/content/Intent.html#addCategory(java.lang.String)
  6. http://developer.android.com/reference/android/content/Intent.html#CATEGORY_LAUNCHER
  7. http://developer.android.com/reference/android/content/Intent.html#CATEGORY_TAB
  8. http://developer.android.com/reference/android/content/Intent.html#CATEGORY_DEFAULT
  9. http://developer.android.com/reference/android/content/Intent.html#CATEGORY_LAUNCHER
  10. http://developer.android.com/reference/android/content/Intent.html#ACTION_MAIN
  11. http://developer.android.com/reference/android/os/Bundle.html
  12. http://developer.android.com/reference/android/content/Intent.html#putExtra(java.lang.String,%20android.os.Bundle)
  13. http://developer.android.com/reference/android/content/Intent.html#getStringExtra(java.lang.String)
  14. http://developer.android.com/reference/android/app/PendingIntent. Stand 14. November 2010

Das Manifest

Zurück zu Googles Android


Eine Pflicht für jede Android-Anwendung ist das AndroidManifest.xml-File. Es enthält die wichtigsten Informationen über eine Anwendung, welche es dem Android-System zur Verfügung stellt. Diese Informationen müssen dem System vor dem Start der Anwendung bekannt sein, damit es diese ausführen kann.

Folgendes wird im Manifest festgehalten:

  • Als eindeutige ID für die Anwendung wird das Java-Package der Anwendung genannt
  • Eine Auflistung aller Komponenten einer Anwendung. Dazu gehören Activities, Services, Broadcast-Receiver oder Content-Provider. Für jede dieser Komponenten werden der Klassenname und die jeweiligen Fähigkeiten angegeben.
  • Alle Berechtigungen (Permissions), die benötigt werden, um bestimmte Teile der API in der Anwendung zu nutzen, z.B. Standort, Dateien, und Sensoren. Die Funktionsweise der Berechtigungen änderte sich im Verlauf der Android-Versionen ständig.

Struktur des Manifests

<?xml version="1.0" encoding="utf-8"?>

<manifest>

    <uses-permission />
    <permission />
    <permission-tree />
    <permission-group />
    <instrumentation />
    <uses-sdk />
    <uses-configuration />  
    <uses-feature />  
    <supports-screens />  
    <compatible-screens />  
    <supports-gl-texture />  

    <application>

        <activity>
            <intent-filter>
                <action />
                <category />
                <data />
            </intent-filter>
            <meta-data />
        </activity>

        <activity-alias>
            <intent-filter> . . . </intent-filter>
            <meta-data />
        </activity-alias>

        <service>
            <intent-filter> . . . </intent-filter>
            <meta-data/>
        </service>

        <receiver>
            <intent-filter> . . . </intent-filter>
            <meta-data />
        </receiver>

        <provider>
            <grant-uri-permission />
            <meta-data />
        </provider>

        <uses-library />

    </application>

</manifest>

Hier eine Liste aller erlaubten Tags im Manifest:

Einzelnachweise

  1. http://developer.android.com/guide/topics/manifest/action-element.html. Stand 17. März 2011
  2. http://developer.android.com/guide/topics/manifest/activity-element.html. Stand 17. März 2011
  3. http://developer.android.com/guide/topics/manifest/activity-alias-element.html. Stand 17. März 2011
  4. http://developer.android.com/guide/topics/manifest/application-element.html. Stand 17. März 2011
  5. http://developer.android.com/guide/topics/manifest/category-element.html. Stand 17. März 2011
  6. http://developer.android.com/guide/topics/manifest/data-element.html. Stand 17. März 2011
  7. http://developer.android.com/guide/topics/manifest/grant-uri-permission-element.html. Stand 17. März 2011
  8. http://developer.android.com/guide/topics/manifest/instrumentation-element.html. Stand 17. März 2011
  9. http://developer.android.com/guide/topics/manifest/intent-filter-element.html. Stand 17. März 2011
  10. http://developer.android.com/guide/topics/manifest/manifest-element.html. Stand 17. März 2011
  11. http://developer.android.com/guide/topics/manifest/meta-data-element.html. Stand 17. März 2011
  12. http://developer.android.com/guide/topics/manifest/permission-element.html. Stand 17. März 2011
  13. http://developer.android.com/guide/topics/manifest/permission-group-element.html. Stand 17. März 2011
  14. http://developer.android.com/guide/topics/manifest/permission-tree-element.html. Stand 17. März 2011
  15. http://developer.android.com/guide/topics/manifest/provider-element.html. Stand 17. März 2011
  16. http://developer.android.com/guide/topics/manifest/receiver-element.html. Stand 17. März 2011
  17. http://developer.android.com/guide/topics/manifest/service-element.html. Stand 17. März 2011
  18. http://developer.android.com/guide/topics/manifest/supports-screens-element.html. Stand 17. März 2011
  19. http://developer.android.com/guide/topics/manifest/uses-configuration-element.html. Stand 17. März 2011
  20. http://developer.android.com/guide/topics/manifest/uses-feature-element.html. Stand 17. März 2011
  21. http://developer.android.com/guide/topics/manifest/uses-library-element.html. Stand 17. März 2011
  22. http://developer.android.com/guide/topics/manifest/uses-permission-element.html. Stand 17. März 2011
  23. http://developer.android.com/guide/topics/manifest/uses-sdk-element.html. Stand 17. März 2011

Activities, Tasks und Launch Modes

Zurück zu Googles Android


Taskstacks

Die meisten Android-Anwendungen bestehen aus einer Abfolge von Activities. Startet ein Nutzer eine Android-Anwendung, so wird intern ein [[../_Intents oder "Ich hätte gern den Zucker"|Intent]] zum Starten (Launch) einer neuen Anwendung erzeugt. Im Manifest-File des Programms ist wiederum beschrieben, welche Activity bei einem solche „Launch-Intent“ zu starten ist. Diese „Root Activity“ wird im Manifest-File durch einen entsprechenden Intent-Filter definiert, zum Beispiel so:

...
<intent-filter> 
	<action android:name="android.intent.action.MAIN" /> 
	<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
...


Wie wir bereits wissen, können Activities andere Activities aufrufen, was mittels eines Intent-Objektes erfolgt. Daraus ergibt sich eine Abfolge von Activities. Diese Abfolge erscheint den Anwendern dann als Android-Anwendung. Um in dieser Anwendung von einer laufenden Activity zu der Vorherigen zurückzukehren, kann der BACK-Button genutzt werden. Es ist dem Anwender aber auch möglich eine weitere Anwendung zu starten. Dazu wird die aktuell laufende Anwendung in den Hintergrund versetzt und die neue Anwendung gestartet – nach den eben beschriebenen Abläufen. In der Regel erzeugt Android beim Start einer neuen Anwendung einen neuen Task. Dieser enthält einen Stack, in dem die Activities gespeichert und abgelegt werden.

1. Darstellung von Anwendungsstack im Android-Betriebssystem
1. Darstellung von Anwendungsstack im Android-Betriebssystem

Die Abbildung skizziert zwei Tasks. Der erste Task enthält zwei Activities – A1 und A2 –, da A2 oben auf dem Stack liegt, ist sie momentan aktiv und für den Anwender sichtbar. Nach Drücken des BACK-Buttons würde A2 vom Stack heruntergenommen („Pullen“) und A1 aktiv werden (Dadurch wird A2 gelöscht). Wechselt der Anwender in diesem Zustand zur Anwendung 2 (Task 2), so würde die Activity A5 sichtbar werden.

Das Standardverhalten von Android ist damit dieses: Wird eine neue Anwendung erzeugt, so wird ein neuer Task angelegt und eine neue Instanz der Root-Activity erzeugt. Diese erzeugt einen View und kann genutzt werden. Ruft die Root-Activity eine andere auf, wird wiederum eine neue Instanz dieser neuen Activity erzeugt und oben auf dem Task Stack gelegt („Pushen“). Sie interagiert nun mit dem Anwender – die rufende Activity ist nun im Hintergrund und passiv.

An dieser Stelle spricht man von der Affinität einer Activity. Im Standardfall hat eine Activity die Affinität bei dem Task zu bleiben, von dem aus sie gestartet wurde. Diese Verhalten kann für die gesamte Anwendung geändert werden, indem im Manifest-File die Affinität verändert wird, siehe auch android:taskAffinity.[1] Die Affinität kann aber auch für jede Activity einzeln verändert werden. So kann es in einigen Fällen sinnvoll sein, dass eine Activity nur einmal existiert (Singleton), sodass bei jedem weiteren Aufruf dieser Activity keine neue Instanz erzeugt, sondern immer die bereits vorhandene Instanz genutzt wird.

Das Verhalten beim Start einer neuen Activity kann an zwei Stellen gesteuert werden. Zum einen kann der aufrufende Content (meist eine Activity) mittels Flags im Intent den Prozess beeinflussen, siehe dazu.[2]

Die gerufene Activity kann ebenfalls den Erzeugungsprozess beeinflussen. Das erfolgt durch Deklaration einer Activity im Manifest-File.[3] Das Prinzip soll im Folgenden erläutert werden.

Intent-Flags zum Starten eines neuen Tasks

  • FLAG_ACTIVITY_NEW_TASK:[4] Hierdurch wird (in der Regel) ein neuer Task erzeugt und die neuerzeugte Activity ist das erste Element im Stack des neuen Tasks. Für den Anwender erscheint es, als ob eine neue Anwendung erzeugt wurde.

Aber nicht immer wird dadurch ein neuer Task erzeugt. Falls die Activity bereits einmal in einem neuen Task erzeugt wurde, so wird auf die erneute Erzeugung eines Tasks verzichtet. Es wird stattdessen der bereits existierende Task in den Vordergrund gebracht. Aber auch dieses Verhalten kann beeinflusst werden:

  • FLAG_ACTIVITY_MULTIPLE_TASK:[5] Durch dieses Flag ist es möglich, dass immer ein neuer Task erzeugt wird.

Activity Attribute

In der Anwendung kann die Affinität der Activities generell verändert werden. Das kann aber auch in einer einzelnen Activity erfolgen. Es wird das gleiche Attribut genutzt: android:taskAffinity allerdings in der Deklaration der Activity.[6]

Das Attribute android:allowTaskReparenting[7] definiert, dass eine Activity nach wiederholtem Aufruf den Tasks wechseln kann. Wird also eine Activity A1 in einem Task T1 erzeugt, so liegt sie im Stack von T1, anfangs oben, aber sie kann nach unten wandern. Wird die gleiche Activity nun in einem Task T2 erzeugt, so wechselt sie den Task T2.

Activity Launch Modes

Android erlaubt weitere Deklarationen bezüglich des Verhaltens beim Start einer Activity. Dazu dient das Attribut android:launchMode.[8] Es sind folgende Parameter erlaubt:

  • standard (Defaultwert), singleTop, singleTask, singleInstance

Singletons

Die Parameter singleInstance und singleTask definieren, dass die Activity immer in der Wurzel (dem Boden) des Tasks-Stacks stehen. Das heißt, wenn eine solche Activity erzeugt wird, wird in jedem Fall ein neuer Task angelegt und die Activity ist die erste, die in diesen Task eingeordnet wird.

Die beiden Modi unterscheiden sich aber darin, was mit folgenden Activities passiert. Die Activities können wie alle anderen auch folgende Activities aufrufen. Es besteht kein Unterschied. Der Unterschied liegt aber darin, welchen Task die neue Activities zugeordnet werden:

Die folgenden Activities bleiben im gleichen Task bei der Nutzung von singleTask. Es wird in jedem Fall ein weiterer neuer Task erzeugt, wenn singleInstance genutzt wurde. Es hat den gleichen Effekt, als wenn das Flag FLAG_ACTIVITY_NEW_TASK im Intent genutzt würde.

SingleInstance erzwingt demnach, dass ein Task exakt eine einzige Activity enthält.

SingleTask erzwingt, dass die Activity in jedem Fall am Boden des Task-Stacks liegt, darüber können aber weitere Activities liegen.

Standard

Der Standardfall wurde bereits beschrieben. Bei jedem Aufruf wird eine neue Instanz der gewünschten Activity erzeugt und oben auf den Stack des rufenden Tasks gelegt.

SingleTop

Dieser Parameter ähnelt dem Standardverhalten, mit einem Unterschied: Wird in einem Task ein Intent erzeugt, dass eine Activity verlangt, von der aber bereits eine Instanz oben auf dem Stack liegt, so wird keine weitere Instanz angelegt. Liegt die Instanz aber weiter unten im Stack, so wird eine neue Instanz angelegt und oben auf den Stack gelegt. Beispiel. In obigen Abbildung seien A1 und A2 als singleTop deklariert. Wird in Task 1 wiederholt eine A2 verlangt, so wird keine neue Instanz angelegt, sondern die Instanz von A2 bleibt oben auf dem Stack liegen. A2 wird über den Ruf der Methode onNewIntent() über diesen Vorgang informiert. Wird im Gegensatz dazu A1 gewünscht, so wird eine neue Instanz erzeugt und oben auf den Stack gelegt. Die Reihenfolge wäre dann: A1, A2, A1.

Einzelnachweise

  1. http://developer.android.com/guide/topics/manifest/application-element.html#aff. Stand 22. Oktober 2010
  2. http://developer.android.com/guide/topics/fundamentals.html#acttask. Stand 22. Oktober 2010
  3. http://developer.android.com/guide/topics/manifest/activity-element.html. Stand 22. Oktober 2010
  4. http://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK. Stand 22. Oktober 2010
  5. http://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_MULTIPLE_TASK. Stand 22. Oktober 2010
  6. http://developer.android.com/guide/topics/manifest/activity-element.html#aff. Stand 22. Oktober 2010
  7. http://developer.android.com/guide/topics/manifest/activity-element.html#reparent. Stand 22. Oktober 2010
  8. http://developer.android.com/guide/topics/manifest/activity-element.html#lmode. Stand 22. Oktober 2010

Services

Zurück zu Googles Android


Manchmal ist es sinnvoll, Prozesse auf einem Gerät dauerhaft laufen zu lassen und unterschiedlichen Anwendungen einen Zugriff darauf zu erlauben. Solche Prozesse mögen Anwendungen sein, die regelmäßig Daten von einer externen Quelle beziehen und lokal auf dem Android-Gerät speichern. Es mögen lang laufende Berechnungen sein oder Dienste, die die Werte der Sensoren des Handy regelmäßig auslesen und verarbeiten. Es können aber auch Dienste sein, die keine Werte für Anwendungen berechnen und danach ihren Dienst einstellen.

In Android existiert das Konzept der Services. Services laufen parallel zu Activities und können von unterschiedlichen Aktivitäten genutzt werden. Mit Services wird mittels RPC kommuniziert. RPC steht für Remote Procedure Call und wird Ihnen gegebenenfalls bereits in der Variante RMI begegnet sein.

Das Konzept der RPC ist schon recht alt. Es geht auf einen Artikel von Nelson und Birrell aus dem Jahre 1984 zurück; zu einigen wenigen Details der Geschichte siehe auch T. Schwotzer.[1] RPC ähneln lokalen Prozeduraufrufen. Wird innerhalb eines Prozesses eine Prozedur/Funktion/Methode aufgerufen, so werden die Eingabewerte auf den Stack des Prozesses (konkret des Threads, wenn es mehrere gibt) gelegt. Die aufgerufene Funktion entnimmt am Anfang die Parameter, danach wird die Prozedur ausgeführt. Die Rückgabewerte werden wiederum auf den Stack gelegt und die Funktion wird beendet. Die rufende Funktion entnimmt nun die Rückgabewerte und arbeitet weiter.

Sollen Prozeduren über Prozessgrenzen aufgerufen werden, so müssen die Eingabewerte von rufenden Prozess zum aufgerufenen Prozess übertragen werden. Umgekehrt müssen die Ergebnisse vom gerufenen Prozess zum Aufrufer zurückgeschickt werden. Funktionen, die diese Parameter in ein Datenpaket packen, versenden und entpacken können anhand einer Interfacebeschreibung generiert werden. überlegen Sie einmal, wie Sie das machen würden!

In Android dient dazu eine Interfacebeschreibung und das Tool AIDL.[2] Lesen Sie dort Details zur Nutzung von AIDL und der Interfacebeschreibung nach. Hier wird nur das grobe Prinzip erklärt.

IDL

IDL steht für Interface Definition Language. Es gibt eine Fülle von IDL-Sprachen. Viele sind an Programmiersprachen angelehnt. Das erscheint auch logisch. Hier ist ein Beispiel.

interface Test {
	int increment(int i);
}

Eine solche Beschreibung muss in Android in einer Datei Test.aidl gespeichert werden. Das Tool aidl erzeugt daraus eine einzige Datei: Test.java. Diese Datei enthält Javacode und konkret Folgendes:

  • die Benutzeroberfläche „Test“. Diese enthält
  • Stub
  • Proxy

Wenn Sie Eclipse nutzen, so wird aidl automatisch gestartet, sobald Sie irgendeine Änderung in der aidl-Datei durchführen. Sollten Sie also das Beispielprogramm importieren, so müssen Sie einmal eine Leerzeile einfügen oder Ähnliches und aidl generiert ein (neues) Java-File.

Server

Es soll zunächst die Serverseite eines Services diskutiert werden. Auf der Serverseite wird der Dienst erbracht. Dazu sind folgende Dinge notwendig:

  • Es muss eine Implementierung existieren.
  • Es muss Code existieren, der die ankommenden Daten deserialisiert und der die Rückgabewerte serialisiert und zurücksendet.
  • Es muss ein Prozess laufen, der auf Dienstaufrufe reagiert und sie an die Implementierung weiterreicht.

Die zweiten Aufgabe wird durch einen Stub bereitgestellt, die durch den aidl-Compiler erzeugt wird. Die Implementierung wird in das Gesamtsystem integriert, indem vom Stub abgeleitet wird und die Implementierung hinzugefügt wird. In der Beispielanwendung sieht das wie folgt aus:

public class TestImplementation extends Test.Stub { 
	public int increment(int i) throws RemoteException {
		return i+1;
	}
}

Die Implementierung ist offenbar denkbar simpel. Der Parameter wird entgegengenommen und das Inkrement wird zurückgegeben. Die Serialisierung und Deserialisierung bleibt den implementieren vollständig verborgen. Die einzige Ausnahme bildet die RemoteException, die innerhalb der Methode erzeugt und geworfen werden kann. Damit kann angezeigt werden, dass ein Fehler entstanden ist, der sich aus der Verteilung ergibt. Von einem Server wird erwartet, dass er auf Anfragen von Clients wartet und diese bearbeitet. Dazu ist ein Prozess nötig. In Android dient dazu die Klasse „Service“. Um einen eigenen Service zu definieren, wird von dieser Klasse abgeleitet. Beispiel:

public class SomeService extends Service { 
	private TestImplementation impl;
	public void onCreate() { 
		super.onCreate();
		impl = new TestImplementation(); 
	}	
	public IBinder onBind(Intent intent) { return impl; }
}

Die Implementierung stellt lediglich ein Beispiel dar, man kann den Service auch anders implementieren. Man muss allerdings auf zwei Ereignisse reagieren: Erzeugung und Binden.

Ein Service wird einmal erzeugt. Das erfolgt, wenn der erste Aufruf eines Clients erfolgt. Der Client versucht, sich in dem Moment an den Service zu binden. Binden bedeutet, dass eine Verbindung zwischen Client und Server aufgebaut wird. Nach erfolgreichem Binden können RPCs aufgerufen werden. Das Binden erfolgt immer, wenn ein Client eine neue Verbindung aufbauen will.

Diese Implementierung ist sehr einfach. Sobald ein Service erzeugt werden soll (onCreate()), wird ein Objekt der Implementierung erzeugt. Diese Serviceimplementierung merkt sich dieses eine Objekt. Bei jedem Bindeversuch wird eine Referenz auf dieses Objekt geliefert, siehe onBind(). Es ist zu erkennen, dass die Implementierung offenbar auch IBinder implementiert. Auch dieser Code ist generiert: Der Stub implementiert IBinder und damit erbt die Implementierung (TestImplementation) diese Fähigkeit.

Der Service muss nun noch Android bekannt gegeben werden. Wenig verwunderlich erfolgt das im Manifest der Anwendung:

<service android:name=".SomeService" />

Client

Auf Clientseite sind folgende Aufgaben zu erfüllen:

  • Es ist eine Implementierung notwendig, die dem Client „vortäuscht“, dass er den Service direkt aufruft.
  • Vor dem Aufruf muss sich der Client an den Service binden können. Letzteres erfolgt über eine ServiceConnection. Eine Instanz einer ServiceConnection repräsentiert die Bindung an einen Service, der den entsprechenden Dienst erbringen kann. Eine beispielhafte Implementierung ist diese:
public class TestServiceConnection implements ServiceConnection { 
	
	private Test testService = null;
	
	public Test getTestService() {
		return this.testService;
	}
	
	public void onServiceConnected(ComponentName name, IBinder service) {
		Log.d(null, "Service Connected");
		this.testService = (Test) service;
	}

	public void onServiceDisconnected(ComponentName name) {}
}

Objekte konkreter ServiceConnections werden erzeugt und danach im Prozess des Bindens von einem Binder genutzt. Folgender Code initiiert beispielsweise das Binden:

mConnection = new TestServiceConnection();
this.bindService(new Intent(this, SomeService.class), mConnection, Context.BIND_AUTO_CREATE);

In diesem Fall wird exemplarisch eine Verbindung zu einem Service aufgebaut, der durch den Klassennamen bekannt ist. Informieren Sie sich über andere Möglichkeiten des Bindens im Handbuch! Das Prinzip ist in jedem Fall gleich. Es wird verlangt, dass eine Verbindung zu einem Service erzeugt werden soll (bindService()). Zur Beschreibung des Services dient das Intent. Das Connection-Objekt wird als Referenz übergeben, der letzte Parameter ermöglicht das Setzen von Optionen für den Verbindungsaufbau.

Dieser Aufruf erfolgt asynchron, das heißt im Hintergrund wird nun versucht, eine Verbindung zum Service aufzubauen. Konnte eine Verbindung zum Serviceprozess aufgebaut werden, so wird auf der ServiceConnection die Methode onServiceConnected() aufgerufen. Als Parameter wird eine Referenz auf einen IBinder übergeben. Tatsächlich zeigt diese Referenz auf den Clientstub der RPC-Implementierung. Er implementiert ebenfalls das Interface „Test“. In dieser Implementierung wird dieses Objekt gespeichert und bei Bedarf wird es mittels getTestService() herausgegeben. In der Beispielanwendung erfolgt das, wenn auf einen Knopf gedrückt wird. Der Clientcode sieht dort etwa so aus:

int value = 0;
Test testSrv = this.mConnection.getTestService();
value = testSrv.increment(value);

Die ServiceConnection (mConnection) wurde bereits zuvor erzeugt und als Parameter dem Binder übergeben, siehe oben. An dieser Stelle wird davon ausgegangen, dass das Binden erfolgreich war. Es wird eine Referenz auf den Service (konkret den Clientstub) verlangt (getTestService() – siehe Implementierung von TestServiceConnection oben). Der Service implementiert die Servicebenutzeroberfläche „Test“. Der Service kann nun aufgerufen werden. An dieser Stelle ist er von einem lokalen Prozeduraufruf nicht zu unterscheiden. In der vollständigen Implementierung ist aber zu erkennen, dass zusätzlich auf eine RemoteException reagiert werden muss. Die Verteilung bleibt also nicht völlig transparent.

Einzelnachweise

  1. T. Schwotzer, Entfernter Mailzugriff auf RPC-Basis, Diplomarbeit, TU Chemnitz-Zwickau, Juli 1994 http://www.sharksystem.net/paper/diplom_schwotzer.pdf
  2. http://developer.android.com/guide/developing/tools/aidl.html. Stand 12. November 2010.

BroadcastReceiver

Zurück zu Googles Android

Broadcasts

Auf einem Mobiltelefon passiert ein Menge: SMS-Nachrichten und Telefonanrufe treffen ein, WLAN-Netze tauchen auf und verschwinden, die Uhrzeit ändert sich – selbst wenn nichts dergleichen passiert, geht irgendwann der Akku zur Neige. All dies sind Beispiele für Ereignisse die behandelt werden können. Aus eigener Erfahrung wissen wir, dass Einiges bereits vom Android-System übernommen wird: Wenn etwa ein Anruf eintrifft, wird die Telefon-Activity aufgerufen.

In diesem Kapitel erfahren wir, wie wir solche Ereignisse

  • mit selbstentwickelten Komponenten verarbeiten und
  • selbst auslösen können.

GUIs werden im Allgemeinen entwickelt, um dem Anwender Interaktion zu ermöglichen. Wenn er beispielsweise einen Button drückt, dann arbeitet ein Listener, der zuvor mit dem Button verbunden wurde, diesen Klick ab.

Diese Vorgehensweise hat sich etwa innerhalb homogener Java-Anwendungen bewährt, kann aber für die oben beschriebenen Android-Szenarien nicht verwendet werden: Eine Android-Anwendung ist modular aufgebaut und besteht aus mehreren DVM-Prozessen, die wir nicht mit Java-Bordmitteln auf Ereignisse ihrer Kollegen lauschen lassen können. Eine Android-Komponente kann aber DVM-übergreifend Broadcasts versenden, die andere Komponenten dann verarbeiten können. Beispielsweise versendet die Komponente, die eine SMS empfängt, einen Broadcast aus. Dieser Broadcast wird in einen Intent (siehe das Kapitel „[[../_Intents oder "Ich hätte gern den Zucker"|Intents]]“) eingebettet, dem wir über seine Extras auch Daten übergeben können. Wenn ein geeigneter Intent-Filter vorliegt, kann der Broadcast von einer speziellen Komponente, dem Broadcast-Receiver empfangen werden.

Der Receiver kann die Bundle-Daten – und dazu gehört etwa auch der Inhalt einer SMS – auslesen und verarbeiten. Broadcast-Receiver gehören zusammen mit Activities und Services zu den wichtigsten Komponenten einer Android-Anwendung.

Um Broadcasts zu abonnieren, müssen wir

  • den Namen der zugehörigen Action kennen
  • einen entsprechenden Intent-Filter im Manifest definieren
  • einen Broadcast-Receiver für diese Action entwickeln

BroadcastReceiver – ein einfaches Beispiel

Das probieren wir gleich mal aus. Ansprechende Beispiele kann man für den SMS-Empfang entwickeln. Da es hier aber wieder um das grundsätzliche Verständnis des Typs BroadcastReceiver[1] geht, backen wir kleinere Brötchen und beschränken uns auf den Broadcast, der versendet wird, wenn jemand die Uhrzeit am Telefon ändert. Die zugehörige Action heisst Intent.ACTION_TIME_CHANGED[2], der hinterlegte Text ist android.intent.action.TIME_SET.

Wir legen ein neues Android-Projekt an oder nehmen einfach ein Bestehendes und fügen in das Manifest die folgende Receiver-Definition mit einem Intent-Filter ein:

<receiver android:name=".TimeChangedReceiver"
          android:label="@string/app_name">
  <intent-filter>
    <action android:name="android.intent.action.TIME_SET" />
  </intent-filter>
</receiver>

Eine Kategorie ist nicht erforderlich. Die Klasse TimeChangedReceiver aus dem <receiver>-Tag sind wir natürlich noch schuldig. Sie muss eine Unterklasse von BroadcastReceiver sein. Wir zeigen einfach nur einen Toast (siehe das Kapitel „[[../_Intents oder "Ich hätte gern den Zucker"|Intents]]“), wenn ein Broadcast eingetroffen ist:

public class TimeChangedReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    Toast toast = Toast.makeText(context, "Time Changed", Toast.LENGTH_LONG);
    toast.show();
  }
}

Uns fallen zwei Dinge auf:

  • Die Methode onReceive[3] ist für die Entgegennahme und Verarbeitung des Broadcasts verantwortlich.
  • Am ganzen Ablauf ist keine Activity beteiligt. Tatsächlich können wir eine App entwickeln, die nichts anderes macht, als Broadcasts zu verarbeiten und gar nicht mit dem Anwender kommuniziert.

Jeder BroadcastReceiver lebt nur so lange, wie seine onReceive-Methode aktiv ist. Nach dem Abschluss der Methode haben wir auf die zugehörige Instanz keinen Zugriff mehr, insbesondere nimmt er keine weiteren Broadcasts mehr entgegen. Das ändert natürlich nichts an der Zuordnung der Broadcast-Action zu der BroadcastReceiver-Klasse, so wie sie im Intent-Filter definiert wurde. Für jeden Broadcast wird ein neues Objekt erzeugt. Die Zuordnung bleibt bestehen, so lange die Anwendung installiert ist!


Wenn wir nach der Installation der App die Uhrzeit im Emulator ändern, sollte das ungefähr so wie in der folgenden Abbildung aussehen.

Grundsätzlich

  • ist ein Receiver nicht nur auf eine Action festgenagelt, sondern kann auch verschiedene Broadcast-Arten verarbeiten.
  • kann der zum Broadcast gehörende Intent auch Daten enthalten. In dieser Hinsicht hat ACTION_TIME_CHANGED aber wenig zu bieten.

Wir verändern unsere Anwendung jetzt so, dass auch diese beiden Optionen umgesetzt werden: Ähnlich einfach wie ACTION_TIME_CHANGED wird Intent.ACTION_TIMEZONE_CHANGED[4] immer dann ausgelöst, wenn in den Einstellungen unseres Android-Systems die Zeitzone geändert wird.

Anders als im ersten Beispiel, enthalten die Extras des Intents Daten in Form der neuen Zeitzone. Diese holen wir ab und zeigen sie an.

@Override
public void onReceive(Context context, Intent intent) {
  String action=intent.getAction();
  String msg="";
  if (action.equals(Intent.ACTION_TIME_CHANGED))
    msg="Time Changed";
  else if(action.equals(Intent.ACTION_TIMEZONE_CHANGED))
    msg="time-zone";
  else
    msg="Unknown Broadcast";
  Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}

Unser Receiver kann jetzt zwei verschiedene Arten von Actions verarbeiten. Im Fall einer ACTION_TIMEZONE_CHANGED wird aus den Extras der zum Schlüssel „time-zone“ gehörende Wert ausgelesen und in einer Toast-Nachricht angezeigt. Der Fall der ACTION_TIME_CHANGED wird praktisch genauso wie im ersten Beispiel behandelt. In der Manifestdatei muss der Broadcast für ACTION_TIMEZONE_CHANGED angefordert werden.

Komplexe Broadcast-Verarbeitung

Die Grundzüge der Broadcast-Verarbeitung haben wir an zwei einfachen Beispielen kennen gelernt. So ganz lebensnah ist das aber noch nicht:

Nur in den wenigen Fällen reicht die Anzeige eines Toasts. Häufiger findet eine komplexere Verarbeitung des Broadcasts statt. Und genau hier treten zwei Probleme auf:

  • Wenn wir eine Activity in der onReceive-Methode starten wollen, um mit unseren Benutzern zu interagieren, werden wir – wenn wir nicht aufpassen – wenig Freude haben. Der DVM-Prozess zu dem der Receiver und somit auch die Activity gehört, wird nämlich zusammen mit der onReceive-Methode beendet. Das Gleiche passiert, wenn der Broadcast zur Verarbeitung einen Thread startet. Auch seine Lebenszeit ist durch die onReceive-Methode begrenzt.
  • Wenn wir keinen Thread oder keine Activity für die Verarbeitung starten, sondern den Broadcast an Ort und Stelle im gleichen Thread behandeln, lauert Android bei einer Laufzeit von mehr als 10 Sekunden mit Abschüssen in Form von 'Application Not Responding'.

Für beide Probleme gibt es aber Lösungen. Im Kapitel „[[../_Activities, Tasks und Launch Modes|Activities, Tasks und Launch Modes]]“ haben wir gelernt, wie wir Activities in einem eigenen DVM-Prozess starten können. Ein Implementierungsmuster sehen wir in der folgenden onReceive-Methode:

@Override
public void onReceive(Context context, Intent intent) {
  String action=intent.getAction();
  if (action.equals(Intent.ACTION_TIME_CHANGED))
    Toast.makeText(context, "Time Changed", Toast.LENGTH_LONG).show();
  else if(action.equals(Intent.ACTION_TIMEZONE_CHANGED))
    showTimezone(context, intent.getStringExtra("time-zone"));

}

private void showTimezone(Context context, String msg) {
  Intent intent=new Intent("de.wikibooks.android.TIMEZONEACTIVITY");
  intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  intent.putExtra("Message", msg);
  context.startActivity(intent);
}

Wir sehen, dass für ACTION_TIMEZONE_CHANGED-Broadcasts unsere eigene Methode showTimezone aufgerufen wird.

Ihr übergeben wir neben dem Kontext noch den Wert der neuen Zeitzone als Text. Die Intent-Verarbeitung erfolgt wie im Kapitel „[[../_Intents oder "Ich hätte gern den Zucker"|Intents]]“) beschrieben; neben der Activity wurde dem Intent noch das Flag FLAG_ACTIVITY_NEW_TASK[5] mitgegeben, um die Activity in einem separaten DVM-Prozess einzubetten. Selbstverständlich müssen wir die zu unserer selbstdefinierten Action TIMEZONEACTIVITY gehörende Klasse noch definieren und im Manifest erfassen.

AndroidManifest.xml

<activity android:name=".TimezoneActivity"
          android:label="@string/app_name">
  <intent-filter>
    <action android:name="de.wikibooks.android.TIMEZONEACTIVITY" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>        
</activity>

TimezoneActivity.java

public class TimezoneActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.timezone);
    String msg=getIntent().getStringExtra("Message");    
    TextView view = (TextView) findViewById(R.id.zone);
    view.setText("New Timezone: "+msg);
  }
}

Der Inhalt der Layout-Datei timezone.xml sollte klar sein und wird hier nicht weiter diskutiert: Die Datei enthält nur eine einfache TextView[6] mit zone als id. Auch die Entgegennahme der Extras in der onCreate-Methode verläuft geschmeidig.

Im Kapitel über Services (siehe [[../_Services|Services]]) haben wir gesehen, dass wir auch Services in eine separate DVM einbetten können.

Nun ist es aber nicht die feine englische Art den Benutzer mit einer plötzlich erscheinenden Activity bei seiner 'User-Experience' zu stören. Geeigneter sind hier die Notfications, die wir auch noch in einem eigenen Kapitel (siehe [[../_Notifications|Notifications]]) kennen lernen werden.

Dynamische Receiver

Wenn wir einen Receiver über einen Intent-Filter im Manifest mit einer Action verbunden haben, dann gilt dieser Bund bis dass die zum Receiver gehörende Anwendung vom Gerät entfernt wird. Diese Art der Broadcast-Receiver wird daher auch statisch genannt. Statische Receiver sind aber nicht immer das, was wir brauchen. Oft soll die Bindung einer Action an einen Receiver nur so lange, wie eine bestimmte Komponente bestehen. Hier gibt es die Möglichkeit den Intent-Filter 'programmatisch' zu definieren und dann die Zuordnung innerhalb der Komponente und nicht im Manifest durchzuführen.

Im folgenden Beispiel definieren wir einen Receiver als innere Klasse einer Activity. In der onResume-Methode der Activity werden Receiver und Intent-Filter erzeugt und über die registerReceiver-Methode mit der Activity verbunden. In der onPause-Methode werden Activity und Receiver wieder entkoppelt. So wird erreicht, dass der Receiver so lange lebt und Broadcasts entgegennehmen kann, wie die Activity im Vordergrund ist. Erst beim Wechsel in den Hintergrund, wird die Zuordnung aufgehoben und der Receiver der Garbage-Collection übergeben.

private class TimeChangedReceiver extends BroadcastReceiver{
  @Override
  public void onReceive(Context context, Intent intent){
    Toast.makeText(context, "Time  Tick", Toast.LENGTH_LONG).show();
  }
}

private BroadcastReceiver receiver;
@Override
public void onResume() {
  receiver=new TimeChangedReceiver();
  IntentFilter filter =new IntentFilter(Intent.ACTION_TIME_TICK  );
  this.registerReceiver(receiver, filter);
  super.onResume();
}
@Override
public void onPause() {
  this.unregisterReceiver(receiver);
  receiver=null;
  super.onPause();
}

Während ihrer Lebenszeit können dynamische Receiver also, anders als statische Receiver, mehrere Broadcasts entgegenehmen.

Broadcasts versenden

Bisher haben wir Broadcasts nur empfangen. Der Versand ist aber für uns kein Problem. Er erfolgt ganz ähnlich wie mit der Methode startActivity zum Start einer Activity im Rahmen der Intent-Verarbeitung mit Hilfe der Methode startBroadcast, die ebenso wie startActivity von der Klasse Context geerbt wird.

Im folgenden Beispiel versenden wir einen ACTION_TIME_CHANGED[7]-Broadcast aus einer beliebigen Activity. Wenn unser kleiner Demo-Receiver TimeChangedReceiver jetzt noch installiert ist, sollte er gleich beim Start dieser Activity anschlagen und einen Toast anzeigen.

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  Intent intent = new Intent(Intent.ACTION_TIME_CHANGED);
  sendBroadcast(intent);
}

Wir werden uns noch mit dem Empfang von SMS-Nachrichten beschäftigen und werden dann Gelegenheit haben, unser Wissen über Broadcasts weiter anzuwenden und an praxisnäheren Beispielen zu vertiefen.

Geordnete Broadcasts

Zusätzlich zur Methode sendBroadcast gibt es noch sendOrderedBroadcast.[8] Auch hier wird ein Intent als Parameter übergeben, der neben der Beschreibung des Intents auch weitere Daten in Form von Extras enthalten kann.

Ein Broadcast, der mittels sendBroadcast versendet wurde, wird an alle Empfänger (quasi) gleichzeitig übermittelt. Ein Broadcast, der mittels sendOrderedBroadcast verschickt wurde, wird dagegen sequentiell an Receiver verschickt. Begonnen wird bei denen mit der höchsten Priorität. Bei Receivern mit gleicher Priorität ist die Reihenfolge willkürlich.

Die Priorität kann durch ein Tag <android:priority>[9] innerhalb der Definition des Intent-Filters in der Deklaration des <receiver>-Tags[10] definiert werden. All diese Deklarationen sind aber optional.

Broadcasts, die mit sendOrderedBroadcast gesendet wurden, bieten einzelnen Receivern die Möglichkeit, Nachrichten anzuhängen, die der nächste Empfänger lesen kann. Dazu dienen die Methoden setResult[11] (und Varianten davon) und getResult[Code[12]|Data|Extras]. Damit lassen sich Abarbeitungsketten aufbauen und Zwischenergebnisse und Status übermitteln. Das ist ein bekanntes und sinnvolles Entwurfselement für Software (siehe auch das Design Pattern Zuständigkeitskette).

Einzelnachweise

  1. http://developer.android.com/reference/android/content/BroadcastReceiver.html
  2. http://developer.android.com/reference/android/content/Intent.html#ACTION_TIME_CHANGED
  3. http://developer.android.com/reference/android/content/BroadcastReceiver.html#onReceive(android.content.Context,%20android.content.Intent)
  4. http://developer.android.com/reference/android/content/Intent.html#ACTION_TIMEZONE_CHANGED
  5. http://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK
  6. http://developer.android.com/reference/android/widget/TextView.html
  7. http://developer.android.com/reference/android/content/Intent.html#ACTION_TIMEZONE_CHANGED
  8. http://developer.android.com/reference/android/content/Context.html#sendOrderedBroadcast%28android.content.Intent,%20java.lang.String%29. Stand 14. November 2010
  9. http://developer.android.com/reference/android/R.styleable.html#AndroidManifestIntentFilter_priority. Stand 14. November 2010
  10. http://developer.android.com/reference/android/R.styleable.html#AndroidManifestReceiver. Stand 14. November 2010
  11. http://developer.android.com/reference/android/content/BroadcastReceiver.html#setResult%28int,%20java.lang.String,%20android.os.Bundle%29. Stand 14. November 2010
  12. http://developer.android.com/reference/android/content/BroadcastReceiver.html#getResultCode%28%29. Stand 14. November 2010

Notifications

Zurück zu Googles Android


Wenn auf einem Android-Telefon ein Anruf eintrifft, wird automatisch die Telefon-Activity aufgerufen. Wie das grundsätzlich funktioniert, haben wir im Kapitel „[[../_BroadcastReceiver|BroadcastReceiver]]“ gesehen. In den meisten Fällen ist es aber kein gutes GUI-Design wenn Activities plötzlich den Vordergrund dominieren. Trifft beispielsweise eine SMS ein, dann werden wir darüber, wie in der der folgenden Abbildung 1, durch ein kleines Icon oben links in der Statusleiste informiert.

Diese so genannte Notification kann noch durch einen Vibrationsalarm, eine Audiosignal oder eine blinkende LED begleitet werden. Wenn wir die Statusleiste wie in der Abbildung 2 nach unten ziehen, sehen wir eine kleine Activity, die zur Notification gehört. Wenn wir diese Activity anklicken gelangen wir zur eigentlichen SMS-Activity (Abbildung 3).

Ein Beispiel

So eine Notification wollen wir hier entwickeln. Ganz ähnlich wie im Kapitel über BroadcastReceiver wird der Broadcast, der ausgelöst wird, wenn ein Anwender die Zeitzone ändert, abgefangen. In unserem Beispiel verschickt der Receiver dieses Mal eine Notification. Wenn der Anwender diese Notification anklickt, wird er zu einer Activity geführt, die die Zeitzone – in Großbuchstaben – anzeigt.

In der onReceive-Methode unseres Broadcast-Receivers, mit dem wir Änderungen an der Zeitzone verarbeiten (siehe auch das Kapitel über [[../_BroadcastReceiver|BroadcastReceiver]]) erzeugen wir mit Hilfe eines Konstruktors der Klasse Notification [1] eine Notification:

public class TimeChangedReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    int icon = android.R.drawable.ic_dialog_alert;
    String title = "Settings Changed";
    String text = "User select new timezone";
    long when = System.currentTimeMillis();
    String timezone = intent.getStringExtra("time-zone");
    Notification notification = new Notification(icon, title, when);
  }
}

Die Parameter des Konstruktors bedürfen einer kurzen Erläuterung.

  • Jede Notification wird durch ein Symbol (icon) in der Statusleiste repräsentiert, dessen Code wir aus der Ressourcenklasse R erhalten.
  • Zusammen mit dem Symbol wird ein Kurztitel in der Statusleiste angezeigt (title).
  • Außerdem geben wir noch die Uhrzeit (when) an, zu der die Notification rausgeht.

Der Notification-Manager

An unserem Telefon tut sich jetzt aber noch nichts. Die Notification muss einem Objekt vom Typ NotificationManager[2] übergeben werden, der die Notification bearbeitet und sie in geeigneter Form zur Anzeige bringt.

...
NotificationManager mgr =
  (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
int notidication_id = (int) System.currentTimeMillis();
mgr.notify(notidication_id, notification);

Es gibt im Android-System verschiedene Manager-Klassen, die der Kontext über die getSystemService-Methode zur Verfügung stellt.

So bekommen wir auch den NotificationManager, über den wir dann die Notification mit der Methode notify[3] (die nichts mit der gleichnamigen Methode der Klasse Object zu tun hat) verschicken.

  • Der erste Parameter, den wir dieser Methode übergeben, ist eine Zahl, die Android verwendet, um Notifications zu unterscheiden. Wenn wir hier immer die gleiche Zahl verwenden würden, dann würden auch alle Notifications wie eine einzige erscheinen.
  • Der zweite Parameter ist die eigentliche Notification.

Wenn wir die Notifications wie beschrieben versenden, erhalten wir noch zur Laufzeit einen Fehler. Wir haben es nämlich versäumt eine Activity anzugeben, die uns beim Anklicken der Notification – ähnlich wie in Abbildung 3 – angezeigt werden soll.

Intents und Pending-Intents

Da wir die Activity verzögert ausführen wollen, müssen wir sie irgendwie 'einpacken'. Auf den ersten Blick scheint dazu ein Intent bestens geeignet zu sein.

...
Intent notificationIntent = new Intent(context, UpperActivity.class);
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
notificationIntent.putExtra("time-zone", timezone);

Auf diese Weise haben wir schon häufiger Intents erzeugt. Hier werden auch wieder Extras übergeben und ein Flag gesetzt, damit die Activity in einem eigenen DVM-Prozess ausgeführt wird.

Diesen Intent definieren wir in unserem Beispiel aber in der onReceive-Methode eines BroadcastRecevier-Objektes. Nach dem Ende der Methode müssen wir davon ausgehen, dass der zugehörige DVM-Prozess nicht mehr existiert und die Activity, die wir in dem Intent verpackt haben, nicht mehr gestartet werden kann. Wir können den Intent und damit die Activity aber konservieren, indem wir sie in ein Objekt vom Typ PendingIntent (siehe auch das Kapitel „[[../_Intents oder "Ich hätte gern den Zucker"|Intents]]“) einbetten:

...
PendingIntent contentIntent =
  PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_ONE_SHOT );        
  • Der erste Parameter repräsentiert ein Objekt vom Typ Context,
  • Der zweite wird derzeit (Android 2.2) noch nicht ausgewertet,
  • Im dritten ist der Intent hinterlegt und
  • Im vierten, wird beschrieben, was beim Anklicken der Notification passieren soll. Hier haben wir uns für die Variante FLAG_ONE_SHOT entschieden: Nur der erste Klick führt uns zur Activity.

Unsere Notification können wir um einen Pending-Intent erweitern. In dem Fall wird nicht nur eine Mitteilung angezeigt, sondern die Mitteilung auch mit einer Activity verbunden. Diese Activity ist im Pending-Intent definiert, den wir jetzt zur Notification hinzufügen.

...
notification.setLatestEventInfo(context, title, text, contentIntent);
mgr.notify(notidication_id, notification);

Es ist zu sehen, dass die Notification, die weiter oben erzeugt wurde, mittels setLastestEventInfo[4] um ein PendingIntent-Objekt erweitert wird und dass danach der NotificationManager gebeten wird, diese Notification anzuzeigen.

Alles zusammen

Wenn wir die Code-Fragmente dieses Kapitels zusammensetzen, ergibt sich die folgende Receiver-Klasse

public class TimeChangedReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    int icon = android.R.drawable.ic_dialog_alert;
    String title = "Settings Changed";
    String text = "User select new timezone";
    long when = System.currentTimeMillis();
    String timezone = intent.getStringExtra("time-zone");

    Notification notification = new Notification(icon, title, when);
        
    NotificationManager mgr =
      (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    int notidication_id = (int) System.currentTimeMillis();
    Intent notificationIntent = new Intent(context, UpperActivity.class);
    notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    notificationIntent.putExtra("time-zone", timezone);
    PendingIntent contentIntent =
      PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_ONE_SHOT );        
    notification.setLatestEventInfo(context, title, text, contentIntent);
    mgr.notify(notidication_id, notification);

}

Die Gestaltung der sehr einfachen Klasse UpperActivity sowie die Erfassung der Komponenten im Manifest haben wir bereits mehrfach erläutert. Diese abschließenden Schritte bleiben dem Leser zur Übung überlassen.

Einzelnachweise

  1. http://developer.android.com/reference/android/app/Notification.html#Notification%28%29. Stand 14. Nov. 2010
  2. http://developer.android.com/reference/android/app/NotificationManager.html
  3. http://developer.android.com/reference/android/app/NotificationManager.html#notify%28java.lang.String,%20int,%20android.app.Notification%29. Stand 14. Nov. 2010
  4. http://developer.android.com/reference/android/app/Notification.html#setLatestEventInfo(android.content.Context,%20java.lang.CharSequence,%20java.lang.CharSequence,%20android.app.PendingIntent)

ContentProvider

Zurück zu Googles Android


Eine Eigenschaft von Programmierframeworks besteht in der Schaffung von einheitlichen Sichten auf unterschiedliche Dinge. Dieser Aufgabe wird sich mehrfach gestellt, so auch beim Thema Datenzugriff. Auf Betriebssystemebene hat sich die Abstraktion Stream etabliert, um Dateien, Netzwerkverbindungen etc. gleich zu behandeln. Es wird eine Quelle geöffnet, Daten werden in den Stream geschrieben oder von dort gelesen.

In Android ist die allgemeine Sicht auf Daten (aus Dateien, über eine Netzverbindung, berechnete Daten etc.) der ContentProvider. An einem Beispiel soll der Aufbau eines (höchst simplen) eigenen ContentProviders gezeigt werden, der aber als Rahmen dienen kann, um für realistische Anwendungen weiterentwickelt zu werden. Verwiesen sei auch hier auf die exquisite Online-Dokumentation von Android [1] und ein Android-Buch.[2]

Beim Programmieren gibt es in der Regel zwei Sichten: Die des Aufrufenden (Caller, Client) und die des Aufgerufenen (Callee, Server, Provider).

Datenzugriff durch den Caller

Für den Caller stellt ein ContentProvider Tabellen zur Verfügung, wie man sie von Datenbanken her kennt. Vor der Nutzung muss der Caller zunächst den konkreten ContentProvider ermitteln. Dies erfolgt in Android mittels einer URI: die URI adressiert also konkreten Content.

Es gibt in Android eine Fülle von fertigen ContentProvidern. Eine Übersicht findet sich zum Beispiel in der Online-Dokumentation.[3] Jede Klasse verfügt über eine statische Konstante namens URI, die zur Identifikation des gewünschten Providers genutzt werden kann.
Wird ein eigener ContentProvider geschrieben, so ist eine URI zu definieren. Diese muss den Callees bekannt sein.

Der ContentResolver [4] stellt die Verbindung zwischen Caller und ContentProvider her und bildet gleichzeitig eine Abstraktion des ContentProviders. Eine Referenz auf den ContentResolver kann ihrerseits innerhalb jedes Ausführungskontextes durch die Methode getContentResolver() beschafft werden. Über den ContentResolver kann man anschließend die Datenquelle (die durch die URI ausgewählt wurde) mittels einer Query auf Daten durchsuchen. Folgendes Codefragment zeigt eine höchst simple Anfrage, illustriert aber das Vorgehen.

String uriString = "content://de.htw-berlin.f4.ai.thsc.sampleContentProvider"; 
Uri sampleURI = Uri.parse(uriString);
ContentResolver cr = this.getContentResolver(); 
Cursor cursor = cr.query(sampleURI, null, null, null, null);

In dem Beispiel wird als Abfrage lediglich die URI übergeben, alle andere Parameter sind leer. Das hat zum Ergebnis, dass alle Daten der Tabelle ausgewählt werden. Die Parameter sollen hier nur kurz genannt werden. Eine vollständige Beschreibung findet sich zum Beispiel in einem Online-Tutorial [ADevCP].

Query hat folgende Parameter:

  • URI: Adresse der Datenquelle, das heißt des ContentProviders
  • projection: ein String[], der die Spalten benennt, die selektiert werden sollen. Die Namen der Spalten muss der Content-Provider bekannt geben. Üblicherweise erfolgt das durch Konstanten, die in der Implementierung des Providers deklariert sind.
  • selection: Auswahl der Zeilen. Das erfolgt mittels eines Strings, der die Syntax einer SQL WHERE clause haben muss.
  • selection args: Hier können in einem Stringarray Argumente deklariert werden, die in der WHERE clause referenziert werden.
  • sortOrder: Ein String in der Syntax der SQL ORDER BY clause.

Das Ergebnis ist eine Datenmenge, durch die in SQL-üblicher Weise mittels eines Cursors navigiert werden kann. Es empfiehlt sich, den Cursor direkt nach dessen Erzeugung auf die erste Ergebniszeile zu setzen.

cursor.moveToFirst();

Der Cursor weist nun auf die erste Zeile der Datensätze, die den Anfragekriterien entsprachen. Es können nun die Werte der Ergebniszeile entnommen werden. Die Auswahl der Spalten erfolgt durch einen Index. Beispiel:

cursor.getString(1);

Das Beispiel entnimmt dem aktuellen Datensatz das Element der ersten Spalte und liefert sie als String zurück. Dieser Methodenaufruf würde scheitern, wenn es die Spalte 1 nicht gäbe oder wenn der Inhalt kein String wäre.
Natürlich empfiehlt sich auch in Android die Nutzung von Konstanten, das heißt es sollte nicht explizit eine 1 als Index genutzt werden, sondern ein symbolischer Name, zum Beispiel

cursor.getString(SampleContentProvider.VALUE);

Die Nutzung eines ContentProviders unterscheidet sich nicht grundsätzlich von der Nutzung einer relationalen Datenbank. Die URI ist das Pendant zum Namen einer Tabelle. Es erfolgt über den ContentResolver eine Query, durch deren Ergebnismenge mittels eines Cursor navigiert werden kann.

ContentProvider (Callee)

Die Nutzung eines ContentProviders ist überschaubar einfach. Ebenso ist die Implementierung eines ContentProviders, basierend auf einer SQL-Datenbank, ebenfalls recht einfach, da das Grundkonzept identisch ist.

Sollen andere (nicht datenbankartige) Datenquellen angeschlossen werden, kann das Unterfangen allerdings ungleich komplexer werden. An dieser Stelle soll der grobe Rahmen gezeigt werden, wie ein eigener Provider geschrieben werden kann. Details, wie eine konkrete Quelle angebunden werden kann oder wie gar die Parameter der Query parsiert werden können, werden hier außen vorgelassen.

Auf den ersten Blick ist die Implementierung eines ContentProviders allerdings weiterhin überschaubar: Es muss zunächst eine Klasse für den ContentProvider angelegt werden, zum Beispiel

public class SampleContentProvider extends ContentProvider {
... }
public static final Uri CONTENT_URI = Uri.parse("content://de.htw-berlin.f4.ai.thsc.sampleContentProvider");
/** the id */ public static final String _ID = "_id";
/** value */ public static final int VALUE = 1;

Der ContentProvider repräsentiert die enthaltenen Daten eine Tabelle. Wie oben beschrieben, wird die Adresse der Tabelle mittels einer URI definiert. Die Tabelle im Beispiel hat zwei Spalten (ID und VALUE), was der Mindestanforderung an jeden ContentProvider entspricht. An dieser Stelle lassen sich bei Bedarf beliebig viele weitere Spalten definieren.

Der Provider muss nun im System bekannt gemacht werden. Dies erfolgt mittels der Manifest-Datei und eines Eintrages wie folgendem:

<provider android:name=".SampleContentProvider"
		  android:authorities="de.htw-berlin.f4.ai.thsc.sampleContentProvider">
</provider>

Damit wird definiert, dass es einen Provider mit der Adresse de.htw-berlin.f4.... gibt und dass die Klasse SampleContentProvider in diesem Package die Implementierung davon ist. Mittels dieses Eintrages kann der ContentResolver ein Objekt der Klasse (ContentProvider) ermitteln, um eine Query zu erfüllen.

Tatsächlich erzeugt der Resolver beim Aufruf der Query ein Objekt dieser Klasse und leitet die Parameter des Aufrufers weiter an den ContentProvider. Dieser muss anschließend darauf reagieren.

Es gibt noch eine Reihe weiterer Methoden, die ein ContentProvider erfüllen muss. Sie seien hier kurz notiert:

  • insert
  • delete
  • query
  • update
  • getType

Die meisten Methoden sind selbsterklärend; sie sind semantisch identisch zu ihren Pendants in SQL. Die Methode getType soll hingegen den MIME-Typ der Daten liefern, die der ContentProvider anbietet.

Im beigefügten Beispielcode wird ein höchst simpler Provider realisiert. Er repräsentiert eine Tabelle, die nicht veränderlich ist. Daher ist die Implementierung von delete, insert und update nicht nötig. Lediglich die Query muss implementiert werden.

Auch das ist hinreichend komplex, wenn alle Parameter der Query unterstützt werden sollen. Das Vorgehen des Parsens der WHERE und ORDER-BY clause werden hier nicht gezeigt. Im Gegenteil, es wird davon ausgegangen, dass die Provider diese Parameter nicht unterstützt und ignoriert. Selbst mit diesen harschen Einschränkungen besteht noch die Notwendigkeit, einen Cursor zu realisieren. Warum? Rein syntaktisch, weil der Rückgabewert von query ein Cursor ist:

public abstract Cursor query(Uri uri, String[] projection, String selection,
                             String[] selectionArgs, String sortOrder)

Die Implementierung eines eigenen Cursor ist nicht zu empfehlen, wenn auch möglich. Ein eigener Cursor wird implementiert, indem das Interface „Cursor“ [5] implementiert wird.

In der Dokumentation ist aber schon erkennbar, dass es eine ganze Reihe von fertigen Implementierungen gibt, so zum Beispiel SQLLiteCursor. Die Realisierung eines solche Cursors erscheint einfach, da das Modell und damit auch die Parameter von update, insert, delete und query identisch einer SQL-Datenbank sind. Ein solcher SQLLiteCursor delegiert den Aufruf an die Datenbasis.

Im Beispielprogramm wird der MatrixCursor genutzt. Wie es der Name vermuten lässt, bilden Objekte dieser Klasse eine Matrix. Die Breite der Matrix entspricht der Anzahl der Spalten und die Länge der Anzahl der Zeilen. Ein MatrixCursor wird durch Auswahl der jeweiligen Spaltennamen erzeugt. Damit ist die Breite der Tabelle festgelegt. Anschließend können Daten hinzugefügt werden; dies erfolgt einfach durch das Hinzufügen von Object-Arrays. Beispiel:

private MatrixCursor createCursor() { 
	String[] columns = new String[]{ "_id", "value"}; 
	MatrixCursor mc = new MatrixCursor(columns);

	Object[] row = new Object[] {"ein Baum", "hat Blätter"}; 
	mc.addRow(row);
	return mc;
}

In diesem Beispiel wird eine Tabelle mit zwei Spalten simuliert, die lediglich eine Zeile enthält. Es ist einfach erkennbar, dass durch mehrfaches Aufrufen von addRow() weitere Zeilen hinzugefügt werden können. Die Nutzung von Object[] mag auf den ersten Blick irritieren, weil damit nahezu jede Typsicherheit umgangen wird. An dieser Stelle kann allerdings nur so vorgegangen werden, da alle Datentypen innerhalb einer Tabelle erlaubt sein sollen.

Im Beispielprogramm wird eine Cursormatrix genutzt, um query() zu implementieren:

@Override 
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { 
	return this.createCursor();
}

Es ist erkennbar, dass diese Implementierung sämtliche Parameter der Query ignoriert und immer die gleichen Daten liefert. Das ist natürlich nur für ein Beispielprogramm sinnvoll, das heißt, es muss für reale Implementierungen angepasst werden.

Einzelnachweise

  1. http://developer.android.com/guide/topics/providers/content-providers.html. Stand 12. November 2010.
  2. Becker, Arno; Pant, Marcus: „Android – Grundlagen und Programmierung“, dpunkt.verlag, 1. Auflage 2009. Seite 189 ff.
  3. http://developer.android.com/reference/android/provider/package-summary.html. Stand 12. November 2010.
  4. http://developer.android.com/reference/android/content/ContentResolver.html. Stand 12. November 2010.
  5. http://developer.android.com/reference/android/database/Cursor.html. Stand 12. November 2010.

Datenbankzugriffe

Zurück zu Googles Android


SQLite – eine gute Wahl

Daten wie die Einstellungen des Android-Systems, Kontakte oder die Telefonprotokolle legt Android in einer SQLite-Datenbank ab.

Das zugehörige Datenbanksystem SQLite[1] gehört zu den stabilsten und am meisten ausgereiften Werkzeugen, das der Open-Source-Bereich zu bieten hat. Wenn es keine Anforderungen an Transaktionen oder Mehrbenutzerfähigkeit gibt – wie es im Embedded-Bereich meistens der Fall ist – dann ist SQLite auch an Geschwindigkeit kaum zu schlagen, und das bei einer Größe des Installationspaketes von deutlich unter 1 MB. Zwar kann man in Android-Anwendungen auch mit einem anderen Datenbanksystem arbeiten, doch gibt es keinen Grund dazu.

Für die Arbeit mit SQLite stellt Android Klassen wie SQLiteDatabase[2] zur Verfügung, die auf das System zugeschnitten sind.

In Java-Anwendungen ist JDBC [3] (Java Database Connectivity) ja eigentlich der Standard, doch wird in Android-Anwendungen nicht zur Verwendung von JDBC geraten. Wir werden in diesem Kapitel sehen, wie einfach die Arbeit mit der SQLite-Schnittstelle des Android SDK von der Hand geht.

Konfigurieren statt Programmieren

Wenn wir in unserer Android-Anwendung mit SQLite arbeiten, müssen wir immer mal wieder SQL-Anweisungen formulieren. Dabei entstellt es den ganzen Programmcode, wenn wir mehrzeilige SQL-Anweisungen als Text in den Code schreiben. Außerdem müssen wir den Java-Code neu übersetzen, sobald sich etwas an den SQL-Anweisungen ändert. Hier sorgt das Android-Plugin von Eclipse für Erleichterung:

Wir können Texte – und somit auch SQL-Anweisungen – in XML-Dateien schreiben, die dann zur Laufzeit als Objekte vom Typ String zur Verfügung stehen:

Im Android-Projekt gibt es parallel zum layout-Ordner den Ordner values. In diesem Ordner finden wir die Datei strings.xml, in die wir einige SQL-Anweisungen wie create table zum Initialisieren der Datenbank hinterlegen. Neben dem Array, das wir create genannt haben, sind hier auch Texte für app_name, version und dbname vereinbart:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">AndroidDatabase</string>
  <string name="version">1</string>
  <string name="dbname">songsdb</string>
  <string-array name="create">
    <item>
      create table artists(
        id integer primary key autoincrement,
        name varchar(20) not null
      )
    </item>
    <item>
      create table songs(
        id integer  primary key autoincrement,
        title varchar(20) not null,
        artist int references artist
      )
    </item>
    <item>
      insert into artists(name) values (\'The Beatles\')
    </item>
    <item>
      insert into artists(name) values (\'Pink Floyd\')
    </item>
    <item>
      insert into songs(title, artist) values (\'Yellow Submarine\', 1);
    </item>
    <item>
      insert into songs(title, artist) values (\'Help\', 1);
    </item>
    <item>
      insert into songs(title, artist) values (\'Get Back\', 1);
    </item>
    <item>
      insert into songs(title, artist) values (\'Wish You Were Here\', 2);
    </item>
    <item>
      insert into songs(title, artist) values (\'Another Brick in the Wall\', 2);
    </item>
  </string-array>
</resources>

Im Rahmen des Build-Prozesses wird aus dieser Datei eine Java-Klasse namens R[4] generiert, die wir – wie in der folgenden Abbildung gezeigt – im Android-Projektverzeichnis finden. Diese Klasse enthält IDs in Form von ganzen Zahlen für unsere Objekte aus strings.xml.

Wir sehen jetzt, wie wir die in strings.xml hinterlegten SQL-Anweisungen ganz einfach ausgeben können: Wir erzeugen ein neues Android-Projekt und hängen an die onCreate-Methode der Standard-Activity die folgenden Zeilen Code:

for(String sql : getResources().getStringArray(R.array.create))
   System.out.println(sql);

Zu jedem Android-Projekt kann es Ressourcen geben, wie etwa die Texte, die wir hinterlegt haben. Die Methode Context.getResources[5] liefert uns ein Objekt vom Typ Resources,[6] das wiederum die Methoden wie getStringArray[7] enthält, mit dem wir wiederum auf unser String-Array zugreifen können. Dieser Methode übergeben wir die IDs unseres Arrays aus der Klasse R und können es dann wie jedes andere Array durchiterieren. Die println-Methode schreibt unsere SQL-Anweisungen in das Protokoll.

Texte und Arrays von Texten sind nur ein Teil der Ressourcen, die der Entwickler konfigurieren kann. Die Dokumentation zu den Typen R und Resources zeigt uns weitere Möglichkeiten auf.

Die Datenbank erzeugen

Objekte vom Typ SQLiteOpenHelper[8] versorgen uns mit Datenbankverbindungen, die wir brauchen, um überhaupt SQL-Anweisungen zu SQLite schicken zu können.

Da diese Klasse aber abstrakt ist, müssen wir eine eigene Unterklasse entwickeln. In unserem Beispiel haben wir sie SongDatabaseHelper genannt. Das folgende Listing zeigt die Implementierung; ihre Details werden wir uns gleich erarbeiten.

public class SongDatabaseHelper extends SQLiteOpenHelper {
 
  private Context context;

  SongDatabaseHelper(Context context){
    super(
        context,
        context.getResources().getString(R.string.dbname),
        null,
        Integer.parseInt(context.getResources().getString(R.string.version)));
    this.context=context;
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
    for(String sql : context.getResources().getStringArray(R.array.create))
      db.execSQL(sql);
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  }
}

Der (einzige) Konstruktor der Basisklasse SQLiteOpenHelper hat die folgende Signatur:

SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)

Unser eigener Konstruktor hat nur den Kontext als Parameter, aus dem wir dann die Argumente für den Konstruktor der Basisklasse ermitteln, den super repräsentiert.

  • Den Namen der Datenbank und ihre Version lesen wir wie weiter oben beschrieben aus strings.xml aus.
  • Der Dokumentation entnehmen wir, dass null im dritten Parameter die Standard-CursorFactory repräsentiert – und die soll erst einmal reichen.
  • Die Version der Datenbank bezeichnet nicht die Produktversion von SQLite, sondern eine Versionsnummer, die von unserer Anwendung verwaltet wird. Immer wenn sich die Anwendung ändert, kann dies auch Änderungen an der Datenbank erfordern: sei es, dass Tabellen und Spalten kommen und gehen oder dass neue Datensätze hinzugefügt werden. Wir sehen später in diesem Kapitel, wie die Methode onUpgrade für Maßnahmen beim Versionswechsel greifen kann.

Da wir den Kontext auch an anderer Stelle benötigen, kopieren wir ihn in ein privates Attribut. Zunächst interessiert uns aber die Methode onCreate.[9] Sie ist ebenso wie onUpgrade[10] in der Basisklasse mit dem Schlüsselwort abstract markiert und muss daher überschrieben werden. Die Methode onCreate wird immer dann aufgerufen, wenn es beim Aufbau der Verbindung die Datenbank, die mit dem Konstruktorparameter name bezeichnet wird, noch nicht gibt. Es ist sehr praktisch, dass diese Methode mit einem Parameter vom Typ SQLiteDatabase aufgerufen wird. So können wir mit execSQL[11] gleich unsere in strings.xml hinterlegten SQL-Anweisungen ausführen. Bevor wir uns mit onUpgrade beschäftigen, wollen wir die Datenbank erzeugen, einige Tabellen anlegen und ein paar Daten einfügen.

Dazu gehen wir in die Standard-Activity unseres Projektes und fügen an das Ende ihrer onCreate-Methode die folgenden Zeilen:

SQLiteOpenHelper database = new SongDatabaseHelper(this);
SQLiteDatabase connection = database.getWritableDatabase();

Wir erzeugen ein Objekt vom Typ SongDatabaseHelper und geben dem Konstruktor eine Referenz auf unsere Activity mit, die ja vom Typ Context abgeleitet ist. Über die Methode getWritableDatabase erhalten wir dann eine Datenbankverbindung vom Typ SQLiteDatabase, über die wir SQL-Anweisungen verschicken und wieder einsammeln können. Doch dazu später mehr.

Das ER-Diagramm, das zu den beiden Tabellen gehört, die wir mit den „create table“-Anweisungen angelegt haben, finden wir in der folgenden Abbildung:

Ein Blick hinter die Kulissen

Zunächst schauen wir uns unsere Datenbank genauer an. Dazu führen wir auf der Unix-Shell oder der Win32-Konsole unserer Entwicklungsmaschine das folgende Kommando aus, das zum Android-SDK gehört:

adb shell

Wir erhalten so eine Shell auf unserem virtuellen Device. Es sollte ein einfacher Prompt erscheinen:

#

Wenn de.wikibooks.android der Paketname unseres Android-Projektes ist, dann führt uns der Verzeichniswechsel

cd /data/data/de.wikibooks.android/databases

zu dem Verzeichnis, in dem auch die von SQLite angelegte Datenbankdatei liegt:

# ls
songsdb

Wir starten SQLite und verbinden uns mit der Datenbank songsdb:

# sqlite3 songsdb
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>

Mit der Anweisung .help bekommen wir eine Übersicht über alle Anweisungen, die für die interaktive Arbeit mit dem Datenbanksystem zur Verfügung stehen. Zunächst reicht uns eine Übersicht über die Tabellen:

sqlite> .tables
android_metadata  artists  songs

Die Tabellen artists und songs haben wir in der onCreate-Methode unseres SQLiteOpenHelper-Objektes angelegt, die Tabelle android_metadata wurde vom Android-System angelegt. Sie enthält aber keine interessanten Informationen:

sqlite> select * from android_metadata;
en_US

Bei SQL-Anweisungen müssen wir auch immer an das abschließende Semikolon denken.

Neue Versionen

Wir haben uns davon überzeugt, dass die Datenbank, ihre Tabellen und einige Daten jetzt angelegt sind. Immer wenn wir unsere App neu starten, arbeiten wir mit dieser gleichen Datenbank. Die onCreate-Anweisung wird nur ein einziges Mal ausgeführt.

Wenn es im Laufe der Zeit nötig wird, die Datenbank zu ändern, geben wir ihr einfach eine neue Version. In unserem Szenario ist die Versionsänderung bereits mit einer kleinen Änderung in strings.xml getan:

<string name="version">2</string>

Wenn wir unser SQLiteOpenHelper-Objekt jetzt erzeugen, wird erkannt, dass es einen Versionswechsel gegeben hat und die Methode

onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

aufgerufen. Die Parameternamen verraten uns dabei bereits die Bedeutung. In unserem Fall soll beim Wechsel von der Version 1 auf die Version 2 einfach eine Spalte zur Tabelle artists hinzugefügt werden, die aussagt, ob der Interpret eine Gruppe oder ein Solist ist. Da wir in unserem initialen Datenbestand nur die Gruppen ‚The Beatles‘ und ‚Pink Floyd‘ haben, setzen wir die entsprechenden Einträge auf ‚Y‘. Die zugehörigen Anweisungen verpacken wir in XML und fügen sie zu strings.xml hinzu:

<string-array name="v1to2">
  <item>
    alter table artists add column band char(1);
  </item>
  <item>
    update artists set band=\'Y\';
  </item>
</string-array>

Beim nächsten Start der Anwendung wird zwar nicht die onCreate-Methode aufgerufen, dafür aber die Methode onUpgrade, die jetzt wie folgt aussieht:

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  if(oldVersion==1 && newVersion==2)
    for(String sql : context.getResources().getStringArray(R.array.v1to2))
      db.execSQL(sql);
  else
    System.out.println("onUpgrade called - version not handled");
}

Diese defensive Programmierweise gewährleistet, dass bei jedem Versionswechsel entweder eine Änderung an der Datenbank ausgeführt oder eine entsprechende Warnung im Protokoll verzeichnet wird. Besser wäre hier sicher die Nutzung der Klasse Log, die den Eintrag noch mit einem entsprechenden Tag versieht, doch sind dies Details, die in diesem Kapitel keine übergeordnete Rolle spielen. Wenn wir die Protokolle gewissenhaft auswerten, entgeht uns jedenfalls nichts. Möglicherweise ist es sogar angemessener, eine Ausnahme zu machen, wenn nicht mit einem bestimmten Versionswechsel gerechnet wird.

Cursor

Weiter oben haben wir ja bereits gesehen, dass wir etwa innerhalb einer Activity mit

SQLiteOpenHelper database = new SongDatabaseHelper(this);
SQLiteDatabase connection = database.getWritableDatabase();

eine Verbindung zur Datenbank bekommen. Wenn wir nicht die Absicht haben, Daten zu verändern, können wir die letzte Zeile auch durch die defensivere Variante

SQLiteDatabase connection = database.getReadableDatabase();

ersetzen. Genauso wie in der onUpdate-Methode unserer SongDatabaseHelper-Klasse können wir jetzt SQL-Anweisungen gegen die Datenbank laufen lassen.

connection.execSQL("insert into artists(name) values ('The Rolling Stones')")

Dieses Mal haben wir die Anweisung der Einfachheit halber nicht in strings.xml eingetragen. Ganz ähnlich können wir auch Datensätze mit update ändern oder mit der SQL-Anweisung delete löschen. Grundsätzlich können wir der Methode execSQL auch select-Anweisungen übergeben, doch werden wir daraus keinen Nutzen ziehen: Da die Methode void als Rückgabetyp hat, erfahren wir nicht, welche Daten der select gefunden hat. Für Abfragen gibt es etwa die Methode rawQuery[12] mit der folgenden Signatur:

public Cursor rawQuery (String sql, String[] selectionArgs)

Die select-Anweisung wird einfach als Text übergeben. Zusätzlich haben wir auch die Möglichkeit, mit Platzhaltern zu arbeiten und diese dann über den zweiten Parameter mit Werten zu versorgen. Doch dazu später mehr.

Zunächst interessiert uns der Rückgabetyp. Die Ergebnisse unserer Abfrage werden uns als Cursor[13] zurückgegeben. Ein Cursor ist eine listenartige Datenstruktur, aus der wir alle gefundenen Datensätze abrufen können. In der einfachsten Form sieht das dann so aus:

Cursor result=connection.rawQuery("select name from artists", null);
String s="";
while(result.moveToNext ())
    s+=result.getString(0)+"\n";
Toast.makeText(this, s, Toast.LENGTH_LONG).show();

Die App blendet einen Toast mit allen Interpreten ein, die in unserer Datenbank verzeichnet sind. Mit Hilfe von Methoden wie moveToNext[14] und moveToPrevious[15] können wir uns in der Ergebnismenge der select-Anweisung bewegen.

Mit Methoden wie getString[16] kopieren wir Daten aus dem Cursor in unsere eigenen Variablen. Wie der Cursor das macht, ist für uns als Anwender transparent. Im Idealfall fordert er von SQLite nur einige Datensätze an. Diese bilden einen Ausschnitt, in dem wir uns bewegen. Sobald die Grenzen diese Teilmenge überschritten werden, fordert der Cursor neue Daten vom Datenbanksystem – und das alles ohne, dass wir etwas davon merken. Neben getString gibt es für einige Standard-Datentypen eine eigene Methode. Für Zahlen vom Typ int gibt es etwa getInt,[17] für solche vom Typ float gibt es entsprechend getFloat.[18]

Das Argument ist bei jeder dieser Methoden der Spaltenindex. Der Index 0 bezieht sich dabei auf die erste Spalte in der Ergebnismenge und 1 auf die zweite Spalte. Der Leser sollte sich mit Hilfe der Dokumentation zum Typ Cursor einen Überblick über dessen vielfältige Methoden verschaffen.

Insgesamt hat es sich als eine gute Praxis erwiesen, die Daten dort zu belassen, wo sie sind. Oft wird der Fehler gemacht, die Daten aus dem Cursor in „eigene“ Datenstrukturen – vorzugsweise Arrays – zu kopieren. Doch sind Cursor gerade für solche Ergebnismengen von select-Anweisungen entwickelt worden und sollten auch von uns dazu genutzt werden.

Eine weitere gute Praxis besteht darin, Ressourcen, die wir angefordert haben, auch wieder freizugeben. In Java haben wir die Garbage-Collection, die ja hinter uns herräumt und etwa nicht benötigten Speicherplatz freigibt. Wir müssen uns darum nicht kümmern. Die Welt außerhalb von Java kennt aber vielfach keine Garbage-Collection. Wenn wir also Datenbankverbindungen angefordert haben, sollten wie sie nach getaner Arbeit auch wieder schließen, um zu vermeiden, dass SQLite Ressourcen für die Verbindung bunkert:

connection.close();

Nicht nur der Typ SQLiteDatabase hat eine close-Methode,[19] sondern auch andere Typen aus dem Paket android.database. Und dazu gehört auch Cursor. Wenn wir den Cursor aus dem Beispiel nicht mehr brauchen, teilen wir das SQLite über die folgende Anweisung mit:

result.close();

Prepared Statements

Wenn eine SQL-Anweisung bei SQLite eintrifft, wird es einer echten Rosskur unterzogen:

  • Die Syntax wird geprüft.
  • Es wird überprüft, ob die Tabellen und Spalten aus der Anweisung überhaupt existieren und
  • ob der Benutzer überhaupt berechtigt ist, auf sie zuzugreifen.
  • Es wird optimiert.
  • Es wird in eine ausführbare Form gebracht
  • … und schließlich ausgeführt.

Und das passiert bei jedem execSQL aufs Neue. Dabei hätte es gereicht, die ersten fünf Schritte ein einziges Mal je Anweisung auszuführen und dann den Befehl in seiner ausführbaren Form wieder zu verwenden. Und genau das geht mit Hilfe des Typs SQLiteStatement. Die Methode

public SQLiteStatement compileStatement (String sql)

aus der Klasse SQLiteDatabase bringt eine als Text vorliegende SQL-Anweisung in seine ausführbare Form. Objekte vom Typ SQLiteStatement haben wiederum eine parameterfreie Methode namens execute[20] zum Ausführen der SQL-Anweisung. Insgesamt ergibt sich so

String sql="insert into artists(name) values('Beach Boys')";
SQLiteStatement insert=connection.compileStatement(sql);
for(int i=0; i<10; i++)
    insert.execute();
insert.close();

Die insert-Anweisung wird einmal „präpariert“ und zehnmal ausgeführt. Die Verwendung von Prepared Statements stellt in der Entwicklung von Enterprise-Anwendungen, in denen sekündlich hunderte, wenn nicht gar tausende von Anweisungen beim Datenbanksystem eintreffen, eine ganz zentrale Tuning-Technik dar. Da in Android-Anwendungen typischerweise nur vergleichsweise wenig Datenbankaktivität stattfindet, spielen Prepared Statements hier eine nicht ganz so zentrale Rolle.

Unserem Beispiel mangelt es etwas an Dynamik: Kein Mensch fügt zehnmal einen Datensatz ein, der im Wesentlichen das Gleiche enthält. Wenn wir aber jedes Mal einen anderen Künstler wählen, müssen wir auch immer wieder aufs Neue präparieren und unser ohnehin sehr bescheidener Tuning-Effekt wäre ganz hinüber. Genau hier greifen Platzhalter. Wenn wir die insert-Anweisung folgendermaßen formulieren:

String sql="insert into artists(name) values(?)";

können wir mit der Methode bindString dem Platzhalter '?' neue Werte zuweisen, ohne die Anweisung neu zu präparieren. Im Idealfall wird eine Anweisung nur ein einziges Mal präpariert und dann immer wieder recycelt:

String[] artists={"The Who", "Jimi Hendrix", "Janis Joplin", "The Doors"};
String sql="insert into artists(name) values(?)";
SQLiteStatement insert=connection.compileStatement(sql);
for(String s : artists){
    insert.bindString(1, s);
    insert.execute();
}
insert.close();

SQL häppchenweise

Mit der Methode rawQuery können wir eine SQL-Anweisung in Textform zu SQLite schicken. Es besteht aber auch die Möglichkeit, die Anweisung in ihre Bestandteile zu zerlegen und diese dann mit der Methode query[21] wegzuschicken. Das Datenbanksystem baut die Teile wieder zu einer select-Anweisung zusammen und führt sie aus. Wir wollen uns das mal anhand der folgenden etwa komplexeren Abfrage klarmachen:

select artist, count(title)
from songs where id>1
group by artist
having count(*)>1
order by id", null)

In dieser Form würden wir die Anweisung an rawQuery übergeben. Die Zerlegung sehen wir an dem folgenden äquivalenten Aufruf der Methode query:

Cursor result =connection.query(
  "songs", new String[]{"artist","count(title)"},
  "id>?", new String[]{"1"}, "artist", "count(*)>1", "id"
);

Welche der beiden Varianten man bevorzugt, ist allein eine Frage des Programmierstils. Aus technischer Sicht sind beide Methoden gleichberechtigt.

Einzelnachweise

  1. www.sqlite.org/
  2. http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html
  3. http://www.oracle.com/technetwork/java/javase/jdbc/index.html#corespec40
  4. http://developer.android.com/reference/android/R.html
  5. http://developer.android.com/reference/android/content/Context.html#getResources()
  6. http://developer.android.com/reference/android/content/res/Resources.html
  7. http://developer.android.com/reference/android/content/res/Resources.html#getStringArray(int)
  8. http://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper.html
  9. http://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper.html#onCreate(android.database.sqlite.SQLiteDatabase)
  10. http://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper.html#onUpgrade(android.database.sqlite.SQLiteDatabase,%20int,%20int)
  11. http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#execSQL(java.lang.String)
  12. http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#rawQuery(java.lang.String,%20java.lang.String[])
  13. http://developer.android.com/reference/android/database/Cursor.html
  14. http://developer.android.com/reference/android/database/Cursor.html#moveToNext()
  15. http://developer.android.com/reference/android/database/Cursor.html#moveToPrevious()
  16. http://developer.android.com/reference/android/database/Cursor.html#getString(int)
  17. http://developer.android.com/reference/android/database/Cursor.html#getInt(int)
  18. http://developer.android.com/reference/android/database/Cursor.html#getFloat(int)
  19. http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#close()
  20. http://developer.android.com/reference/android/database/sqlite/SQLiteStatement.html#execute()
  21. http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#query(java.lang.String,%20java.lang.String%5B%5D,%20java.lang.String,%20java.lang.String%5B%5D,%20java.lang.String,%20java.lang.String,%20java.lang.String,%20java.lang.String)

Positionsbestimmung

Zurück zu Googles Android


Eine Vielzahl der heutigen Smartphone Apps kommt ohne die Bestimmung der eigenen Position nicht aus. Aus diesem Grund bietet Android eine einfache Schnittstelle, um diese Daten auszulesen und zu verwenden. Wie man die Position eines Android-Smartphones bestimmt, werden wir in diesem Kapitel kennen lernen.

Lokalisierung über GPS und Netzwerk

Die Bestimmung der Position über GPS oder über das Netzwerk unterscheidet sich im Androidsystem nur hinsichtlich der Genauigkeit. Über beide Varianten erhalten wir Geokoordinaten (Latitude & Longitude). GPS-Daten sind dabei bis auf einige Meter genau, Daten über das Netzwerk können durchaus eine Toleranz von bis zu 1km haben, je nachdem wie weit man sich vom nächsten Router befindet, der seine Geokoordinaten kennt.

Wie man nun diese Koordinaten in Android erhält, sehen wir jetzt.

Code

Zunächst muss man sich den LocationManager[1] vom Androidsystem holen, der für das Bereitstellen der Positionsdaten verantwortlich ist.

LocationManager locationManager = (LocationManager) this.getSystemService(LOCATION_SERVICE);

Um auch immer die aktuellsten Geodaten zu bekommen, braucht es noch einen LocationListener[2] der auf die Aktivitäten des LocationManagers reagiert:

LocationListener locationListener = new LocationListener() {

	// Wird Aufgerufen, wenn eine neue Position durch den LocationProvider bestimmt wurde
	public void onLocationChanged(Location location) {
		
		TextView tv = (TextView) this.findViewById(R.id.Latitude);
		tv.setText(String.valueOf(location.getLatitude()));
		
		...
    }

    public void onStatusChanged(String provider, int status, Bundle extras) {...}

    public void onProviderEnabled(String provider) {...}

    public void onProviderDisabled(String provider) {...}
  };

Als Letztes muss das Bestimmen der Position durch den LocationManager gestartet und dabei der Listener und der gewünschte LocationProvider[3] dem Manager übergeben werden.

locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListener);

Wenn man nicht die Position über das Netzwerk, sondern über GPS erhalten möchte, muss in requestLocationUpdates()[4] anstatt von LocationManager.NETWORK_PROVIDER der LocationManager.GPS_PROVIDER gesetzt werden.

Permissions

Damit der obige Code auch genutzt werden kann, müssen noch zwei Permissions im Manifest-Files gesetzt werden.

Für die Positionierung über das Netzwerk benötigen wir:

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

Und für die Positionierung per GPS:

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

Diese einfachen Schritte sind alles was man braucht, um in einer Applikation die Position eines Smartphones zu bestimmen.

Bestimmung einer Adresse

Mit Android ist es sogar möglich, sich aus den Geokoordinaten spezielle Ortsinformationen zu erhalten. Dazu benötigen wir eine Instanz des GeoCoder.[5]

Geocoder gc = new Geocoder(this);
		
List<Address> addressList=null;

try {
  addressList = gc.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
} catch (IOException e) {
  ...
}				
Address address = addressList.get(0);
System.out.println("Address: "+ address.getThoroughfare()+ " " + address.getSubThoroughfare());

Durch die Methode getFromLocation()[6] erhält man eine Liste von Adressen, die mit der Geokoordinate in Verbindung stehen. Die maximale Anzahl der Adressen können dabei begrenzt werden. Aus den Ergebnissen lassen sich Informationen wie Land, Postleitzahl, Stadt, Straße, Straßennummer etc. gewinnen.

Einzelnachweis

  1. LocationManager: http://developer.android.com/reference/android/location/LocationManager.html. Stand 16. Februar 2011
  2. LocationListener: http://developer.android.com/reference/android/location/LocationListener.html. Stand 16. Februar 2011
  3. LocationProviderhttp://developer.android.com/reference/android/location/LocationProvider.html. Stand 16. Februar 2011
  4. requestLocationUpdates(): http://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates%28java.lang.String,%20long,%20float,%20android.app.PendingIntent%29. Stand 16. Februar 2011
  5. GeoCoder: http://developer.android.com/reference/android/location/Geocoder.html. Stand 16. Februar 2011
  6. getFromLocation(): http://developer.android.com/reference/android/location/Geocoder.html#getFromLocation%28double,%20double,%20int%29. Stand 16. Februar 2011

HTTP

Zurück zu Googles Android


Viele Anwendungen bauen auf dem Client-Server-Prinzip auf, welche über das Internet Verbindung zueinander aufnehmen. Zu diesem Zweck wird meist HTTP als Übertragungsmittel genutzt. Wie man seine Daten in einer Android-Applikation via HTTP versendet, lernen wir in diesem Kapitel kennen.

Eine Voraussetzung hierfür ist, dass man über die grundlegenden Mechaniken von HTTP im Bilde ist.

Request-Methode

Je nachdem wie bzw. welche Informationen von einer URL oder Server bekommen möchte, nutzt man bei HTTP verschiedene Request-Methoden. Die meist genutzten sind GET, POST, PUT und DELETE. Und für diese gibt es in Android eine jeweilige Entsprechung: HttpGet,[1] HttpPost,[2] HttpPut[3] und HttpDelete.[4]

HttpGet get = new HttpGet("http://www.example.com/");
...
HttpPost post = new HttpPost("http://www.example.com/");
...
HttpPut put = new HttpPut("http://www.example.com/");
...
HttpDelete delete = new HttpDelete("http://www.example.com/");
...

Diesen Objekten können weitere Informationen wie Header durch addHeader()[5] hinzugefügt werden … ganz wie man es von HTTP gewohnt ist.

Request und Response

Nachdem das Request-Päckchen gepackt ist, will es auch verschickt werden und man möchte gegebenenfalls auch eine Antwort erhalten. Zu diesem Zweck benötigen wir ein DefaultHttpClient[6]-Objekt, welches die Aufgabe des Senden und Empfangen übernimmt.


HttpGet get = new HttpGet("http://www.example.com/");

DefaultHttpClient client = new DefaultHttpClient();
        
try {
	
	HttpResponse response = client.execute(get);
			
} catch (ClientProtocolException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
}

Durch die execute()[7]-Methode wird der Request ausgeführt und an die angegebene URL geschickt. Als Antwort erhält man ein Response-Objekt, welches die gewollten Informationen enthält. Es können Header und/oder Body-Informationen ausgelesen und genutzt werden.

Einzelnachweise

  1. HttpGet: http://developer.android.com/reference/org/apache/http/client/methods/HttpGet.html. Stand 18. Februar 2011
  2. HttpPost: http://developer.android.com/reference/org/apache/http/client/methods/HttpPost.html. Stand 18. Februar 2011
  3. HttpPut: http://developer.android.com/reference/org/apache/http/client/methods/HttpPut.html. Stand 18. Februar 2011
  4. HttpDelete: http://developer.android.com/reference/org/apache/http/client/methods/HttpDelete.html. Stand 18. Februar 2011
  5. http://developer.android.com/reference/org/apache/http/message/AbstractHttpMessage.html#addHeader%28org.apache.http.Header%29. Stand 18. Februar 2011
  6. http://developer.android.com/reference/org/apache/http/impl/client/DefaultHttpClient.html. Stand 18. Februar 2011
  7. http://developer.android.com/reference/org/apache/http/impl/client/AbstractHttpClient.html#execute%28org.apache.http.client.methods.HttpUriRequest%29. Stand 18. Februar 2011

Bluetooth

Zurück zu Googles Android


Erlaubnis (Permission)

Wie auch bei vielen andere Systemen in Android, kann Bluetooth in einer Anwendung nur genutzt werden, wenn die entsprechenden Erlaubnisse im Manifest gesetzt wurden. Für die Nutzung von Bluetooth werden zwei Erlaubnisse gebraucht:

  • BLUETOOTH: Diese erlaubt es die grundlegendsten Funktionen der Bluetooth-Kommunikation zu nutzen. Darunter fallen das Anfragen einer Verbindung, das Akzeptieren einer eingehenden Verbindung und die Datenübertragung.
  • BLUETOOTH_ADMIN: Durch diese ist es möglich, eine Suche nach anderen Bluetooth-Geräten zu starten oder die Bluetooth-Einstellungen zu verändern.
<manifest ... >
  ...
  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  ...
</manifest>

Sind Sie da?

Auch wenn man davon ausgehen kann, dass die allermeisten (Android-)Smartphones über Bluetooth verfügen, sollte man jedoch als guter Entwickler immer testen, ob ein zu benutzender Dienst auch existiert. Das in Android zu überprüfen, ist simpel ab API-Level 5:

BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    // Das Gerät verfügt über kein Bluetooth.
}

Um zu testen, ob das Smartphone über Bluetooth verfügt, holen wir uns vom System den BluetoothAdapter[1] über die Methode getDefaultAdapter().[2] Dieser BluetoothAdapter stellt die Repräsentation des eigenen Bluetooth-Gerätes dar. Wenn man als Rückgabewert ein NULL erhält, besitzt das Gerät kein Bluetooth.

Wenn das Gerät über Bluetooth verfügt, muss sichergestellt werden, dass es auch zum Zeitpunkt der Nutzung aktiviert ist. Die Aktivierung muss aber nochmals explizit durch den Nutzer bestätigt werden.

if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

Wenn Bluetooth noch nicht aktiv ist, dann wird ein Intent mit der BluetoothAdapter.ACTION_REQUEST_ENABLE-Aktion erzeugt, welche per startActivityForResult()[3]-Methode ins System abgesetzt wird. Dadurch öffnet sich ein Dialog, welcher den Nutzer fragt, ob Bluetooth aktiviert werden soll oder nicht. Durch startActivityForResult() wird nach Abarbeitung des Intents bzw. die Antwort des Nutzers, die Callback-Methode onActivityResult()[4] aufgerufen. Dieser ResultCode muss allerdings zunächst definiert werden. [5]

private final static int REQUEST_ENABLE_BT = 123;

Durch Auslesen des ResultCodes kann abgelesen werden, ob die Aktivierung erfolgreich war oder nicht.

public void onActivityResult(int requestCode, int resultCode, Intent data) {
   if (resultCode == Activity.RESULT_OK) {
      // Bluetooth ist aktiv.
   } else {
      // Bluetooth wurde nicht aktiviert.
   }
}

Andere Geräte aufspüren

Um eine Verbindung zu einem anderen Bluetooth-Gerät herzustellen, ist es zunächst notwendig, dass man nach allen in der Nähe befindlichen Geräten sucht und von diesen Informationen abruft. Diese Informationen beinhalten den Gerätenamen, dessen Klasse und seine MAC-Adresse. Anhand dieser Informationen ist es dann möglich, sich mit einem anderen Gerät zu „paaren“ (vom engl. Begriff pairing), das heißt diese Geräte tauschen zum Zweck der Authentifizierung und Verschlüsselung einen Schlüssel aus. Alle mit dem eigenen Gerät gepaarten Geräte werden gespeichert und können durch die Bluetooth-API jederzeit abgerufen werden. So kann man anhand der MAC-Adresse jederzeit eine Verbindung zu einem Gerät herstellen ohne eine neue Suche anstoßen zu müssen. Voraussetzung dafür ist natürlich, dass sich das gewünschte Gerät in Reichweite befindet.

Gepaarte Geräte

Wenn man eine Verbindung zu einem bereits bekannten Gerät aufbauen möchte, muss man keine neue Suche nach anderen Geräten starten, da man alle Informationen auf dem eigenen Gerät finden kann.

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// wenn gepaarte Geräte existieren
if (pairedDevices.size() > 0) {
    // Durchgehen der gepaarten Geräte
    for (BluetoothDevice device : pairedDevices) {
        	// einem Array die Adresse und den Namen der Geräte hinzufügen
        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
    }
}

Durch die Methode getBondedDevices()[6] wird die Liste aller gespeicherten und gepaarten Geräte abgerufen.

Neue Geräte suchen

Wenn man eine Verbindung zu einem dem eigenen Gerät noch unbekanntem Gerät herstellen will, muss man zunächst nach diesem Gerät suchen. Dies erreicht man durch den Aufruf der Methode startDiscovery().[7] Der Aufruf dieser Methode löst eine asynchrone Suche aus. Nach Beendigung dieser Suche, wird durch das Aussenden der Aktion ACTION_FOUND in das System das Ergebnis dem System zugänglich gemacht und kann abgefragt werden. Zu diesem Zweck benötigt man einen BroadcastReceiver,[8] der auf die Beendigung der Suche reagiert.

// BroadcastReceiver für ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        // wenn durch die Suche ein Gerät gefunden wurde
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
	    // das Bluetooth-Gerät aus dem Intent holen
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            // Hinzufügen des Namens und der Adresse in ein Array
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
};

// den BroadcastReceiver im System registrieren
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);

Sich sichtbar machen

Um sein Gerät für andere sichtbar zu machen einfach den folgenden Code kopieren.[9]

Intent sichtbarMachen = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
startActivity(sichtbarMachen);

Geräte verbinden

Um die Verbindung zwischen zwei Geräten herzustellen, benötigt eine Anwendung jeweils eine Server- und Client-Implementierung. Wie bei diesem Modell üblich öffnet der Server eine Verbindung und wartet, bis diese von einem Client genutzt wird. Ziel beider Seiten ist es, einen BluetoothSocket[10] zu öffnen, auf welchem Daten zwischen den beiden Seiten übertragen werden können.

Server

Um auf dem Server die Verbindung zustande zu bringen, sind folgende Schritte zu beachten:

1. Durch den Aufruf der Methode listenUsingRfcommWithServiceRecord(String, UUID)[11] wird ein BluetoothServerSocket[12] geöffnet. listenUsingRfcommWithServiceRecord() schreibt einen Eintrag ins Service Discovery Protocol (SDP). Dabei wird ein Name und eine UUID für den eigenen Service benötigt. Dieser Name ist beliebig. Die UUID muss auf der Server- und der Client-Seite identisch sein, damit eine Verbindung zustande kommen kann.

2. Auf eine eingehende Verbindung warten

Durch den Aufruf von accept()[13] wartet der Server auf eine eingehende Verbindung und gibt ein BluetoothSocket-Objekt zurück. Der Aufruf von accept() blockiert den Thread bis zu dieser Verbindung, weshalb er nicht im UI-Thread der Anwendung laufen sollte.

3. Schließen des BluetoothServerSockets

Nach dem Erhalt des BluetoothSockets muss der BluetoothServerSocket durch den Aufruf von close()[14] geschlossen werden. Dies gibt den BluetoothServerSocket und all seine Ressourcen frei, schließt dabei aber nicht den BluetoothSocket. Dieser kann weiter für die Datenübertragung genutzt werden.

Beispiel-Implementierung für die Server-Seite:

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;

    public AcceptThread() {
        // Use a temporary object that is later assigned to mmServerSocket,
        // because mmServerSocket is final.
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID is the app’s UUID string, also used by the client code.
            tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) { }
        mmServerSocket = tmp;
    }

    public void run() {
        BluetoothSocket socket = null;
        // Keep listening until exception occurs or a socket is returned.
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            // if a connection was accepted
            if (socket != null) {
                // Do work to manage the connection (in a separate thread).
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }

    /** Will cancel the listening socket, and cause the thread to finish. */
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}

Client

Für die Verbindung zum Server muss der Client folgende Schritte durchlaufen:

1. Erstellen eines BluetoothSockets zum gewählten Gerät

Durch den Aufruf von createRfcommSocketToServiceRecord(UUID) auf dem gewählten Gerät, erhalten wir ein BluetoothSocket-Objekt. Die verwendete UUID muss dieselbe sein wie auf der Server-Seite.

2. Verbindung aufbauen

Durch den Aufruf von connect() auf dem BluetoothSocket versucht das System, eine Verbindung zum Socket auf dem Server herzustellen. Ist dies von Erfolg gekrönt, kann zwischen beiden eine Kommunikation stattfinden.


Beispiel-Implementierung für die Client-Seite:

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;

    public ConnectThread(BluetoothDevice device) {
        // Use a temporary object that is later assigned to mmSocket,
        // because mmSocket is final.
        BluetoothSocket tmp = null;
        mmDevice = device;

        // Get a BluetoothSocket to connect with the given BluetoothDevice.
        try {
            // MY_UUID is the app’s UUID string, also used by the server code.
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }

    public void run() {
        // Cancel discovery because it will slow down the connection.
        mAdapter.cancelDiscovery();

        try {
            // Connect the device through the socket. This will block
            // until it succeeds or throws an exception.
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and get out.
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }

        // Do work to manage the connection (in a separate thread).
        manageConnectedSocket(mmSocket);
    }

    /** Will cancel an in-progress connection, and close the socket. */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

Daten-Handhabung

Der Aufbau einer Verbindung zwischen zwei Bluetooth-Geräten ist nur die eine Seite der Medaille. Nach dem Aufbau dieser Verbindung muss dann auch noch der Austausch der Daten bewältigt werden. Bei der Übertragung der Daten kommen Input- und Outputstreams zum Einsatz.

Beispiel-Implementierung für die Handhabung der Daten:

private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;

    public ConnectedThread(BluetoothSocket socket) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;

        // Get the input and output streams, using temp objects because
        // member streams are final.
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }

        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }

    public void run() {
        byte[] buffer = new byte[1024];  // buffer store for the stream
        int bytes; // bytes returned from read()

        // Keep listening to the InputStream until an exception occurs.
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // Send the obtained bytes to the UI Activity.
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }

    /* Call this from the main Activity to send data to the remote device. */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }

    /* Call this from the main Activity to shutdown the connection. */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

Einzelnachweise

  1. BluetoothAdapter: http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html. Stand 24. Februar 2011.
  2. http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#getDefaultAdapter%28%29. Stand 24. Februar 2011.
  3. http://developer.android.com/reference/android/app/Activity.html#startActivityForResult%28android.content.Intent,%20int%29. Stand 24. Februar 2011.
  4. http://developer.android.com/reference/android/app/Activity.html#startActivityForResult%28android.content.Intent,%20int%29. Stand 24. Februar 2011.
  5. http://stackoverflow.com/questions/8188277/error-checking-if-bluetooth-is-enabled-in-android-request-enable-bt-cannot-be-r
  6. http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#getBondedDevices%28%29, Stand 25. Februar 2011.
  7. http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#startDiscovery%28%29. Stand 25. Februar 2011.
  8. BroadcastReceiver: http://developer.android.com/reference/android/content/BroadcastReceiver.html. Stand 25. Februar 2011.
  9. https://androidcookbook.com/Recipe.seam?recipeId=1978&recipeFrom=home Stand 03. März 2016.
  10. BluetoothSocket:http://developer.android.com/reference/android/bluetooth/BluetoothSocket.html, Stand 25. Februar 2011.
  11. http://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#listenUsingRfcommWithServiceRecord%28java.lang.String,%20java.util.UUID%29. Stand 25. Februar 2011.
  12. BluetoothServerSocket: http://developer.android.com/reference/android/bluetooth/BluetoothServerSocket.html. Stand 25. Februar 2011.
  13. http://developer.android.com/reference/android/bluetooth/BluetoothServerSocket.html#accept%28%29. Stand 25. Feb. 2011
  14. http://developer.android.com/reference/android/bluetooth/BluetoothServerSocket.html#close%28%29. Stand 25. Februar 2011.

TCP-Sockets

Zurück zu Googles Android


Die erste Frage, die sich bei der Betrachtung von TCP bei Android stellt, ist: Haben Sie schon mal Sockets in Java programmiert? Wenn die Antwort „Ja“ lautet, dann herzlichen Glückwunsch. Sie brauchen sich das Kapitel über TCP-Sockets in Android nicht anschauen, da Android exakt die gleichen Java-Klassen und Strukturen zum Erzeugen von Sockets benutzt und sie somit schon alles kennen.

Da dieses Buch aber so vollständig wie möglich sein soll, möchten wir nicht so sein und erklären hier, wie Sie TCP-Sockets auf der Client- und der Server-Seite in Android implementieren können, um eine Verbindung zwischen Geräten herstellen zu können.

Client


Socket client;

try{
   client = new Socket("www.example.com", 4321);
   out = new PrintWriter(client.getOutputStream(),true);
   in = new BufferedReader(new InputStreamReader(client.getInputStream()));

} catch(UnknownHostException e) {
   System.out.println("Unknown host: www.example.com");

} catch(IOException e) {
   System.out.println("No I/O");
}

Um einen Socket auf der Client-Seite zu öffnen wird einfach ein Socket[1]-Objekt erstellt. Diesem wird die Server-Adresse in Form einer URL oder auch einer IP-Adresse und die Portnummer, auf der der Socket arbeitet, übergeben. Sollte auf der angegebenen Adresse bzw. dem Port kein anderer Socket vorhanden sein, wird eine UnkownHostException[2] ausgegeben.

Wenn der Socket auf der anderen Seite existiert, dann lassen sich von diesem der Input- und Outputstream bestimmen, um auf dem Socket Daten zu senden und zu empfangen.

Server

ServerSocket server;

try{
   server = new ServerSocket(4321); 
} catch (IOException e) {
   System.out.println("Could not listen on port 4321");
}

Socket client;

try{
   client = server.accept();
} catch (IOException e) {
   System.out.println("Accept failed: 4321");
}

try{
   in = new BufferedReader(new InputStreamReader(client.getInputStream()));
   out = new PrintWriter(client.getOutputStream(),true);
} catch (IOException e) {
   System.out.println("Read failed");
}

Das Manifest

Im Manifest wird dann nur noch folgender Eintrag benötigt, um die Berechtigung zu erhalten, Netzwerk übergreifende Dienste nutzen zu können:

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

Besonderheiten Emulatorbetrieb

Fest definierte Emulator-IP-Adressen[3]
Adresse Funktion
127.0.0.1 Abhängig davon, in welchem Netzwerk ein Programm läuft (Emulator oder Rechner)
10.0.2.1 IP-Adresse des Routers (Gateway-Adresse)
10.0.2.2 IP-Adresse des Rechners, auf dem der Emulator läuft (Entwicklungsrechner)
10.0.2.15 IP-Adresse des Emulators
10.0.2.3 Erster DNS-Server des Routers
10.0.2.4–6 Weitere DNS-Server des Routers


Für den Socket auf der Server-Seite ist ein Schritt mehr zu machen als auf der Client-Seite. So muss zuerst ein ServerSocket[4]-Objekt erstellt werden, welcher auf dem gewünschten Port arbeitet. Dieser ServerSocket wartet durch den Aufruf der Methode accept()[5] auf eine eingehende Verbindung eines anderen Sockets. Kommt so eine Verbindung zustande, gibt accept() ein Socket-Objekt zurück, auf dem man, wie im Client-Abschnitt, arbeiten kann. Das heißt, dass man durch den Abruf der Input- und Outputstreams Daten zwischen den beiden Sockets senden kann.

Einzelnachweis

  1. Socket: http://developer.android.com/reference/java/net/Socket.html. Stand 05. März 2011
  2. UnkownHostException: http://developer.android.com/reference/java/net/UnknownHostException.html. Stand 05. März 2011
  3. Android Grundlagen und Programmierung, 1. Auflage 2009, dpunkt.verlag GmbH, ISBN 978-3-89864-574-4.
  4. ServerSocket: http://developer.android.com/reference/java/net/ServerSocket.html. Stand 05. Stand 2011
  5. http://developer.android.com/reference/java/net/ServerSocket.html#accept%28%29. Stand 05. März 2011

Spezielle Activities


MapActivity

Zurück zu Googles Android


Neben der eigenständigen „Google Maps“-Anwendung, welche auf Android-Geräten zu finden ist, besteht die Möglichkeit, auch in der eigenen Anwendung Kartenmaterial von Google bereitzustellen. Zu diesem Zweck können Sie Activities als MapActivities implementieren, die genau diese Aufgabe erfüllen. Wie man diese Funktion umsetzt, wird nun im Folgenden gezeigt.

Vorbereitung

Um Karten in Ihrer Anwendung zu benutzen, müssen einige Voraussetzungen geschaffen werden.

SDK mit Google-API

Da es sich bei der Bereitstellung von Karten um einen Dienst von Google handelt, benötigen Sie nebst dem Android-SDK auch die API von Google. Zu diesem Zweck existiert zu jeder SDK-Version auch eine SDK-Version, die mit der Google-API ausgestattet ist. Um an das SDK mit Google-API zu kommen, müssen Sie diese, analog zur normalen „SDK-Version“, über den „Android SDK and AVD Manager“ auswählen und installieren.

Google-API-Key

Um den Kartendienst zu nutzen, müssen Sie sich für diesen zusätzlich noch registrieren. Eine genaue Anleitung, wie Sie Ihren Maps-API-Key erhalten, finden Sie hier. Für unsere Zwecke reicht es, wenn Sie die Registrierung mit Ihrem „SDK Debug“-Zertifikat durchführen. Sollten Sie später Anwendungen schreiben, die ihren Weg in den Android-Markt finden und Sie sich selbst ein privates Zertifikat erstellen, dann müssen Sie mit diesem einen neuen API-Key generieren (vorausgesetzt, Ihre Anwendung benötigt diesen).

Hinweis: Unter Java7 wird standardmäßig der SHA1-Schlüssel angezeigt. Um den MD5 Schlüssel zu sehen einfach -v an den keystore-Befehl anhängen

MapActivity selbst

Um nun einmal die MapActivity in Aktion zu erleben, brauchen wir ein neues Projekt. Also auf auf. Eclipse geöffnet und ein neues Androidprojekt gestartet. Geben Sie Ihrer Start-Activity den Namen HelloMapView ein!

Manifest

Zunächst müssen ein paar Änderungen am Manifest vorgenommen werden. So müssen wir als erstes die Maps Library unserem Projekt hinzufügen, da diese nicht zur Standard Library von Android gehört. Dazu fügen Sie bitte die Zeile

<uses-library android:name="com.google.android.maps" />

dem Manifest als untergeordneten Tag des <application>-Tags hinzu.

Des Weiteren benötigt die MapActivity Zugriff zum Internet, um neues Kartenmaterial herunterzuladen. Mit der Zeile

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

als untergeordnetes Tag des <manifest>-Tags, erlauben wir der Anwendung die Kommunikation mit dem Internet.

Layout

Um später in der Activity die Karte einzublenden und mit dieser zu interagieren, müssen Sie im Layoutfile ein MapView-Tag anlegen, welches die Karte repräsentiert. Verändern Sie ihr main.xml, sodass es folgendem Code entspricht:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mainlayout"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <com.google.android.maps.MapView
        android:id="@+id/mapview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:clickable="true"
        android:apiKey="Your Maps API Key"
    />

</RelativeLayout>
  • android:apiKey – Für dieses Attribut geben Sie Ihren API-Key an, welches Sie dazu berechtigt, den Kartendienst von Google zu nutzen und die Daten herunterzuladen.

Activity

Um die Anwendung abzurunden, müssen wir jetzt noch die MapActivity etwas bearbeiten.


1. Öffnen Sie die HelloGoogleMaps Activity und erweitern Sie diese mit MapActivity[1]

public class HelloMapView extends MapActivity

Daraufhin müssen Sie mit Hilfe von Eclipse die benötigte Bibliothek importieren und die abstrakten Methoden der MapActivity einfügen. In diesem Fall wird nur die Methode boolean isRouteDisplayed() eingefügt. Diese wird genutzt, wenn Sie auf der Karte eine Route darstellen wollen. Da das bei uns nicht der Fall sein wird, geben wir false zurück.


2. In der onCreate()-Methode fügen Sie jetzt noch diese beiden Zeilen hinzu:

MapView mapView = (MapView) findViewById(R.id.mapview);
mapView.setBuiltInZoomControls(true);

Damit holen wir uns die Referenz auf unseren im Layoutfile definierten MapView.[2] Mit setBuiltInZoomControls(true) fügen wir dem MapView einen ZoomIn- und ZoomOut-Button hinzu. Ohne diese beiden könnten wir die Karte auf nur einem Detaillevel hin- und herverschieben.


Im Grunde genommen haben wir bereits jetzt eine kleine (nicht unbedingt umfangreiche) Maps-Anwendung gebaut. Wir können die Karte verschieben und in diese hin- oder herauszoomen. Nun wollen wir aber noch die Möglichkeit schaffen, unsere Position durch einen Marker auf der Karte zu verdeutlichen (Nach dem Motto: Das X markiert die Stelle).

Dazu benötigen wir in unserem Projekt eine neue Klasse.


3. Erstellen Sie die Klasse HelloItemizedOverlay. Wenn Sie die Klasse über den Eclipse-Dialog erstellen, dann geben Sie als Superklasse com.google.android.maps.ItemizedOverlay an[3] und machen Sie ein Häkchen bei Constructors from superclass. Anderfalls lassen Sie die neue Klasse von Hand von der Superklasse ItemizedOverlay erben. Achten Sie darauf, dass bei der Superklasse in den Pfeilen NICHT Item, sondern OverlayItem[4] steht. Nachdem Sie auch noch die abstrakten Methoden der Superklasse eingefügt haben, sollte die Klasse wie folgt aussehen:

public class HelloItemizedOverlay extends ItemizedOverlay<OverlayItem> {

	public HelloItemizedOverlay(Drawable defaultMarker) {
		super(defaultMarker);
		// TODO Auto-generated constructor stub
	}

	@Override
	protected OverlayItem createItem(int i) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public int size() {
		// TODO Auto-generated method stub
		return 0;
	}

}

Diese Klasse stellt eine zusätzliche Schicht dar, welche über die eigentliche Karte gelegt wird. Wir legen auf dieser Schicht unsere Marker ab, um von uns bestimmte Punkte besser hervorzuheben.


4. Als Nächstes braucht unsere Schicht eine Liste, in der sie die einzelnen Marker ablegen kann. Dafür erweitern wir die Klasse um ein ArrayList<OverlayItem>, in der wir die Marker speichern.

private ArrayList<OverlayItem> mOverlays = new ArrayList<OverlayItem>();


5. Später in der Anwendung wird die Position der Marker durch Geokoordinaten (Längen- und Breitengraden) angegeben. Damit sie nicht irgendwie auf dieser Position angezeigt werden, geben wir an, welche Stelle des Markers auf der Position liegt. In unserem Fall sagen wir, dass die Mitte der unteren Kante des Markers auf dem Geoposition liegt. Dazu erweitern Sie den Konstruktor um eine kleine statische Methode:

public HelloItemizedOverlay(Drawable defaultMarker) {
	super(boundCenterBottom(defaultMarker));
}


6. Nun müssen unsere Marker ja auch noch in das Overlay gebracht werden. Zu diesem Zwecks spendieren wir der Klasse eine weitere Methode:

public void addOverlay(OverlayItem overlay) {
    mOverlays.add(overlay);
    populate();
}

Mit addOverlay(OverlayItem overlay) fügen wir dem Overlay beziehungsweise der Liste einen weiteren Marker hinzu. Nachdem wir das gemacht haben, MÜSSEN wir die Methode populate() aufrufen, um bekannt zu geben, dass sich etwas an den zugrunde liegenden Daten beziehungsweise der Anzahl der Marker getan hat. In Folge des Aufrufs von populate() werden die Methoden size() und createItem(int i) unserer Klasse HelloItemizedOverlay aufgerufen. Dadurch werden die alten und auch der neue Marker auf dem Overlay neu dargestellt. Damit diese beiden Methoden richtig arbeiten, müssen sie noch an die Begebenheiten angepasst werden.

@Override
	protected OverlayItem createItem(int i) {
		return mOverlays.get(i);
	}

	@Override
	public int size() {
		return mOverlays.size();
	}

In beiden Fällen wird an unsere OverlayItem-Liste herangetreten, um die entsprechenden Informationen, die nötig sind, zu bekommen.

Damit sind wir mit der Arbeit an dieser Klasse für dieses Beispiel fertig. Als Letztes müssen wir in der Activity noch einige Veränderungen vornehmen, damit wir auch ein paar Marker auf die Karte bringen können.


7. Bevor wir zu den letzten Schritten im Programmcode kommen, brauchen wir noch die Grafik, die unseren Marker auf der Karte darstellt. Dazu kopieren Sie sich bitte die nebenstehende Abbildung 1 in den drawable-Ordner Ihres Projekts unter res/drawable-mdpi/ (res/drawable/, wenn Sie eine Android-Version unter 1.6 verwenden). ‎

8. Zunächst benötigt wir noch einige Klassenvariablen:

private List<Overlay> mapOverlays;		      // eine Liste mit allen Overlays des ''MapViews''
private Drawable drawable;				// das ''Drawable'' für unseren Marker
private HelloItemizedOverlay itemizedOverlay;		// unser Overlay

Welche dann in onCreate() instanziiert werden:

mapOverlays = mapView.getOverlays();	          // Damit wir später unser Overlay auf die Karte anwenden können, 
                                                     müssen wir uns vom ''MapView'' die Liste aller Overlays holen.
drawable = this.getResources().getDrawable(R.drawable.arrow_down); // Aus den Ressourcen holen wir uns unser Bild des Markers …
itemizedoverlay = new HelloItemizedOverlay(drawable);		   // … und setzten es als Defaultmarker in unseren Overlay.

Jetzt brauchen wir noch die Position, an der unser Marker auf der Karte angezeigt werden soll.

GeoPoint point = new GeoPoint(52457270,13526380);
OverlayItem overlayitem = new OverlayItem(point, "", "");

Dem GeoPoint[5] werden die Koordinaten als Längen- und Breitengraden angegeben. Zu beachten ist, dass die normalen Grade mit 10^6 mal genommen werden müssen. Bsp: Aus 52,45° werden 52450000. Diese Position wird einem OverlayItem als Parameter übergeben, damit dieses an der gewünschten Position angezeigt wird.

Als letzten Schritt fügen wir das gerade erstellte OverlayItem unserem Overlay[6] und dieses Overlay der Liste der Overlays des MapView hinzu.

itemizedOverlay.addOverlay(overlayitem);
mapOverlays.add(itemizedOverlay);


Wenn wir nichts vergessen haben, können Sie die Anwendung einmal laufen lassen. Sie können über die gesamte Weltkarte navigieren, hinein- und herauszoomen und Sie werden einen Marker finden, den Sie aber erst einmal finden müssen.

Einzelnachweise

  1. MapActivity: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/MapActivity.html, Stand 18. November 2010.
  2. MapView: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/MapView.html. Stand 18. November 2010.
  3. ItemizedOverlay: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/ItemizedOverlay.html. Stand 18. November 2010.
  4. OverlayItem: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/OverlayItem.html. Stand 18. November 2010.
  5. GeoPoint: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/GeoPoint.html, Stand 18. November 2010.
  6. Overlay: http://code.google.com/intl/de-DE/android/add-ons/google-apis/reference/com/google/android/maps/Overlay.html, Stand 18. November 2010.

ListActivity

Zurück zu Googles Android


Bei der Entwicklung von Android-Anwendungen werden Sie noch oft in die Lage kommen, bestimmte Inhalte als Liste anzeigen zu wollen oder zu müssen. Als kleiner Einstieg in die Welt der Listen soll dieses Kapitel dienen.

Einfache Darstellung

Bei einfachen Listen bestehen die einzelnen Listenelemente meist nur aus einem Eintrag bzw. einem String. Das heißt, jeder Eintrag besitzt nur eine Information wie einen Namen oder eine Bezeichnung. Für diesen Fall lässt sich die Darstellung in Android auch sehr einfach realisieren:

1. Wie immer erstellen wir ein neues Android-Projekt, welches wir diesmal SimpleListView nennen. Dabei nennen wir auch die StartActivity SimpleListView.

2. Öffnen Sie die HelloListView.java und erweitern sie mit ListActivity.[1]

public class SimpleListView extends ListActivity

3. Die onCreate()-Methode muss jetzt noch wie folgt abgeändert werden:

@Override
public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, CARS));
}

Wie Sie sehen können, haben wir diesmal keine setContentView()-Methode in onCreate(), da wir kein extra Layout für die Activity laden, sondern die setListAdapter()-Methode automatisch ein ListView der Activity hinzufügt. Der setListAdapter() wird dabei ein ArrayAdapter[2] übergeben, welcher einmal den Activity Context,[3] die Layoutbeschreibung für die Listenelemente und ein String-Array mit den anzuzeigenden Inhalten erhält. Als Layoutbeschreibung nehmen wir hier ein von Android vorgefertigtes Layout, das für die Darstellung von einzelnen Strings gut geeignet ist. Natürlich können auch komplexere Layouts für die einzelnen Listenelemente gebaut und verwendet werden. Wie das gemacht wird, sehen wir später in diesem Kapitel. Das String-Array zum Befüllen unserer Liste erstellen wir jetzt.

4. Fügen Sie das Array vor oder nach der onCreate()-Methode ein (wobei das reine Geschmackssache ist):

static final String[] CARS = new String[] {
	    "Audi", "Opel", "VW", "BMW", "FIAT", "Mercedes", "SEAT", "Ferrari", "Nissan"};

Damit haben wir eine erste einfache Liste in Android erstellt. Wie man aber nun komplexere Layouts mit mehreren Strings oder auch Bildern in die Liste bringt, sehen wir jetzt.

Komplexe Darstellungen

Um Listen mit komplexeren Layouts ausstatten zu können, benötigen wir zwei Dinge: 1. Ein eigenes Layout, welches die einzelnen Listenelemente beschreibt, und 2. einen eigenen ListAdapter.

Im Folgenden wird eine Listenanwendung entstehen, in der jedes Listenelement den Vor- und Nachnamen einer Person untereinander darstellt. Wir gehen wie immer vor und erstellen ein neues Androidprojekt, welchem wir den Namen AWListView geben.

Bevor wir zu unserem Layout und ListAdapter kommen, passen wir zunächst unsere StartActivity an und fügen unserem Projekt eine neue Klasse hinzu, welche die Vor- und Nachnamen repräsentiert.

public class AWListView extends ListActivity {
    
	private AWListAdapter mAdapter;
	
	private ArrayList<AWName> mData;
	
	/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        initiateData();
        
        mAdapter = new AWListAdapter(this, mData);
        this.setListAdapter(mAdapter);
        
    }
    
    private void initiateData(){
    	mData = new ArrayList<AWName>();
    	
    	AWName name = new AWName("Max", "Mustermann");
    	mData.add(name);
    	
    	name = new AWName("Hans", "Wurst");
    	mData.add(name);
    	
    	name = new AWName("Struwwel", "Peter");
    	mData.add(name);
    }
}

Unserer StartActivity geben wir den Namen AWListView, die schon wie beim SimpleListView durch ListActivity erweitert wird. Des Weiteren fügen wir der Activity zwei neue Attribute hinzu: Zum einen unseren eigenen ListAdapter (AWListAdapter) und eine ArrayList mit unseren Vor- und Nachnamen. In onCreate() befüllen wir zunächst unsere ArrayList mit initiateData() mit einigen Beispielnamen, damit in der Liste auch etwas angezeigt werden kann. Nach den Befüllen erstellen wir eine neue Instanz des ListAdapters und geben diesen an setListAdapater() weiter. Dadurch wird bei Start der Activity unsere Liste erzeugt und dargestellt. Bevor wir zum ListAdapter kommen, hier noch kurz, wie die Klasse AWName aufgebaut ist (da sie sehr simpel ist, spare ich mir weitere Kommentare):

public class AWName {
	
	private String mForename;
	private String mSurname;
	
	public AWName(String pForename, String pSurname){
		mForename = pForename;
		mSurname = pSurname;
	}
	
	public void setForename(String pForename) {
		mForename = pForename;
	}
	
	public String getForename() {
		return mForename;
	}
	
	public void setSurname(String pSurname) {
		mSurname = pSurname;
	}
	
	public String getSurname() {
		return mSurname;
	}

}

Da wir diesmal ein etwas komplexeres Layout haben wollen, reicht es nicht, sich ein von Android vorgefertigtes Layout wie im vorangegangenen Beispiel zu nehmen. Hierfür müssen wir wieder ein eigenes Layout definieren, welches wir row_layout.xml nennen:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

	<TextView
		android:id="@+id/row_forename"  
	    android:layout_width="fill_parent" 
	    android:layout_height="wrap_content" 
	    android:textSize="20dip"/>
	    
	<TextView
		android:id="@+id/row_surname"  
	    android:layout_width="fill_parent" 
	    android:layout_height="wrap_content" 
	    android:textSize="18dip"/>
	
</LinearLayout>

Im Grunde besteht unser Layout nur aus zwei TextViews, die untereinander angeordnet sind.

Da alle Vorbereitungen getroffen sind, können wir uns nun unserem ListAdapter zuwenden.

ListAdapter

Für unseren ListAdapter benötigen wir eine neue Klasse, welche wir AWListAdapter nennen. Diese erweitern wir durch die Klasse BaseAdapter.[4]

public class AWListAdapter extends BaseAdapter

Die Klasse BaseAdapter beherbergt einige abstrakte Methoden (die aus der Super-Klasse Adapter[5] stammen), welche wir im Anschluss noch importieren müssen. Zum Inhalt der einzelnen Methoden kommen wir später. Hier erstmal nur die Methodenköpfe.

@Override
public int getCount() {}

@Override
public Object getItem(int pPosition) {}

@Override
public long getItemId(int arg0) {}

@Override
public View getView(int pPosition, View convertView, ViewGroup parent) {}

Als Nächstes bekommt der Adapter noch zwei Attribute und seinen Konstruktor spendiert:

private ArrayList<AWName> mData = new ArrayList<AWName>();
	
private final LayoutInflater mLayoutInflater;

public AWListAdapter(Context pContext, ArrayList<AWName> pData){
		mData = pData;
		
		mLayoutInflater = (LayoutInflater) pContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}

Mit der ArrayList mData behalten wir unsere Vor- und Nachnamen, da wir diese im Verlauf des Aufbaus unserer ListViews benötigen. Des Weiteren erstellen wir einen LayoutInflater[6] der uns später aus unserem oben erstellten Layout das dazugehörige View-Objekt generiert, auf welchem wir dann arbeiten können.

Nun machen wir uns daran, die oben genannten abstrakten Methoden zu befüllen, damit diese auch einen Zweck erfüllen können.

@Override
public int getCount() {
	return mData.size();
}
  • getCount() gibt die Anzahl der Elemente zurück, die sich in der ArrayList befinden
@Override
public Object getItem(int pPosition) {
	return mData.get(pPosition);
}
  • getItem() gibt das Element an der gewünschten Stelle der ArrayList zurück
	
@Override
public long getItemId(int arg0) {
	return 0;
}
  • getItemId() gibt normalerweise die Reihen-ID zurück, welche mit dem Element an der Position in der ArrayList in Verbindung gebracht wird. Für unseren Zweck aber uninteressant.
	
@Override
public View getView(int pPosition, View convertView, ViewGroup parent) {
		
	if (convertView == null) {
		convertView = mLayoutInflater.inflate(R.layout.row_layout, null);
	}

	((TextView) convertView.findViewById(R.id.row_forename)).setText(((AWName)getItem(pPosition)).getForename());
	((TextView) convertView.findViewById(R.id.row_surname)).setText(((AWName)getItem(pPosition)).getSurname());
		
	return convertView;
}
  • getView() ist die Methode, die aus den Daten der ArrayList die Views zusammenbaut, die wir später in dem ListView zu sehen bekommen. Zu diesem Zweck bekommt die Methode die Position des Elements, das dargestellt werden soll. Des Weiteren erhält sie einen „convertView“. Dieser kann ein bereits früher erstellter View des ListViews sein, der nun wiederverwendet bzw. recycelt werden kann und auch sollte. Das Recyceln eines solchen Views erleichtert die Darstellung von Listen um ein Vielfaches. Da man aber nicht wissen kann, ob ein convertView bereits verwendet wurde, prüft man ihn vorher, ob er NULL ist oder nicht. Bei NULL erstellen wir uns mittels LayoutInflater aus unserem Layout einen neuen View und referenzieren den convertView darauf. Danach setzen wir im convertView für die beiden TextViews' die Werte der Vor- und Nachnamen und geben den „neuen“ convertView zurück. Sollte der convertView schon von Beginn an NICHT NULL sein, können wir ihn gleich weiterbenutzen und müssen keinen neuen View aus dem Layout generieren. getView() wird so oft aufgerufen, wie getCount() an Wert zurückgegeben hat.

Wichtig zu sagen ist noch, dass bis auf getItem() keine der oben genannten Methoden von uns selbst, sondern nur von Android in Verbindung mit setListAdapter() aufgerufen werden.

Damit haben wir es geschafft! Es sollte nun eine Anwendung vorhanden sein, die einen ListView darstellt, welche ihre Daten aus einem eigenen ListAdapter erhält. Natürlich ist unser Adapter ein nur sehr kleiner. Die Layouts, die verwendet werden, können beliebig viel größer sein und/oder auch andere Elemente wie Bilder enthalten.

Don’t, Do, Even Better

Bei der Implementierung eines Adapters für einen ListView kann eigentlich nicht viel falsch gemacht werden. Aber das, was falsch gemacht werden kann, ist dann umso schwerwiegender. Dabei geht es um die Implementierung der getView()-Methode. Hier gibt es Wege, wie es auf keinen Fall gemacht werden sollte, und solche, welche sich wesentlich besser eignen.

Dieser kleine „Knigge“ für ListViews wurde auf der Google I/O 2009 im Rahmen der Keynote „Turbo-charge Your UI: How to Make Your Android UI Fast and Efficient“ [7] vorgestellt.

Don’t

@Override
public View getView(int pPosition, View convertView, ViewGroup parent) {
	View row = (View) mLayoutInflater.inflate(R.layout.row_layout, null);

	((TextView) row.findViewById(R.id.row_forename)).setText(mData.get(pPosition).getForename());
	((TextView) row.findViewById(R.id.row_surname)).setText(mData.get(pPosition).getSurname());

	return row;
}

So wie in diesem Beispiel sollte es nie gemacht werden. Hier wird für jedes Listenelement ein neuer View erzeugt, mit Daten gefüttert und an den ListView zurückgegeben. Das bedeutet, dass für jeden dieser neuen Views natürlich neuer Speicherplatz belegt werden muss, was bei einer großen Anzahl von Listenelementen schnell den Speicher verstopft und auch die Darstellung stark verlangsamt.

Do

@Override
public View getView(int pPosition, View convertView, ViewGroup parent) {
	if (convertView == null) {
		convertView = mLayoutInflater.inflate(R.layout.row_layout, null);
	}

	((TextView) convertView.findViewById(R.id.row_forename)).setText(((AWName)getItem(pPosition)).getForename());
	((TextView) convertView.findViewById(R.id.row_surname)).setText(((AWName)getItem(pPosition)).getSurname());
		
	return convertView;
}

Dieses Beispiel stellt die optimale Lösung dar. Hierbei wird zuerst getestet, ob bereits ein View für ein Listenelement erstellt wurde, welches dann gegebenenfalls recycelt werden kann. Nur wenn noch kein View existiert, wird ein neuer erzeugt, der dann mit Daten gefüttert und zurückgegeben wird.

Even Better

Es gibt aber eine Möglichkeit, seinem ListView noch einen kleinen Geschwindigkeitsschub zu geben beziehungsweise ihn noch ressourcenschonender zu machen. Hierzu benötigt man eine kleine, aber feine Zusatzklasse:

static class ViewHolder {
	TextView forename;
	TextView surname;
}
@Override
public View getView(int pPosition, View convertView, ViewGroup parent) {

	ViewHolder holder;

	if(convertView == null){
		convertView = mLayoutInflater.inflate(R.layout.row_layout, null);
	
		holder = new ViewHolder();
		holder.forename = (TextView) convertView.findViewById(R.id.row_forename);
		holder.surname = (TextView) convertView.findViewById(R.id.row_surname);
	
		convertView.setTag(holder);
	
	} else {
		holder = (ViewHolder) convertView.getTag();
	}

	holder.forename.setText(mData.get(pPosition).getForename());
	holder.surname.setText(mData.get(pPosition).getSurname());

	return convertView;
}

Neben dem Recyceln der Views muss nun auch nicht mehr die Referenz auf die einzelnen Views innerhalb des Listenelements gesucht werden. Diese werden im ViewHolder-Objekt gespeichert und als Tag dem Listenelement mitgegeben werden. So wird beim Recyceln des Listenelements dessen Tag ausgelesen und man spart sich den Aufruf der findViewById()-Methode.

Performance

Durch das Recyceln der Views bzw. durch Verwendung eines ViewHolders lassen sich neben dem verringerten Speicherbedarf auch Verbesserungen in der Darstellung einer Liste feststellen. Welche Auswirkungen das Recyceln und die ViewHolder auf die Frames Per Second (FPS) haben, zeigt Abbildung 1. Beim heutigen Stand der mobilen Technik, sprich der Größe des Speichers und der schnellen Prozessoren, fallen einem diese Unterschiede erst bei einer gewissen beziehungsweise großen Anzahl von Listenelementen auf, was nicht bedeuten soll, dass auch bei definitiv geringer Anzahl an Elementen in der Liste schlechter Code geschrieben werden darf.

Daten ändern

Sicherlich wird es dazu kommen, dass die Daten, die der Liste zur Verfügung stehen, sich ändern. Das heißt, dass einige Daten hinzukommen oder wegfallen. Dann will man auch, dass sich diese Änderung in der Liste widerspiegeln. Zu diesem Zweck ändert man im Adapter die zugrunde liegenden Daten. Wichtig dabei zu beachten ist, dass man dem Adapter mitteilt, dass sich etwas an den Daten geändert hat. Ansonsten ist er etwas sauer auf Sie und beendet einfach die Anwendung. :)


Hier ein kleines Beispiel, wie Sie ihn beschwichtigen können:

public void changeData(ArrayList<AWName> pData){
	mData = pData;
	this.notifyDataSetChanged();
}

Zum einen wird dem Adapter eine neue (oder auch nur aktualisierte Liste) mit den geänderten Daten übergeben. Daraufhin muss dann noch die Methode notifyDataSetChanged() aufgerufen werden, um dem Adapter zu signalisieren, dass sich etwas an den Daten geändert hat. Hierdurch wird die gesamte Liste neu zusammengestellt und angezeigt.

Einzelnachweise

  1. ListActivity: http://developer.android.com/reference/android/app/ListActivity.html. Stand 7. Januar 2011.
  2. ArrayAdapter: http://developer.android.com/reference/android/widget/ArrayAdapter.html. Stand 7. Januar 2011.
  3. Context: http://developer.android.com/reference/android/content/Context.html. Stand 7. Januar 2011.
  4. BaseAdapter: http://developer.android.com/reference/android/widget/BaseAdapter.html. Stand 7. Januar 2011.
  5. Adapter: http://developer.android.com/reference/android/widget/Adapter.html. Stand 7. Januar 2011.
  6. LayoutInflaterhttp://developer.android.com/reference/android/view/LayoutInflater.html, Stand 7. Januar 2011,
  7. Turbo-charge Your UI: How to Make Your Android UI Fast and Efficient

Zusammenfassung des Projekts

Zurück zu Googles Android


  • Zielgruppe:
    • Für Leute mit Programmiererfahrung in Java
  • Lernziele:
    • Am Ende dieses Buches soll der Leser dazu fähig sein, die Architektur des Android-Betriebssystems zu verstehen und Android-Anwendungen zu schreiben.
  • Buchpatenschaft/Ansprechperson: Zur Zeit niemand, das Buch darf gerne übernommen werden.
  • Sind Co-Autoren gegenwärtig erwünscht? jeder ist willkommen


  • Richtlinien für Co-Autoren:


  • Themenbeschreibung:
    • Dieses Buch beschäftigt sich mit den Elementen und der Programmierung des Android-Betriebssystems.