Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
Vorwort
Teil I Grundlagen
1 Android – eine offene, mobile Plattform
2 Hallo Android!
3 Von der Idee zur Veröffentlichung
Teil II Elementare Anwendungsbausteine
4 Wichtige Grundbausteine von Apps
5 Benutzeroberflächen
6 Multitasking
Teil III Gerätefunktionen nutzen
7 Telefonieren und surfen
8 Sensoren, GPS und Bluetooth
Teil IV Dateien und Datenbanken
9 Dateien lesen, schreiben und drucken
10 Datenbanken
Teil V Multimedia und Produktivität
11 Multimedia
12 Kontakte und Organizer
A Einführung in Kotlin
B Jetpack Compose
C Häufig benötigte Codebausteine
D Literaturverzeichnis
E Die Begleitmaterialien
Stichwortverzeichnis

Ihre Meinung?
Spacer
<< zurück
Android 11 von Thomas Künneth
Das Praxisbuch für App-Entwickler
Buch: Android 11

Android 11
Pfeil 6 Multitasking
Pfeil 6.1 Leichtgewichtige Nebenläufigkeit
Pfeil 6.1.1 Java-Erbe
Pfeil 6.1.2 Der Main- oder UI-Thread
Pfeil 6.1.3 Koroutinen
Pfeil 6.2 Services
Pfeil 6.2.1 Gestartete Services
Pfeil 6.2.2 Gebundene Services
Pfeil 6.3 Regelmäßige Arbeiten
Pfeil 6.3.1 JobScheduler
Pfeil 6.3.2 WorkManager
Pfeil 6.4 Mehrere Apps gleichzeitig nutzen
Pfeil 6.4.1 Zwei-App-Darstellung
Pfeil 6.4.2 Beliebig positionierbare Fenster
Pfeil 6.5 Zusammenfassung
 
Zum Seitenanfang

6.2    Services Zur vorigen ÜberschriftZur nächsten Überschrift

Activities sind für den Benutzer unmittelbar sichtbar, und er interagiert mit ihnen. Wenn Sie eine App starten, die Musik aus dem Internet streamt, möchten Sie vielleicht zunächst ein Genre wählen und sich dann einen Sender aussuchen. Sobald die Übertragung der Daten begonnen hat, verliert die Benutzeroberfläche bei so einer App an Bedeutung. Es liegt nahe, die Activity zu verlassen, um etwas anderes zu tun, beispielsweise eine E-Mail zu schreiben oder im Web zu surfen. Damit die Musik in so einem Fall nicht abbricht, muss das System eine Möglichkeit bieten, das Streamen im Hintergrund weiter auszuführen.

Services sind Anwendungsbausteine, die ohne Benutzeroberfläche auskommen. Anders als beispielsweise Broadcast Receiver werden sie aber nicht nur beim Eintreten eines Ereignisses aktiviert. Auch sind Services – im Gegensatz zu Broadcast Receivern – gerade für länger andauernde Tätigkeiten gedacht.

 
Zum Seitenanfang

6.2.1    Gestartete Services Zur vorigen ÜberschriftZur nächsten Überschrift

Der Bau von Activities und Broadcast Receivern folgt sehr ähnlichen Mustern, denn beide müssen ihre Implementierungen von bestimmten Basisklassen ableiten und in der Manifestdatei der App registrieren. Auch Services entstehen auf diese Weise.

Ein einfaches Beispiel

Das Projekt ServiceDemo1 stellt Ihnen einen sehr einfach gehaltenen Service vor. Die Klasse DemoService gibt standardmäßig alle zehn Sekunden das aktuelle Datum und die Uhrzeit in Logcat aus. Einfache Services können Sie von der abstrakten Klasse android.app.Service ableiten. Die Methode onBind() müssen Sie implementieren. Sie liefert entweder null oder eine Instanz des Typs android.os.IBinder. Android unterscheidet zwischen gestarteten und gebundenen Services. Vereinfacht ausgedrückt stellen gebundene Services eine Kommunikationsschnittstelle zur Verfügung. Als gestarteter Service tut DemoService dies nicht und liefert deshalb null. Da nur Datum und Uhrzeit ausgegeben werden, ist keine Kommunikation zwischen Aufrufer und Service erforderlich.

Die Methoden onCreate() und onDestroy() repräsentieren wichtige Stationen im Lebenszyklus eines Service. Sie werden vom System aufgerufen. Die Implementierung aus Listing 6.13 startet einen (für das kontinuierliche Ermitteln und Anzeigen von Datum und Uhrzeit verwendeten) Thread. Er ist aktiv, solange die Variable shouldBeRunning den Wert true hat. Das ändert sich, wenn onDestroy() aufgerufen oder der Thread unterbrochen wird. Die Annotation @Volatile bewirkt einen sogenannten Cache Flush. Dieser ist wichtig, weil die Android-Laufzeitumgebung sonst nicht gewährleistet, dass andere Threads den aktuellen Zustand der Variablen sehen. Das wiederum könnte dazu führen, dass der Thread sich niemals beendet, obwohl ein anderer Thread die Variable auf false gesetzt hat.

