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 8 Sensoren, GPS und Bluetooth
Pfeil 8.1 Sensoren
Pfeil 8.1.1 Die Klasse »SensorManager«
Pfeil 8.1.2 Dynamische Sensoren und Trigger
Pfeil 8.1.3 Ein Schrittzähler
Pfeil 8.2 GPS und ortsbezogene Dienste
Pfeil 8.2.1 Den aktuellen Standort ermitteln
Pfeil 8.2.2 Positionen auf einer Karte anzeigen
Pfeil 8.3 Bluetooth
Pfeil 8.3.1 Geräte finden und koppeln
Pfeil 8.3.2 Daten senden und empfangen
Pfeil 8.3.3 Bluetooth Low Energy
Pfeil 8.4 Authentifizierung durch biometrische Merkmale
Pfeil 8.4.1 Fingerabdrucksensor im Emulator einrichten
Pfeil 8.4.2 Jetpack Biometric
Pfeil 8.5 Zusammenfassung
 
Zum Seitenanfang

8    Sensoren, GPS und Bluetooth Zur vorigen ÜberschriftZur nächsten Überschrift

Android-Geräte enthalten zahlreiche Sensoren und Schnittstellen, die sich mit geringem Aufwand in eigenen Apps nutzen lassen. Wie das funktioniert, zeige ich Ihnen in diesem Kapitel.

Geräte schalten ihre Anzeige ab, sobald man sie in Richtung des Kopfes bewegt, und die Darstellung auf dem Bildschirm passt sich der Ausrichtung an. Spiele reagieren auf Bewegungsänderungen. Karten-Apps erkennen automatisch den gegenwärtigen Standort. Restaurant- oder Kneipenführer beschreiben nicht nur den kürzesten Weg zur angesagten Döner-Bude, sondern präsentieren die Meinungen anderer Kunden und bieten Alternativen an. Und mit der Funktechnologie Bluetooth lassen sich im Handumdrehen Geräte in Reichweite ansprechen und vernetzen. Dies und noch viel mehr ist möglich, weil die Android-Plattform eine beeindruckende Sensoren- und Schnittstellenphalanx beinhaltet, die von allen Apps genutzt werden kann.

 
Zum Seitenanfang

8.1    Sensoren Zur vorigen ÜberschriftZur nächsten Überschrift

Android stellt seine Sensoren über eine Instanz der Klasse SensorManager zur Verfügung. Wie Sie diese verwenden, zeige ich Ihnen anhand des Projekts SensorDemo1. Die gleichnamige App (sie ist in Abbildung 8.1 zu sehen) ermittelt alle zur Verfügung stehenden Sensoren und gibt unter anderem deren Namen, Hersteller und Version aus. Außerdem verbindet sich das Programm mit dem Helligkeitssensor des Geräts und zeigt die gemessenen Werte an. Sensoren lassen sich grob in drei Kategorien unterteilen:

  • Bewegungssensoren messen Beschleunigungs- und Drehkräfte entlang dreier Achsen. Zu dieser Kategorie gehören Accelerometer, Gyroskop und Gravitationsmesser.

  • Umweltsensoren erfassen verschiedene Parameter der Umwelt, beispielsweise die Umgebungstemperatur, den Luftdruck, Feuchtigkeit und Helligkeit. Zu dieser Kategorie gehören Barometer, Photometer und Thermometer.

  • Positionssensoren ermitteln die Position bzw. die Lage eines Geräts im Raum. Diese Kategorie beinhaltet Orientierungssensoren sowie Magnetometer.

Die App »SensorDemo1«

Abbildung 8.1    Die App »SensorDemo1«

Welche Messfühler einer App zur Verfügung stehen, hängt sowohl von der Plattformversion als auch von der Hardware ab, auf der die App ausgeführt wird. Android hat im Laufe der Zeit nämlich kontinuierlich neue Sensoren »gelernt«. Sensoren können durch Hard- oder Software realisiert werden. Je nach Typ verbrauchen sie viel oder wenig Strom. Einige liefern kontinuierlich Daten, andere nur, wenn sich seit der letzten Messung etwas geändert hat. Die Nutzung der Sensoren erfolgt primär über die Klasse android.hardware.SensorManager, die ich nun ausführlich vorstellen werde.

 
Zum Seitenanfang