package com.thomaskuenneth.androidbuch.servicedemo1

import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
import java.util.*
import kotlin.concurrent.thread

private val TAG = DemoService::class.simpleName
class DemoService : Service() {

@Volatile
private var shouldBeRunning = false

override fun onBind(intent: Intent?): IBinder? {
Log.d(TAG, "onBind()")
return null
}

override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate()")
shouldBeRunning = true
thread {
while (shouldBeRunning) {
Log.d(TAG, Date().toString())
try {
Thread.sleep(10000)
} catch (e: InterruptedException) {
Log.e(TAG, "Thread.sleep()", e)
shouldBeRunning = false
}
}
}
}

override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy()")
shouldBeRunning = false
}
}

Listing 6.13    Erste Version der Klasse »DemoService«

Services werden in der Manifestdatei eingetragen. Wie das aussehen kann, ist in Listing 6.14 dargestellt. Das Element <service /> enthält mindestens das Attribut android:name. Ihm wird – analog zu Activities – der Name der Klasse, die den Service implementiert, zugewiesen. android:label kann den Klartextnamen des Service enthalten, der dem Benutzer (zum Beispiel auf Seiten der Systemeinstellungen) angezeigt wird. Sofern Sie ihn nicht explizit setzen, »erbt« der Service das Label der App. Dies ist auch bei android:icon so. Sie können für den Service ein eigenes Symbol vergeben, müssen das aber nicht tun. Auch hier wird dann das Icon der Anwendung übernommen.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.servicedemo1">
<application
...
android:theme=
"@style/AppTheme">
<activity android:name=".ServiceDemo1Activity">
...
</activity>
<service android:name=".DemoService" />
</application>
</manifest>

Listing 6.14    Auszug der Manifestdatei des Projekts »ServiceDemo1«

Weitere Attribute steuern die Nutzbarkeit Ihres Service durch Ihre und fremde Apps. android:enabled regelt die grundsätzliche Verfügbarkeit. Der Standardwert ist true. Sie müssen das Attribut also üblicherweise nicht angeben. android:permission ermöglicht die Vergabe von Berechtigungen: Ein potenzieller Servicenutzer muss über die hier festgelegten Rechte verfügen. Wenn Sie dieses Attribut nicht setzen, greift das android:permission-Attribut des Elements <application />. Haben Sie keines der beiden Attribute gesetzt, ist der Zugriff auf den Service nicht durch eine Berechtigung geschützt.

Bitte beachten Sie den Unterschied zu <uses-permission />-Elementen: Diese definieren, welche Berechtigung eine App erhalten möchte. Beispielsweise benötigt das Projekt BroadcastReceiverDemo aus Kapitel 4 android.permission.RECEIVE_BOOT_COMPLETED, um nach dem Ende des Bootvorgangs eine Nachricht anzuzeigen.

Mit android:exported steuern Sie die Sichtbarkeit Ihres Service durch andere Apps. Das Attribut android:process erzwingt die Ausführung in einem bestimmten Prozess. Dabei gelten dieselben Regeln wie für Activities. Um einen Service gegen den Rest des Systems abzuschotten, setzen Sie android:isolatedProcess auf true. In diesem Fall läuft der Dienst in einem gesonderten, isolierten Prozess und hat keine eigenen Rechte. Die Kommunikation erfolgt ausschließlich über die Service-API.

Damit ist die Implementierung der ersten Version unseres Service schon abgeschlossen. Um ihn zu starten, rufen Sie innerhalb einer Activity startService() auf und übergeben ein Intent, das die gewünschte Komponente beschreibt. Das ist in der Methode handleButtonClicked() von Listing 6.15 zu sehen. Solche Intents werden explizites Intent genannt. Die Kombination aus Paket und Klasse heißt Komponentenname. Sie wird durch die Klasse ComponentName repräsentiert. Das Intent zum Starten des Services lässt sich alternativ auch so erzeugen:

val intent = Intent()
intent.component = android.content.ComponentName(this, DemoService::class.java)

Die Methode handleButtonClicked() wird aufgerufen, wenn ein Button (das einzige UI-Element der Activity) angeklickt wird. In onCreate() ist von findViewById() und setOnClickListener() aber nichts zu sehen. Wer ruft handleButtonClicked() dann eigentlich auf? Sie können in der Layoutdatei festlegen, welche Methode beim Anklicken eines Buttons aufgerufen wird, indem Sie dem <Button />-Tag das Attribut android:onClick="..." hinzufügen. Der Wert entspricht dem Namen der Methode bzw. Funktion. Deren Signatur muss (abgesehen natürlich vom Namen) so aussehen wie in meinem Listing gezeigt.

package com.thomaskuenneth.androidbuch.servicedemo1

import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity

class ServiceDemo1Activity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

fun handleButtonClicked(view: View) {
val intent = Intent(this, DemoService::class.java)
startService(intent)
finish()
}
}

Listing 6.15    Die Klasse »ServiceDemo1Activity«

Wie erreichen Services eine »echte« Hintergrundverarbeitung? Mein Beispiel gibt ja nur Datum und Uhrzeit aus. Die hierfür benötigte Rechenzeit ist vernachlässigbar. Da ich im ersten Abschnitt dieses Kapitels Java-Threads vorgestellt habe, liegt die Vermutung nahe, Services könnten von sich aus auf dieses Hilfsmittel zurückgreifen. Dem ist aber nicht so. Services werden auf dem Mainthread ihrer App oder des Prozesses ausgeführt, der im Attribut android:process des <service />-Elements angegeben wird. Konsequenterweise müssen Sie als Entwickler lange andauernde oder rechenintensive Tätigkeiten selbstständig in eigene Threads auslagern.

Services bilden »nur« einen Rahmen, um dem System mitzuteilen, dass eine App entweder etwas im Hintergrund ausführen will (selbst dann, wenn der Benutzer gar nicht mehr mit ihr interagiert) oder Teile ihrer Funktionalität anderen Programmen zur Verfügung stellen möchte. Um Nebenläufigkeit in Services zu erreichen, können Sie, wie in meinem Beispiel zu sehen, einfach einen neuen Thread starten. Sie müssen nur sicherstellen, dass er zur richtigen Zeit wieder beendet wird. Bei mir wird dies durch die Prüfung der Variable shouldBeRunning erreicht. Ich setze sie in der Methode onDestroy() auf false. Diese Service-Lifecycle-Methode wird von Android aufgerufen, wenn ein Service zerstört wird.

Beenden von Services

Aber wann werden Services eigentlich beendet oder zerstört? Das ist davon abhängig, ob ein Service gestartet oder gebunden wurde (siehe den folgenden Abschnitt). Der Service DemoService wird von der Activity ServiceDemo1Activity durch Aufruf der Methode startService() gestartet. Sie könnte ihn mit stopService() beenden. Den Services selbst stehen die Methoden stopSelf() und stopSelfResult() zur Verfügung. Benutzer können unter Einstellungen • System • Erweitert • Entwickleroptionen • Aktive Dienste (im Emulator und auf englischsprachigen Geräten via Settings • System • Advanced • Developer Options • Running Services) die nicht mehr benötigten Services stoppen. Die Einstellungsseite ist in Abbildung 6.4 zu sehen. Wie Sie die Entwickleroptionen aktivieren, lesen Sie in Abschnitt 3.2.3, »Debuggen auf echter Hardware«.

Die Einstellungsseite »Aktive Dienste«

Abbildung 6.4    Die Einstellungsseite »Aktive Dienste«

Und natürlich kann Android selbst einen Service beenden. Dies geschieht zum Beispiel bei knappen Systemressourcen. Außerdem gibt es seit Android 8 Einschränkungen in der Hintergrundverarbeitung, auf die ich später noch ausführlicher zu sprechen komme. Prinzipiell können Services, die mit startService() gestartet wurden, unendlich lange laufen. Sie führen allerdings normalerweise genau eine Operation aus und liefern kein Ergebnis an den Aufrufer. Gestartete Services sollten sich beenden, nachdem die Aufgabe abgearbeitet wurde. Das Hoch- oder Herunterladen von Dateien ist ein Beispiel für solche Operationen. Allerdings stellt Android hierfür bereits einen sehr guten Mechanismus bereit.

Nach startService() ruft das System die Methode onStartCommand() auf. Sie können diese überschreiben, um das ihr übergebene Intent auszuwerten. Es könnte beispielsweise den Namen der zu übertragenden Datei enthalten. Der Rückgabewert von onStartCommand() legt fest, wie Android verfahren soll, wenn das System den Prozess beenden muss, der den Service hostet. Dies ist bei akutem Speichermangel der Fall. START_NOT_STICKY besagt, dass der Service nur im Fall von noch ausstehenden Intents neu gestartet werden soll. Bei START_STICKY startet Android den Service auf jeden Fall neu und ruft onStartCommand() auf. Sofern keine ausstehenden Intents vorhanden sind, wird aber nicht das letzte Intent erneut übergeben, sondern der Wert null. Anders ist es bei START_REDELIVER_INTENT: Hier erhält onStartCommand() immer das letzte Intent. Auf diese Weise kann der Service beispielsweise die Übertragung einer Datei fortsetzen. Die Standardimplementierung in der Klasse Service liefert START_STICKY.

Einschränkungen in der Hintergrundverarbeitung

Seit Oreo schränkt Android die Möglichkeiten von Apps ein, die sich nicht im Vordergrund befinden. Eine App befindet sich im Hintergrund, wenn keine der folgenden Bedingungen zutrifft:

  • Mindestens eine ihrer Activities ist sichtbar. Ob sie im Zustand gestartet oder pausiert ist, spielt keine Rolle.

  • Sie hat mindestens einen aktuell ausgeführten Vordergrundservice.

  • Eine andere Vordergrund-App ist mit ihr verbunden, weil sie einen ihrer Services gebunden hat oder einen ihrer Content Provider nutzt.