8.1.1    Die Klasse »SensorManager« Zur vorigen ÜberschriftZur nächsten Überschrift

Die Methode onCreate() meiner Beispielklasse SensorDemo1Activity kümmert sich nur um das Laden und Anzeigen der Benutzeroberfläche. Alle sensorbezogenen Aktivitäten finden in onResume() und onPause() statt. Beim Fortsetzen der Activity wird mit getSystemService(SensorManager::class.java) die Referenz auf ein Objekt des Typs android.hardware.SensorManager ermittelt. Diese Methode ist in allen von android.content.Context abgeleiteten Klassen vorhanden, beispielsweise android.app. Activity und android.app.Service. Anschließend können Sie mit getSensorList() herausfinden, welche Sensoren in Ihrer App zur Verfügung stehen. name, vendor und version liefern den Namen, den Hersteller und die Version des Sensors. Mit isDynamicSensor können Sie ermitteln, ob ein Sensor dynamisch ist. Was es damit auf sich hat, erkläre ich Ihnen im folgenden Abschnitt.

Beim Aufruf von getSensorList() können Sie anstelle von TYPE_ALL die übrigen mit TYPE_ beginnenden Konstanten der Klasse Sensor nutzen, um nach einer bestimmten Art von Sensor »Ausschau zu halten«. Beispielsweise begrenzt TYPE_LIGHT die Trefferliste auf Helligkeitssensoren. TYPE_STEP_DETECTOR liefert Schrittdetektoren; solche Sensoren melden sich, wenn der Nutzer einen Fuß mit genügend »Schwung« auf den Boden stellt, also einen Schritt macht. In Abschnitt 8.1.3, »Ein Schrittzähler«, erfahren Sie mehr darüber.

Wenn Sie die Art des gewünschten Sensors schon kennen, ist es meist einfacher, anstelle von getSensorList() die Methode getDefaultSensor() aufzurufen. Allerdings weist die Android-Dokumentation darauf hin, dass diese Methode unter Umständen einen Sensor liefert, der gefilterte oder gemittelte Werte produziert. Möchten Sie dies – zum Beispiel aus Genauigkeitsgründen – nicht, dann verwenden Sie getSensorList(). Neben ihren Namen und Herstellern liefern Sensoren eine ganze Menge an Informationen, beispielsweise zu ihrem Stromverbrauch (power), ihrem Wertebereich (maximumRange) und ihrer Genauigkeit (resolution).

package com.thomaskuenneth.androidbuch.sensordemo1

import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.hardware.SensorManager.DynamicSensorCallback
import android.os.*
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

private val TAG = SensorDemo1Activity::class.simpleName
class SensorDemo1Activity : AppCompatActivity() {
private lateinit var manager: SensorManager
private val map = HashMap<String, Boolean>()
private val listener = object : SensorEventListener {
override fun onAccuracyChanged(sensor: Sensor,
accuracy: Int) {
Log.d(TAG, "onAccuracyChanged(): $accuracy")
}

override fun onSensorChanged(event: SensorEvent) {
if (event.values.isNotEmpty()) {
val light = event.values[0]
var text = light.toString()
if (SensorManager.LIGHT_SUNLIGHT <= light &&
light <= SensorManager.LIGHT_SUNLIGHT_MAX) {
text = getString(R.string.sunny)
}
// jeden Wert nur einmal ausgeben
if (!map.containsKey(text)) {
map[text] = true
text += "\n"
textview.append(text)
}
}
}
}

private val callback = object : DynamicSensorCallback() {
override fun onDynamicSensorConnected(sensor: Sensor) {
textview.append(getString(R.string.connected,
sensor.name))
}

override fun onDynamicSensorDisconnected(sensor: Sensor) {
textview.append(getString(R.string.disconnected,
sensor.name))
}
}

private var listenerWasRegistered = false
private var callbackWasRegistered = false

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

override fun onResume() {
super.onResume()
map.clear()
textview.text = ""
manager = getSystemService(SensorManager::class.java)
// Liste der vorhandenen Sensoren ausgeben
manager.getSensorList(Sensor.TYPE_ALL).forEach {
textview.append(getString(R.string.template, it.name,
it.vendor, it.version, it.isDynamicSensor.toString()))
}
// Helligkeitssensor ermitteln
manager.getDefaultSensor(Sensor.TYPE_LIGHT)?.also { sensor ->
manager.registerListener(listener, sensor,
SensorManager.SENSOR_DELAY_NORMAL)
listenerWasRegistered = true
} ?: textview.append(getString(R.string.no_seonsor))
// Callback für dynamische Sensoren
if (manager.isDynamicSensorDiscoverySupported) {
manager.registerDynamicSensorCallback(callback,
Handler(Looper.getMainLooper()))
callbackWasRegistered = true
}
}

override fun onPause() {
super.onPause()
if (listenerWasRegistered) {
manager.unregisterListener(listener)
}
if (callbackWasRegistered) {
manager.unregisterDynamicSensorCallback(callback)
}
}
}