Vordergrundservices machen durch ein Symbol in der Statuszeile auf sich aufmerksam. Der Anwender sieht also, dass auch nach dem Verlassen einer Activity noch eine Komponente der App ausgeführt wird. Hintergrundservices hingegen sind für den Benutzer nicht präsent. Er merkt also möglicherweise nicht, dass eine Aktivität im Hintergrund viele Daten überträgt oder durch komplizierte Berechnungen den Akku leert.

DemoService zeigt kein Symbol in der Statusleiste an und ist deshalb ein Hintergrundservice. Da sich ServiceDemo1Activity unmittelbar nach dem Start des Service mit finish() beendet, ist die Activity nicht mehr aktiv. Da es auch sonst keine aktiven Komponenten mit Benutzeroberfläche gibt, wird ServiceDemo1 zu einer Hintergrund-App. Somit wird Android den Service nach kurzer Zeit zerstören. Zum Glück lässt sich das leicht verhindern. Wir müssen den Service nur zu einem Vordergrundservice machen. Ändern Sie hierzu als Erstes den Aufruf von startService() in handleButtonClicked() um in startForegroundService(). Die Methode steht erst ab API-Level 26 zur Verfügung. Falls Ihre App auf älteren Android-Geräten verwendet werden soll (minSdkVersion ist kleiner als 26), prüfen Sie dies folgendermaßen:

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}

Listing 6.16    Einen Vordergrundservice starten

Nun muss in der Manifestdatei die Berechtigung FOREGROUND_SERVICE angefordert werden.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.servicedemo1">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
...

Listing 6.17    Berechtigung in der Manifestdatei eintragen

Schließlich fügen Sie der Klasse DemoService den Codeschnipsel in Listing 6.18 hinzu. Da es Vordergrundservices erst seit Android 8 gibt, wird gleich zu Beginn die Plattform-Version abgefragt. Der Methode startForeground() wird eine Benachrichtigung sowie eine ID, die diese kennzeichnet, übergeben. Sie müssen die ID beispielsweise verwenden, um die Benachrichtigung abzubrechen. Hierfür enthält die Klasse NotificationManager die Methode cancel(). Die Benachrichtigung dient dazu, den Anwender über die Präsenz des Vordergrundservice sowie dessen Status zu informieren. Mit setContentIntent() können Sie eine Activity angeben, die beim Antippen der Benachrichtigung angezeigt wird.

override fun onStartCommand(intent: Intent?, flags: Int, 
startId: Int): Int {
if (android.os.Build.VERSION.SDK_INT >=
android.os.Build.VERSION_CODES.O) {
val channelId = "channelId_1234"
val channel = android.app.NotificationChannel(
channelId,
getString(R.string.app_name),
android.app.NotificationManager.IMPORTANCE_DEFAULT
)
getSystemService(android.app.NotificationManager::class.java)?.let {
it.createNotificationChannel(channel)
val b = android.app.Notification.Builder(
this,
channelId
)
b.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.app_name))
startForeground(0x1234, b.build())
}
}
return super.onStartCommand(intent, flags, startId)
}

Listing 6.18    Aus einem Service einen Vordergrundservice machen

Ich verwende in diesem Beispiel nicht Jetpack Notifications, sondern die Klassen der Plattform. Sie erkennen das an dem Paketnamen android.app. Ein Mehrwert von Jetpack ist ja, sich nicht um Unterschiede im Funktionsumfang (Benachrichtigungskanäle kamen mit Android 8 an Bord) kümmern zu müssen. Da auch Vordergrundservices mit dieser Version Einzug hielten, kann ich die entsprechende Abfrage prima nutzen. Anders ausgedrückt: Gibt es Vordergrundservices, müssen Notifications auch Benachrichtigungskanäle verwenden. Im nächsten Abschnitt stelle ich Ihnen die zweite Serviceart vor. Sie kommt ohne Benachrichtigungen aus.

 
Zum Seitenanfang

6.2.2    Gebundene Services Zur vorigen ÜberschriftZur nächsten Überschrift

Gebundene Dienste stellen eine Schnittstelle zur Verfügung, über die Servicenutzer mit den Diensten kommunizieren. Diese »Clients« können Teil der App sein, zu der auch der Service gehört, aber auch fremde Prozesse dürfen seine Funktionen ansprechen. Ein gebundener Service läuft, solange mindestens ein Client mit ihm verbunden ist. Danach wird er zerstört.

Wie Sie bereits wissen, muss jede von android.app.Service abgeleitete Klasse die Methode onBind() bereitstellen. Sie liefert ein Objekt, das das Interface android.os.IBinder implementiert. IBinder beschreibt ein abstraktes Protokoll für die Kommunikation mit remotefähigen Objekten, die wiederum die Grundlage für den Aufruf von Funktionen über Prozessgrenzen hinweg bilden, was in der Informatik Remote Procedure Call genannt wird.

Die Objekte eines Kotlin-Programms unterliegen normalerweise dem Zugriff und der Kontrolle einer virtuellen Maschine oder Laufzeitumgebung. Möchte ein Objekt eine Methode eines anderen Objekts desselben Programms aufrufen, so ist dies (eine geeignete Sichtbarkeit vorausgesetzt) problemlos möglich. Anders sieht es aus, wenn zwei Apps miteinander kommunizieren sollen. Denn sie werden standardmäßig in jeweils eigenen, streng abgeschotteten Linux-Prozessen mit eigenem Adressraum ausgeführt.

[»]  Hinweis

Die Verteilung von App-Bausteinen auf Prozesse kann in begrenztem Umfang über android:process-Attribute in der Manifestdatei konfiguriert werden.

Es muss also einen Mechanismus geben, der die Information, welche Funktion ausgeführt werden soll, sowie die korrespondierenden Ein- und Ausgabeparameter in geeigneter Weise transportiert.

Die Klasse »android.os.Binder«

Glücklicherweise müssen Sie das Interface IBinder nicht implementieren, sondern können es von der Klasse android.os.Binder ableiten. Das bietet sich an, wenn Ihr Service nur von der eigenen App und nur innerhalb desselben Prozesses angesprochen wird. Das Projekt ServiceDemo2 zeigt, wie ein solcher lokaler Service aussehen kann. Die Klasse LocalService finden Sie in Listing 6.19. Die Methode onBind() liefert eine Instanz des Typs LocalBinder. Diese Klasse leitet von android.os.Binder ab und enthält die zusätzliche Eigenschaft service. Diese enthält eine Referenz auf das LocalService-Objekt, dessen onBind()-Methode aufgerufen wurde. Die einzige weitere Methode von LocalService (fakultaet()) liefert die Fakultät der ihr übergebenen Zahl n.

package com.thomaskuenneth.androidbuch.servicedemo2

import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder

class LocalService : Service() {
private val binder = LocalBinder()

inner class LocalBinder : Binder() {
val service = this@LocalService
}

override fun onBind(intent: Intent?): IBinder {
return binder
}

fun fakultaet(n: Int): Int {
return if (n <= 0) {
1
} else n * fakultaet(n - 1)
}
}

Listing 6.19    Die Klasse »LocalService«

Um den Service aufrufen zu können, müssen Sie ihn in der Manifestdatei registrieren. Wie das aussehen kann, ist in Listing 6.20 zu sehen. Da der Service die Klasse Binder nutzt und deshalb nur innerhalb desselben Prozesses wie der Aufrufer verwendet werden kann, setzt er das Attribut android:exported auf false. Für andere Apps ist er damit »tabu«.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.servicedemo2">
<application
...
android:theme=
"@style/AppTheme">
<activity android:name=".MainActivity">
...
</activity>
<service
android:name=".LocalService"
android:exported="false" />
</application>
</manifest>

Listing 6.20    Auszug der Manifestdatei des Projekts »ServiceDemo2«

Die Klasse ServiceDemo2Activity (siehe Listing 6.21) realisiert eine Activity mit Eingabezeile, Schaltfläche und Textfeld. Sie verbindet sich mit LocalService und ruft nach dem Anklicken der Schaltfläche BERECHNEN dessen Methode fakultaet() auf. Um die Verbindung zu einem Service herzustellen, müssen Sie die Methode bindService() aufrufen. Ihr wird ein Intent übergeben. Dies geschieht in onStart(). Um ServiceDemo2Activity von LocalService zu trennen, nutze ich unbindService(). Diese Methode wird in onStop() aufgerufen.

In beiden Fällen wird die Referenz auf ein ServiceConnection-Objekt übergeben. Dessen Callback-Methode onServiceConnected() weist den übergebenen IBinder nach einem Cast auf LocalBinder und Zugriff auf dessen Eigenschaft service der ServiceDemo2Activity-Instanzvariablen service zu. Hierbei handelt es sich um den Rückgabewert von onBind() aus LocalService. Bitte beachten Sie, dass ich in onServiceDisconnected() die Variable service (die in der Activity verwendete Referenz auf den Service) auf null setze. Nach dem Trennen der Verbindung darf ein Service nicht mehr verwendet werden.

package com.thomaskuenneth.androidbuch.servicedemo2

import android.content.*
import android.content.Intent
import android.content.ServiceConnection
import android.os.*
import android.view.KeyEvent
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import com.thomaskuenneth.androidbuch.servicedemo2.LocalService.LocalBinder
import kotlinx.android.synthetic.main.activity_main.*