Listing 8.1    Die Klasse »SensorDemo1Activity«

Seit API-Level 21 kennt die Methode getDefaultSensor() einen optionalen zweiten Parameter. Er steuert, ob das System sogenannte Wake-up- oder Non-Wake-up-Sensoren liefert. Der Unterschied besteht darin, ob Sensoren für das Melden von Daten den SoC (System on a Chip) aus dem Ruhezustand aufwecken und das Wechseln in diesen Modus verhindern (wake-up) oder nicht (non-wake-up). Sofern Sensordaten nur während der Ausführung einer Activity erhoben und angezeigt werden, ist die Unterscheidung irrelevant. Für eine möglichst unterbrechungsfreie Aufzeichnung im Hintergrund sind Wake-up-Sensoren die bessere Wahl. Andernfalls muss die App selbstständig dafür sorgen, dass der SoC nicht in den Ruhezustand wechselt. Weitere Informationen finden Sie unter https://source.android.com/devices/sensors/suspend-mode.html.

SensorEventListener

Mit den Methoden registerListener() und unregisterListener() der Klasse SensorManager können Sie sich über Sensorereignisse informieren lassen sowie entsprechende Benachrichtigungen wieder deaktivieren. registerListener() erwartet ein Objekt des Typs android.hardware.SensorEventListener, den Sensor sowie eine Angabe zur Häufigkeit, mit der Wertänderungen übermittelt werden sollen. Sie können einen vordefinierten Wert, zum Beispiel SensorManager.SENSOR_DELAY_NORMAL, oder eine Zeitspanne in Mikrosekunden übergeben. Android garantiert die Einhaltung dieses Wertes allerdings nicht. Sensorereignisse können also häufiger oder seltener zugestellt werden.

Das Activity-Methodenpaar onResume() und onPause() bietet sich an, um SensorEventListener zu registrieren bzw. zu entfernen. Prüfen Sie genau, ob das Sammeln von Sensordaten auch dann erforderlich ist, wenn Ihre Activity nicht ausgeführt wird. Je nach Sensor kann das Messen nämlich in erheblichem Maße Strom verbrauchen.

SensorEventListener-Objekte implementieren die Methoden onAccuracyChanged() und onSensorChanged(). Erstere wird aufgerufen, wenn sich die Genauigkeit eines Sensors geändert hat. Wie wichtig diese Information ist, hängt von der Art des verwendeten Messfühlers ab. Sollte es beispielsweise Probleme beim Ermitteln der Herzfrequenz geben, weil der Sensor kalibriert werden muss (SENSOR_STATUS_UNRELIABLE) oder weil er keinen Körperkontakt hat (SENSOR_STATUS_NO_CONTACT), dann sollte Ihre App auf jeden Fall einen entsprechenden Hinweis anzeigen. Ist hingegen die Genauigkeit des Barometers nicht mehr hoch (SENSOR_STATUS_ACCURACY_HIGH), sondern nur noch durchschnittlich (SENSOR_STATUS_ACCURACY_MEDIUM), ist vielleicht keine diesbezügliche Aktion erforderlich.

Die Methode onSensorChanged() wird aufgerufen, wenn neue Sensordaten vorliegen. Die App SensorDemo1 nutzt den Helligkeitssensor eines Geräts und gibt je nach Helligkeit den gemessenen Wert oder den Text »sonnig« aus. Die Klasse SensorManager enthält zahlreiche Konstanten, die sich auf die vorhandenen Ereignistypen beziehen. Auf diese Weise können Sie, wie im Beispiel zu sehen ist, das Ergebnis der Helligkeitsmessung auswerten, ohne selbst in entsprechenden Tabellen nachschlagen zu müssen.