class ServiceDemo2Activity : AppCompatActivity() {

private var service: LocalService? = null

private val connection = object : ServiceConnection {
override fun onServiceConnected(
name: ComponentName,
service: IBinder
) {
val binder = service as LocalBinder
this@ServiceDemo2Activity.service = binder.service
}

override fun onServiceDisconnected(name: ComponentName) {
this@ServiceDemo2Activity.service = null
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
service?.let {
try {
val n =
edittext.text.toString().toInt()
val fak = service?.fakultaet(n)
textview.text = getString(
R.string.template,
n, fak
)
} catch (e: NumberFormatException) {
textview.setText(R.string.info)
}
}
}
edittext.setOnEditorActionListener { _: TextView?,
_: Int, _: KeyEvent? ->
button.performClick()
true
}
}

override fun onStart() {
super.onStart()
val intent = Intent(this, LocalService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}

override fun onStop() {
super.onStop()
service?.let {
unbindService(connection)
service = null
}
}
}

Listing 6.21    Die Klasse »ServiceDemo2Activity«

Die Klassen LocalService, LocalBinder und ServiceDemo2Activity werden auf diese Weise sehr eng miteinander verwoben. Dabei mag der Cast auf LocalBinder in der Methode onServiceConnected() irritieren. Woher soll die Activity wissen, dass tatsächlich eine geeignete IBinder-Implementierung übergeben wurde? Sie müssen sich vergegenwärtigen, dass unser lokaler Service für die ausschließliche Nutzung durch die eigene App konzipiert wurde, die die beteiligten Klassen kennt. Der Vorteil ist ein sehr unkomplizierter Aufruf der eigentlichen Serviceoperation, also der Berechnung der Fakultät:

val n = edittext.text.toString().toInt()
val fak = service?.fakultaet(n)

Die Kommunikation mit einem Service über Prozessgrenzen hinweg kann auf zweierlei Weise erfolgen. Die meisten Freiheiten bietet die Nutzung von AIDL, der Android Interface Definition Language. Allerdings rät Google unter anderem aus Komplexitätsgründen von der direkten AIDL-Nutzung für den Bau von Services ab. Trotzdem möchte ich Ihnen ein paar grundlegende Informationen darüber geben: Objekte werden in primitive Einheiten zerlegt und vom Betriebssystem an den Zielprozess übermittelt. Um AIDL zu nutzen, müssen Sie die gewünschte Kommunikationsschnittstelle in einer .aidl-Datei ablegen. Die Werkzeuge des Android SDK erzeugen daraus eine abstrakte Klasse, die die von Ihnen definierten Methoden implementiert und sich um die Interprozesskommunikation kümmert. Weiterführende Information finden Sie im Dokument Android Interface Definition Language (AIDL).[ 6 ](https://developer.android.com/guide/components/aidl.html)

Die Klasse »android.os.Messenger«

Auch die Klasse android.os.Messenger funktioniert über Prozessgrenzen hinweg. Die Idee ist, in einem Prozess einen Messenger zu instanziieren, der einen Handler referenziert. Diesem können Nachrichten übermittelt werden. Das Messenger-Objekt wird dann einem anderen Prozess übergeben. Um zu demonstrieren, wie Sie dieses zugegebenermaßen nicht ganz leicht verständliche Konzept praktisch umsetzen können, stelle ich Ihnen die beiden Projekte ServiceDemo3_Service und ServiceDemo3 vor. ServiceDemo3_Service implementiert den Service, ServiceDemo3 eine Nutzer-App. Sie ist ohne den Service natürlich nicht lauffähig.

Lassen Sie uns zunächst einen Blick auf den Service (Listing 6.22) werfen. Meine Klasse RemoteService ist sehr einfach aufgebaut. Sie leitet von android.app.Service ab und implementiert nur onBind(). Die hierbei gelieferte Referenz auf eine IBinder-Instanz wird aus der Eigenschaft binder (genau genommen: der Methode getBinder()) eines Messenger-Objekts ausgelesen. Diesem Objekt, der Instanzvariablen messenger, wurde beim Instanziieren wiederum eine IncomingHandler-Instanz übergeben.

package com.thomaskuenneth.androidbuch.servicedemo3_service

import android.app.Service
import android.content.Intent
import android.os.*
import android.util.Log

const val MSG_FACTORIAL_IN = 1
const val MSG_FACTORIAL_OUT = 2
private val TAG = RemoteService::class.simpleName
class RemoteService : Service() {

private lateinit var messenger: Messenger

override fun onBind(intent: Intent?): IBinder? {
messenger = Messenger(IncomingHandler())
return messenger.binder
}

private class IncomingHandler : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_FACTORIAL_IN -> {
val n = msg.arg1
Log.d(TAG, "Eingabe: $n")
val fak = fakultaet(n)
val m = msg.replyTo
val msg2 = Message.obtain(
null,
MSG_FACTORIAL_OUT, n, fak
)
try {
m.send(msg2)
} catch (e: RemoteException) {
Log.e(TAG, "send()", e)
}
}
else -> super.handleMessage(msg)
}
}

private fun fakultaet(n: Int): Int {
return if (n <= 0) {
1
} else n * fakultaet(n - 1)
}
}
}

Listing 6.22    Die Klasse »RemoteService«

Die Klasse IncomingHandler enthält die eigentliche Kommunikation sowie die Berechnung der Fakultät. Die Implementierung überschreibt die Methode handleMessage(). Ihr wird ein Message-Objekt übergeben, das eine eingehende Nachricht repräsentiert. Enthält dessen Instanzvariable what einen bestimmten Wert (MSG_FACTORIAL_IN), wird die Fakultät der Zahl aus arg1 berechnet. Das Ergebnis wird in Gestalt eines eigenen, neuen Message-Objekts (Rückgabewert von Message.obtain()) mit send() an den Absender der gerade bearbeiteten Nachricht (val m = msg.replyTo) übertragen. Die Berechnung der Fakultät wird also in zwei Mitteilungen aufgeteilt. Die beiden Int-Werte MSG_FACTORIAL_IN (Berechnung starten) und MSG_FACTORIAL_OUT (Ergebnis zurückliefern) müssen auch dem Servicenutzer bekannt sein.

Servicenutzer

Um einen Service aus einer fremden App heraus nutzen zu können, müssen Sie ihn exportieren. Dazu setzen Sie, wie in Listing 6.23 zu sehen, das Attribut android:exported auf true. Außerdem wird mit android:permission eine Berechtigung definiert, die der Servicenutzer angeben muss. Das ist nicht zwingend erforderlich, ist aber bewährte Praxis.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.servicedemo3_service">
<application
...
</activity>
<service
android:name=".RemoteService"
android:exported="true"
android:permission=
"com.thomaskuenneth.androidbuch.servicedemo3_service.USE" />
</application>
</manifest>

Listing 6.23    Auszug der Manifestdatei von »ServiceDemo3_Service«

[»]  Hinweis

Bitte beachten Sie, dass das Projekt ServiceDemo3_Service eigentlich keine Hauptaktivität braucht. Ich habe eine sehr einfache (sie hat keine UI und beendet sich gleich nach dem Start wieder) hinzugefügt, weil es beim Installieren aus Android Studio sonst Probleme geben kann. Um diese zu beheben, müssten Sie mit RunEdit Configurations den Dialog Run/Debug Configurations öffnen und unter Launch Options den Eintrag Nothing auswählen. Das ist nicht nötig, wenn das Projekt eine Hauptaktivität hat.

Das Projekt ServiceDemo3 nutzt den Service aus ServiceDemo3_Service. Er wird in der Methode onStart() der Klasse ServiceDemo3Activity gebunden, und zwar durch einen Aufruf von bindService(). Hierfür wird ein Intent benötigt. Anders als bei lokalen Services können Sie dieses aber nicht durch Intent(this, RemoteService::class.java) instanziieren. Denn die App kennt die Klasse RemoteService ja gar nicht. Sie gehört nicht zum Projekt. In frühen Android-Versionen war es möglich, bindService() ein implizites Intent zu übergeben. Anstelle einer Klasse oder Komponente enthält dieses eine Aktion in Form einer Zeichenkette, beispielsweise den voll qualifizierten Klassennamen. Das System ermittelt Activities, Services oder Broadcast Receiver, die mit dieser Aktion etwas anfangen können, indem es in den Manifestdateien nach Intent-Filtern mit dieser Aktion sucht. Das <service />-Element in Listing 6.23 hätte hierzu folgenden Code als Kind enthalten:

<intent-filter>
<action android:name=
"com.thomaskuenneth.androidbuch.servicedemo3_service.RemoteService" />
</intent-filter>

Dies ist seit Android 5 nicht mehr erlaubt. Sie müssen bindService() ein explizites Intent mit Komponentennamen übergeben. Andernfalls wird zur Laufzeit eine IllegalArgumentException (»Service Intent must be explicit«) ausgelöst. Aber wie lassen sich Bausteine fremder Apps benennen, für die wir ja keine ::class-Referenzen nutzen können? Hier bietet sich die Variante mit zwei String-Parametern an. Der erste ist der Paketname der App, die den Service enthält, der zweite ist der voll qualifizierte Klassenname. Das ist in der Methode onStart() von Listing 6.24 zu sehen. Auf diese Weise kennt Android das Ziel des Intents, ohne die Intent-Filter aller Apps auf die passende Aktion untersuchen zu müssen.