[+]  Tipp

Liefert die Sensor-Methode isAdditionalInfoSupported() den Wert true, kann ein Sensor über einen neuen Mechanismus weitere, zusätzliche Informationen preisgeben. Sie sind in der Klasse SensorAdditionalInfo enthalten. Um solche Objekte zu empfangen, registrieren Sie mit registerListener() anstelle von SensorEventListener ein Objekt des Typs SensorEventCallback und überschreiben zusätzlich die Methode onSensorAdditionalInfo().

Welche Werte in dem SensorEvent-Objekt übermittelt werden und wie Sie diese interpretieren, hängt vom verwendeten Sensor ab. Beispielsweise liefert der Umgebungstemperatursensor (TYPE_AMBIENT_TEMPERATURE) in values[0] die Raumtemperatur in Grad Celsius. Luftdruckmesser (Sensor.TYPE_PRESSURE) tragen dort hingegen den atmosphärischen Druck in Millibar ein.

Die von einem Android-Gerät oder dem Emulator zur Verfügung gestellten Sensoren können Sie erst zur Laufzeit Ihrer App ermitteln. Selbstverständlich sollten Sie nicht einfach Ihre Activity beenden, wenn ein benötigter Sensor nicht zur Verfügung steht, sondern einen entsprechenden Hinweis ausgeben.

Mithilfe des Elements <uses-feature> der Manifestdatei können Sie die Sichtbarkeit in Google Play auf geeignete Geräte einschränken. Hierzu ein Beispiel:

<uses-feature android:name="android.hardware.sensor.barometer"
android:required="true" />

Apps, deren Manifest ein solches Element enthält, werden in Google Play nur auf Geräten angezeigt, in die ein Barometer eingebaut ist. Beachten Sie hierbei aber, dass diese Filterung eine Installation nicht verhindert, falls die App auf anderem Wege auf das Gerät gelangt ist. Deshalb ist es wichtig, vor der Nutzung eines Sensors seine Verfügbarkeit wie weiter oben gezeigt zu prüfen.

 
Zum Seitenanfang

8.1.2    Dynamische Sensoren und Trigger Zur vorigen ÜberschriftZur nächsten Überschrift

Mit Android 7 hat Google sogenannte dynamische Sensoren eingeführt. Bisher war es so, dass ein Sensor entweder immer »da ist« (weil er in ein Smartphone oder Tablet eingebaut wurde) oder eben nicht. Was aber wäre, wenn man ein Gerät durch Module erweitern und je nach Bedarf Sensoren andocken oder abklemmen könnte? Google hatte mit dem Projekt Ara die Vision eines voll modularen Smartphones. Unglücklicherweise wurde es eingestellt, aber es ist möglich, dass andere Hersteller die Idee irgendwann wieder aufgreifen.

Apps können über isDynamicSensorDiscoverySupported abfragen, ob das System das Erkennen von dynamischen Sensoren unterstützt. In diesem Fall lässt sich mit registerDynamicSensorCallback() ein Objekt des Typs DynamicSensorCallback registrieren. Seine Methoden onDynamicSensorConnected() und onDynamicSensorDisconnected() werden nach dem Verbinden bzw. Trennen eines dynamischen Sensors aufgerufen. Dies ist in Listing 8.1 zu sehen.

Analog zu SensorManager.getSensorList() können Sie übrigens mit getDynamicSensorList() die Liste aller aktuell bekannten dynamischen Sensoren eines Typs abfragen.

Trigger-Sensoren

Viele Daten (zum Beispiel Temperatur, Luftdruck und Helligkeit) können bei Bedarf kontinuierlich erfasst werden, denn sie liegen immer vor. Deshalb ist es bewährte Praxis, Listener nur bei Bedarf zu registrieren und nach Gebrauch wieder zu entfernen. Je nach Sensor ist der Akku des Geräts sonst möglicherweise schnell leer. Eine Ausnahme von dieser Regel stelle ich Ihnen übrigens im nächsten Abschnitt vor. Es gibt aber auch Ereignisse, die unvorhersehbar irgendwann eintreten; dann ist eine kontinuierliche Messung sinnlos.

Für solche Fälle kennt Android Trigger-Sensoren. Der Significant-Motion-Sensor ist ein Trigger. Er meldet sich, wenn das System eine Bewegung erkennt, die wahrscheinlich zu einer Positionsänderung führt. Dies ist beim Laufen, Fahrrad- oder Autofahren der Fall. Trigger-Sensoren liefern beim Zugriff auf reportingMode den Wert REPORTING_MODE_ONE_SHOT. Um einen solchen Sensor zu aktivieren, registrieren Sie nicht mit SensorManager.register() einen SensorEventListener, sondern rufen requestTriggerSensor() auf und übergeben der Methode ein TriggerEventListener-Objekt. Dessen Methode onTrigger() wird vom System aufgerufen, wenn der Trigger aktiviert wurde. Danach wird der Trigger automatisch deaktiviert. Um erneut informiert zu werden, müssen Sie deshalb wieder requestTriggerSensor() aufrufen.

Mein Beispiel SensorDemo2 fasst dies in einer kompakten App zusammen. Sie funktioniert folgendermaßen: Nach dem Start wartet die App auf eine plötzliche Bewegung und gibt dann die aktuelle Uhrzeit aus. Nach einem Klick auf Weiter beginnt der Vorgang von vorne. Bei Gerätedrehungen merkt sie sich den aktuellen Zustand. Und so sieht die Hauptklasse SensorDemo2Activity aus:

package com.thomaskuenneth.androidbuch.sensordemo2

import android.hardware.Sensor
import android.hardware.SensorManager
import android.hardware.TriggerEvent
import android.hardware.TriggerEventListener
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.text.DateFormat
import java.util.*

private const val KEY1 = "shouldCallWaitForTriggerInOnResume"
private const val KEY2 = "tv"
class SensorDemo2Activity : AppCompatActivity() {
private val dateFormat = DateFormat.getTimeInstance()
private val listener = object : TriggerEventListener() {
override fun onTrigger(event: TriggerEvent) {
shouldCallWaitForTriggerInOnResume = false
button.visibility = View.VISIBLE
textview.text = dateFormat.format(Date())
}
}
private var shouldCallWaitForTriggerInOnResume = false
private var sensor: Sensor? = null
private lateinit var manager: SensorManager

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
shouldCallWaitForTriggerInOnResume = true
waitForTrigger()
}
manager = getSystemService(SensorManager::class.java)
sensor = manager.getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION)
sensor?.also {
shouldCallWaitForTriggerInOnResume = true
savedInstanceState?.run {
shouldCallWaitForTriggerInOnResume = getBoolean(KEY1)
textview.text = getString(KEY2)
}
} ?: run {
shouldCallWaitForTriggerInOnResume = false
button.visibility = View.GONE
textview.setText(R.string.no_sensors)
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY1, shouldCallWaitForTriggerInOnResume)
outState.putString(KEY2, textview.text.toString())
}

override fun onResume() {
super.onResume()
sensor?.let {
if (shouldCallWaitForTriggerInOnResume) {
waitForTrigger()
}
}
}

override fun onPause() {
super.onPause()
sensor?.let {
manager.cancelTriggerSensor(listener, sensor)
}
}

private fun waitForTrigger() {
button.visibility = View.GONE
textview.setText(R.string.wait)
manager.requestTriggerSensor(listener, sensor)
}
}

Listing 8.2    Die Klasse »SensorDemo2Activity«

In onCreate() wird als Erstes die Benutzeroberfläche angezeigt und danach mit getDefaultSensor(Sensor.TYPE_SIGNIFICANT_MOTION) der Standard-Trigger-Sensor ermittelt. Aktiviert wird er durch den Aufruf von requestTriggerSensor(). Das geschieht allerdings in onResume(). Hierzu rufe ich die private Methode waitForTrigger() auf. Gleiches passiert beim Anklicken des Buttons button. Beim Pausieren (onPause()) wird der Sensor mit cancelTriggerSensor() deaktiviert.

Um beim Drehen des Geräts den aktuellen Zustand speichern und wiederherstellen zu können, habe ich onSaveInstanceState() überschrieben. Meine Implementierung schreibt zwei Werte, die Boolean-Variable shouldCallWaitForTriggerInOnResume sowie den Inhalt des Textfeldes textview. Beide werden gegebenenfalls in onCreate() wieder gesetzt, wenn savedInstanceState nicht null ist. Möchten Sie, dass Ihre App auch dann informiert wird, wenn keine Activity abgearbeitet wird, müssen Sie die beiden Methodenaufrufe in einen Service auslagern. Geht das Gerät aber in den Ruhezustand, während SensorDemo2Activity aktiv ist, wird die Aktivität nach dem Aufwachen des Geräts aktualisiert. Der Significant-Motion-Sensor arbeitet nämlich weiter, während das Gerät schläft.

Vielleicht ist Ihnen beim Stöbern in der Dokumentation aufgefallen, dass die Klasse TriggerEvent einen Zeitstempel enthält, der den Zeitpunkt des Auftretens in Nanosekunden angibt. Dieser Wert ist nicht dafür gedacht, Uhrzeiten oder Datumsangaben abzuleiten. Er sollte nur verwendet werden, um Abstände zwischen Aufrufen eines Sensors zu ermitteln.

 
Zum Seitenanfang

8.1.3    Ein Schrittzähler Zur vorigen ÜberschriftZur nächsten Überschrift

In diesem Abschnitt sehen wir uns einen weiteren Sensor an, den Schrittzähler. Er meldet die Anzahl der Schritte seit dem letzten Start des Geräts, allerdings nur, solange er aktiviert ist. Google empfiehlt in der Dokumentation deshalb, nicht die Methode unregisterListener() aufzurufen, wenn Langzeitmessungen erfolgen sollen. Diese sind unproblematisch, weil der Sensor in Hardware implementiert ist und wenig Strom verbraucht. Befindet sich das Gerät im Ruhemodus, werden bei aktiviertem Sensor weiterhin Schritte gezählt und nach dem Wiederaufwachen gemeldet. Alles in allem eine wirklich praktische Angelegenheit.

Die App »SensorDemo3«

Abbildung 8.2    Die App »SensorDemo3«

Vielleicht fragen Sie sich, was passiert, wenn Sie die Anzahl der Schritte ganz bewusst zurücksetzen möchten. Da der Zähler die Schritte seit dem letzten Systemstart zählt, müssten Sie das Smartphone oder Tablet neu starten. Das klingt nicht sehr elegant. In meiner App SensorDemo3 (sie ist in Abbildung 8.2 zu sehen) zeige ich Ihnen, wie Sie dieses Problem mit SharedPreferences und einem Broadcast Receiver lösen. Bitte sehen Sie sich Listing 8.3 an. Als Erstes fällt auf, dass es neben der Klasse SensorDemo3Activity die Toplevel-Funktionen updateSharedPrefs() und getSharedPreferences() (sie ist privat) enthält. Wir brauchen sie für das Zurücksetzen des Zählers. Ich komme etwas später darauf zurück.

In der Methode onCreate() wird die Benutzeroberfläche geladen und angezeigt. Da ab Android 10 Schrittzähler-Ereignisse nur dann an Apps gemeldet werden, wenn diese die Berechtigung ACTIVITY_RECOGNITION angefordert haben und der Nutzer sie auch gewährt hat, findet eine Fallunterscheidung statt: Wurde die Berechtigung schon erteilt oder ist das gar nicht nötig, weil die Android-Version älter ist, wird durch Aufruf der Methode showStepCounterUi() die Schrittzähler-Oberfläche dargestellt. Andernfalls erscheint ein Hinwies, dass die Berechtigung noch fehlt, sowie ein Button. Ein Klick auf Anfordern führt requestPermissions() aus. In onRequestPermissionsResult() wird ebenfalls showStepCounterUi() aufgerufen.

getSystemService() und getDefaultSensor() liefern wie gehabt Referenzen auf Objekte des Typs SensorManager bzw. Sensor. In showStepCounterUi() wird je nach Zustand des Schalters on_off entweder registerListener() oder unregisterListener() aufgerufen. Das sonst übliche Deaktivieren des Sensors in onPause() entfällt. Sie erinnern sich: Google rät zu diesem Vorgehen, um Langzeitmessungen vornehmen zu können. Sie sollten in Ihrer App auf jeden Fall eine Möglichkeit vorsehen, den Sensor zu deaktivieren.

package com.thomaskuenneth.androidbuch.sensordemo3

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.*
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.no_permission.*

private const val PREFERENCES_KEY = "last"
private const val REQUEST_ACTIVITY_RECOGNITION = 123
fun updateSharedPrefs(
context: Context?,
last: Int
) {
val edit = getSharedPreferences(context)?.edit()
edit?.putInt(PREFERENCES_KEY, last)
edit?.apply()
}

private fun getSharedPreferences(context: Context?) = context?.getSharedPreferences(
SensorDemo3Activity::class.simpleName,
Context.MODE_PRIVATE
)

class SensorDemo3Activity : AppCompatActivity(), SensorEventListener {
private lateinit var manager: SensorManager
private var sensor: Sensor? = null
private var hasSensor = false
private var last = 0

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
manager = getSystemService(SensorManager::class.java)
sensor = manager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
hasSensor = sensor != null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
checkSelfPermission(Manifest.permission.ACTIVITY_RECOGNITION)
!= PackageManager.PERMISSION_GRANTED
) {
setContentView(R.layout.no_permission)
button_permission.setOnClickListener {
requestPermissions(
arrayOf(Manifest.permission.ACTIVITY_RECOGNITION),
REQUEST_ACTIVITY_RECOGNITION
)
}
} else {
showStepCounterUi()
}
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions,
grantResults)
if (requestCode == REQUEST_ACTIVITY_RECOGNITION &&
grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
showStepCounterUi()
}
}

override fun onSensorChanged(sensorEvent: SensorEvent) {
val values = sensorEvent.values
updateSteps(values[0].toInt())
}

override fun onAccuracyChanged(sensor: Sensor?, i: Int) {
}

private fun updateSteps(currentSteps: Int) {
last = currentSteps
steps.text = "${currentSteps - getLastStoredStepCount()}"
}

private fun getLastStoredStepCount() = getSharedPreferences(this)
?.getInt(PREFERENCES_KEY, 0) ?: 0

private fun showStepCounterUi() {
setContentView(R.layout.activity_main)
reset.setOnClickListener {
updateSharedPrefs(this, last)
updateSteps(last)
}
on_off.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
manager.registerListener(
this, sensor,
SensorManager.SENSOR_DELAY_UI
)
} else {
manager.unregisterListener(this)
}
}
on_off.isEnabled = hasSensor
on_off.isChecked = hasSensor
reset.isEnabled = hasSensor
steps.text = getString(
if (!hasSensor) {
R.string.no_sensor
} else {
R.string.waiting
}
)
}
}

Listing 8.3    Die Datei »SensorDemo3Activity.kt«

In onSensorChanged() wird aus dem Feld sensorEvent.values die Anzahl der Schritte seit dem letzten Systemstart ausgelesen und der privaten Methode updateSteps() übergeben. Diese sieht in den anwendungsspezifischen Voreinstellungen nach, ob dort ein Schlüssel mit dem Namen »last« (in der String-Konstante PREFERENCES_KEY hinterlegt) gespeichert wurde. Falls ja, wird der gespeicherte Wert von der Anzahl der Schritte seit dem letzten Systemstart abgezogen. Wenn der Anwender die Schaltfläche Zurücksetzen anklickt, wird in den Voreinstellungen der aktuelle Zählerstand gespeichert. Damit lässt sich ohne großen Aufwand das Zurücksetzen des Schrittzählers nachbauen. Eine Kleinigkeit gibt es aber zu beachten: Wenn das Gerät neu gestartet wurde, muss der gespeicherte Wert selbst auf 0 gesetzt werden, damit die App-Anzeige nicht verfälscht wird. Deshalb registriere ich in der Manifestdatei einen Receiver, der auf android.intent.action.BOOT_COMPLETED reagiert. Die Implementierung ist einfach. Sie ist in Listing 8.4 zu sehen:

package com.thomaskuenneth.androidbuch.sensordemo3

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
updateSharedPrefs(context, 0)
}
}
}

Listing 8.4    Die Klasse »BootCompletedReceiver«

Durch Aufruf der Toplevel-Funktion updateSharedPrefs() wird der beim letzten Anklicken der Schaltfläche Zurücksetzen gespeicherte Wert mit 0 überschrieben, und damit stimmt die Berechnung der Schritte wieder. Damit verlassen wir den spannenden Bereich der klassischen Sensoren. Im Folgenden kümmern wir uns um Standortbestimmungen und um die Integration in Google Maps.

 


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