bindService() erhält als zweiten Parameter die Referenz auf ein ServiceConnection-Objekt (connection), das die beiden Methoden onServiceConnected() und onServiceDisconnected() implementiert. onServiceConnected() erzeugt ein android.os.Messenger-Objekt und weist es der Instanzvariablen service zu. Damit ist in allen Methoden der Activity der Zugriff auf den Service aus ServiceDemo3_Service möglich. onServiceDisconnected() setzt die Referenz auf null. Wenn der Benutzer die App verlässt, sollte die Kommunikation mit dem Service ebenfalls gestoppt werden. Hierzu habe ich onStop() überschrieben, und rufe unbindService() auf – allerdings nur, wenn der Service gebunden ist (service?.let {). Damit er die Fakultät einer ihm übergebenen Zahl berechnet, sind nur wenige Zeilen Code nötig. Mit Message.obtain() wird eine neue Nachricht erzeugt, und mit service?.send() wird sie verschickt.

package com.thomaskuenneth.androidbuch.servicedemo3

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.*
import android.util.*
import android.view.*
import android.widget.*
import androidx.appcompat.app.AppCompatActivity

const val MSG_FACTORIAL_IN = 1
const val MSG_FACTORIAL_OUT = 2
private const val PACKAGE =
"com.thomaskuenneth.androidbuch.servicedemo3_service"
private val TAG = ServiceDemo3Activity::class.simpleName
class ServiceDemo3Activity : AppCompatActivity() {

private var service: Messenger? = null

private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName,
service: IBinder) {
this@ServiceDemo3Activity.service = Messenger(service)
}

override fun onServiceDisconnected(className: ComponentName) {
service = null
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textview = findViewById<TextView>(R.id.textview)
val edittext = findViewById<EditText>(R.id.edittext)
val button = findViewById<Button>(R.id.button)
val messenger = Messenger(IncomingHandler(this, textview))
button.setOnClickListener {
service?.let {
try {
val n = edittext.text.toString().toInt()
val msg = Message.obtain(null,
MSG_FACTORIAL_IN, n, 0)
msg.replyTo = messenger
it.send(msg)
} catch (e: NumberFormatException) {
textview.setText(R.string.info)
} catch (e: RemoteException) {
Log.d(TAG, "send()", e)
}
}
}
edittext.setOnEditorActionListener { _: TextView?,
_: Int, _: KeyEvent? ->
button.performClick()
true
}
}

override fun onStart() {
super.onStart()
val componentName = ComponentName(PACKAGE,
"${PACKAGE}.RemoteService")
val intent = Intent()
intent.component = componentName
if (!bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
Log.d(TAG, "bindService() nicht erfolgreich")
service = null
finish()
}
}

override fun onStop() {
super.onStop()
service?.let {
unbindService(connection)
service = null
}
}

private class IncomingHandler(val context: Context,
val tv: TextView)
: Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_FACTORIAL_OUT -> {
val n = msg.arg1
val fakultaet = msg.arg2
Log.d(TAG, "Fakultaet: $fakultaet")
tv.text = context.getString(R.string.template,
n, fakultaet)
}
else -> super.handleMessage(msg)
}
}
}
}

Listing 6.24    Die Klasse »ServiceDemo3Activity«

Die Klasse RemoteService möchte das Ergebnis ihrer Berechnung ebenfalls als Nachricht versenden. Damit das funktioniert, habe ich msg.replyTo auf messenger gesetzt. Diese lokale Variable referenziert ebenfalls eine Messenger-Instanz. Sie enthält einen Handler. Wie üblich wird dessen Methode handleMessage() überschrieben. Meine Implementierung (die private Klasse IncomingHandler) schreibt die Werte aus arg1 (Zahl, deren Fakultät berechnet werden sollte) und arg2 (das Ergebnis) in ein Textfeld. Die App ServiceDemo3 ist in Abbildung 6.5 zu sehen. Damit sie funktioniert, muss in Ihrer Manifestdatei die vom Service geforderte Berechtigung ebenfalls definiert sein. Sonst wird zur Laufzeit eine SecurityException (»Not allowed to bind to service Intent«) geworfen. Hierfür sind sowohl <permission /> als auch <uses-permission /> erforderlich. Dies sieht folgendermaßen aus (siehe Listing 6.25):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.servicedemo3">
<permission android:name=
"com.thomaskuenneth.androidbuch.servicedemo3_service.USE" />
<uses-permission android:name=
"com.thomaskuenneth.androidbuch.servicedemo3_service.USE" />
<uses-permission android:name=
"android.permission.QUERY_ALL_PACKAGES" />
<application
...
</application>
</manifest>

Listing 6.25    Auszug aus der Manifestdatei der App »ServiceDemo3«

QUERY_ALL_PACKAGES ist erforderlich, weil sonst bindService() fehlschlägt. Die Erstellung von remotefähigen Services ist eine nicht ganz einfache Aufgabe, daher sollten Sie sehr genau prüfen, ob Sie den Aufwand wirklich betreiben müssen oder ob auch ein lokaler Service ausreichend ist.

Die App »ServiceDemo3«

Abbildung 6.5    Die App »ServiceDemo3«

Im nächsten Abschnitt zeige ich Ihnen, wie Sie Aufgaben vom System im Hintergrund ausführen lassen.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
Zur Rheinwerk-Konferenz für Kotlin
 Buchempfehlungen
Zum Rheinwerk-Shop: Kotlin

Kotlin


Zum Rheinwerk-Shop: Praxisbuch Usability und UX

Praxisbuch Usability und UX


Zum Rheinwerk-Shop: Flutter und Dart

Flutter und Dart


Zum Rheinwerk-Shop: App-Design

App-Design


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2023
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern