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.3    Bluetooth Zur vorigen ÜberschriftZur nächsten Überschrift

Der Industriestandard Bluetooth (IEEE 802.15.1) ermöglicht die verbindungslose sowie verbindungsbehaftete Datenübertragung zwischen Geräten über kurze Distanz per Funk. Sein Name geht auf den dänischen König Harald Blauzahn zurück. Dieser einte verfeindete Teile von Norwegen und Dänemark. Man unterscheidet zwischen klassischem Bluetooth und Bluetooth Low Energy. Ersteres wird zum Beispiel verwendet, um Daten zu übertragen. In den folgenden beiden Abschnitten beschäftige ich mich mit Bluetooth Classic. In Abschnitt 8.3.3, »Bluetooth Low Energy«, stelle ich Ihnen die stromsparende Variante vor. Sie ist im Internet der Dinge (IoT) sehr beliebt.

Je nach Anwendungsfall können Daten zwischen Bluetooth-Geräten auf Basis von Profilen ausgetauscht werden. Profile steuern bestimmte Dienste. Im Schichtenmodell befinden sie sich über der Protokollschicht. Sobald eine Verbindung aufgebaut wird, wählen die beteiligten Geräte das zu verwendende Profil aus und legen damit fest, welche Funktionen sie dem Partner zur Verfügung stellen und welche Daten oder Befehle sie dazu benötigen. Beispielsweise fordert ein Headset von einem Smartphone oder Tablet einen Audiokanal an und steuert über zusätzliche Datenkanäle die Lautstärke.

 
Zum Seitenanfang

8.3.1    Geräte finden und koppeln Zur vorigen ÜberschriftZur nächsten Überschrift

Die Beispiel-App BluetoothScannerDemo zeigt Ihnen, wie Sie Bluetooth-Geräte finden und koppeln. Hierfür ist kein Profil nötig. Die Hauptklasse ist in Listing 8.11 zu sehen. In der Methode onCreate() wird wie üblich die Benutzeroberfläche geladen und angezeigt. Außerdem registriere ich mit registerReceiver() einen Broadcast Receiver. Er kommt ins Spiel, wenn Android ein noch nicht gekoppeltes Bluetooth-Gerät findet. Hierzu filtere ich mit einer android.content.IntentFilter-Instanz auf die Aktion BluetoothDevice.ACTION_FOUND. Wird die BroadcastReceiver-Methode onReceive() mit der richtigen Aktion aufgerufen, enthält das übergebene Intent ein Objekt des Typs BluetoothDevice. Sie greifen mit getParcelableExtra() darauf zu. Es enthält unter anderem den Namen sowie die Adresse des gefundenen Geräts. In meinem Beispiel werden diese Informationen nur angezeigt.

[»]  Hinweis

Bitte denken Sie daran, einen in onCreate() registrierten Broadcast Receiver in onDestroy() mit unregisterReceiver() zu entfernen.

Um Bluetooth nutzen zu können (beispielsweise Verbindungen aufbauen und Daten übertragen), muss Ihre App die normale Berechtigung android.permission.BLUETOOTH anfordern. Für die Suche nach Geräten in Reichweite ist zusätzlich android.permission.BLUETOOTH_ADMIN (auch eine normale Berechtigung) erforderlich. Darüber hinaus müssen Sie die gefährliche Berechtigung android.permission.ACCESS_FINE_LOCATION anfordern. Bis einschließlich Android 9 (API-Level 28) war übrigens nur android.permission.ACCESS_COARSE_LOCATION nötig. In meinem Beispiel rufe ich in onStart() die Methode checkSelfPermission() und gegebenenfalls requestPermissions() auf. Die Prüfung, ob der Benutzer die Berechtigung erteilt hat, findet in onRequestPermissionsResult() statt.

Zentrale Klasse bei der Nutzung von Bluetooth ist ein Objekt des Typs BluetoothAdapter. Sie können eine Referenz darauf mit BluetoothAdapter.getDefaultAdapter() ermitteln. Ich weise diese der Instanzvariablen adapter zu. Ist sie null, steht Bluetooth nicht zur Verfügung. In der privaten Methode getBluetoothState() prüfe ich mit isEnabled, ob Bluetooth schon aktiviert ist. Falls nein, wird ein Intent mit der Aktion BluetoothAdapter.ACTION_REQUEST_ENABLE gefeuert. Android fragt daraufhin beim Benutzer nach, ob Bluetooth eingeschaltet werden soll. Der Dialog ist in Abbildung 8.9 zu sehen. Um darauf reagieren zu können, habe ich onActivityResult() überschrieben. Im Erfolgsfall wird die ebenfalls private Methode showDevices() aufgerufen.

package com.thomaskuenneth.androidbuch.bluetoothscannerdemo

import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.*
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

private const val REQUEST_ENABLE_BLUETOOTH = 123
private const val REQUEST_FINE_LOCATION = 321

private enum class BluetoothState {
NotAvailable, Disabled, Enabled
}

class BluetoothScannerDemoActivity : AppCompatActivity() {
private val adapter = BluetoothAdapter.getDefaultAdapter()
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (BluetoothDevice.ACTION_FOUND == intent.action) {
val device = intent.getParcelableExtra<BluetoothDevice>(
BluetoothDevice.EXTRA_DEVICE)
tv.append(getString(R.string.template,
device?.name,
device?.address))
}
}
}

private var started = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
registerReceiver(receiver, filter)
}

override fun onDestroy() {
super.onDestroy()
unregisterReceiver(receiver)
}

override fun onStart() {
super.onStart()
started = false
if (getBluetoothState() == BluetoothState.NotAvailable) {
tv.text = getString(R.string.not_available)
} else {
if (checkSelfPermission(ACCESS_FINE_LOCATION) !=
PERMISSION_GRANTED) {
requestPermissions(arrayOf(ACCESS_FINE_LOCATION),
REQUEST_FINE_LOCATION)
} else {
showDevices()
}
}
}

override fun onPause() {
super.onPause()
if (started) {
adapter?.cancelDiscovery()
started = false
}
}

override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray) {
if (requestCode == REQUEST_FINE_LOCATION &&
grantResults.isNotEmpty() &&
grantResults[0] == PERMISSION_GRANTED) {
showDevices()
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int,
data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK &&
requestCode == REQUEST_ENABLE_BLUETOOTH) {
showDevices()
}
}

private fun getBluetoothState(): BluetoothState {
val state = if (adapter != null) {
if (adapter.isEnabled) {
BluetoothState.Enabled
} else {
BluetoothState.Disabled
}
} else {
BluetoothState.NotAvailable
}
if (state == BluetoothState.Disabled) {
val enableBtIntent =
Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent,
REQUEST_ENABLE_BLUETOOTH)
}
return state
}

private fun showDevices() {
val sb = StringBuilder()
sb.append(getString(R.string.paired))
adapter?.bondedDevices?.forEach {
sb.append(getString(R.string.template,
it.name,
it.address))
}
sb.append("\n")
if (started) {
adapter?.cancelDiscovery()
}
started = adapter?.startDiscovery() ?: false
if (started) {
sb.append(getString(R.string.others))
}
tv.text = sb.toString()
}
}

Listing 8.11    Die Klasse »BluetoothScannerDemoActivity«

Rückfrage, ob Bluetooth aktiviert werden soll

Abbildung 8.9    Rückfrage, ob Bluetooth aktiviert werden soll

Die Methode showDevices() besteht aus zwei Bereichen. Als Erstes wird mit bondedDevices die Liste der gekoppelten Geräte ermittelt und ausgegeben. Danach starte ich mit startDiscovery() die Suche nach Geräten in Reichweite. Wurde die Suche bereits gestartet, breche ich sie mit cancelDiscovery() vorher ab. Der Grund hierfür ist, dass in der BroadcastReceiver-Methode onReceive() direkt mit append() in die TextView geschrieben wird, in showDevices() aber zunächst in einen StringBuilder, der dann den Inhalt des Textfeldes vollständig ersetzt. Bereits gefundene Geräte würden deshalb plötzlich nicht mehr angezeigt.

Die Suche nach Geräten in Reichweite hat signifikante Auswirkungen auf bestehende Verbindungen. Sie sollten sie deshalb so schnell wie möglich wieder beenden – mindestens aber vor einem Verbindungsaufbau.

 
Zum Seitenanfang

8.3.2    Daten senden und empfangen Zur vorigen ÜberschriftZur nächsten Überschrift

Geräte suchen und anzeigen zu können, ist sicher interessant, aber das eigentliche Ziel ist ja, Daten zu senden oder zu empfangen. Wie das funktioniert, zeige ich Ihnen nun anhand einer simplen Chat-App. Die eingegebenen Texte werden mittels RFCOMM[ 12 ](https://de.wikipedia.org/wiki/RFCOMM) (Radio Frequency Communication) übertragen. Damit BluetoothChatDemo nicht zu unübersichtlich wird, habe ich auf die dynamische Auswahl des entfernten Geräts verzichtet. Um sie auszuprobieren, tragen Sie bitte die Namen Ihrer beiden Geräte in die Konstanten DEVICE1 und DEVICE2 der Klasse BluetoothChatDemoActivity ein. Auf welchem Gerät Sie die App zuerst starten, ist egal. Bitte beachten Sie aber, dass die Geräte schon miteinander gekoppelt sein müssen. Leider können Sie für Ihre Tests nicht den Android Emulator von Google verwenden, weil dieser derzeit kein Bluetooth unterstützt.

Eine Verbindung aufbauen

RFCOMM-Verbindungen benötigen einen Client und einen Server. Welches Gerät dabei die Rolle des Servers übernimmt, kann sich entweder zufällig ergeben oder durch die Art des Datenaustauschs vorgegeben sein. In meinem Beispiel ist die früher gestartete App der Server. Die eigentliche Datenübertragung geschieht mittels BluetoothSocket-Objekten. Sowohl Server als auch Client ermitteln Instanzen dieser Klasse, allerdings auf unterschiedliche Weise. Sehen wir uns als Erstes an, wie dies ein Server tut. Die Klasse ServerSocketThread ist in Listing 8.12 zu sehen. Sie leitet von SocketThread ab. Wie diese abstrakte Klasse aufgebaut ist, zeige ich Ihnen gleich. Die BluetoothAdapter-Methode listenUsingRfcommWithServiceRecord() liefert ein Objekt des Typs BluetoothServerSocket. Dessen Methode accept() blockiert den aufrufenden Thread, bis ein BluetoothSocket zur Verfügung steht (dann können und sollten Sie das BluetoothServerSocket mit close() schließen) oder eine Ausnahme geworfen wird. Ich habe den Aufruf von close() in die private Methode closeServerSocket() ausgelagert. Sie wird an zwei Stellen verwendet. Die Methode cancel() wird von anderen Programmteilen aufgerufen, wenn die durch diese Klasse zur Verfügung gestellte BluetoothSocket nicht mehr benötigt wird. Sie gibt Ressourcen frei.

package com.thomaskuenneth.androidbuch.bluetoothchatdemo

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothServerSocket
import android.bluetooth.BluetoothSocket
import android.util.Log
import java.io.IOException
import java.util.*

private val TAG = ServerSocketThread::class.simpleName
class ServerSocketThread(
adapter: BluetoothAdapter,
serviceName: String?,
uuid: UUID
) : SocketThread() {

private var serverSocket: BluetoothServerSocket? = null
private var socket: BluetoothSocket? = null

init {
name = TAG!!
try {
serverSocket = adapter.listenUsingRfcommWithServiceRecord(
serviceName, uuid)
} catch (e: IOException) {
Log.e(TAG, "listenUsingRfcommWithServiceRecord() failed", e)
}
}

override fun run() {
var keepRunning = true
while (keepRunning) {
try {
serverSocket?.accept().run {
closeServerSocket()
keepRunning = false
}
} catch (e: IOException) {
Log.e(TAG, "accept() failed", e)
keepRunning = false
}
}
}

override fun getSocket(): BluetoothSocket? {
return socket
}

override fun cancel() {
closeServerSocket()
socket?.use {
socket?.close()
}
socket = null
}

private fun closeServerSocket() {
serverSocket?.use {
serverSocket?.close()
}
serverSocket = null
}
}

Listing 8.12    Die Klasse »ServerSocketThread«

SocketThread (Listing 8.13) erweitert Threads um die Referenz auf ein BluetoothSocket-Objekt. getSocket() wird später verwendet, um eine Verbindung aufzubauen. Mit cancel() kann sie wieder getrennt werden.

package com.thomaskuenneth.androidbuch.bluetoothchatdemo

import android.bluetooth.BluetoothSocket

abstract class SocketThread : Thread() {
abstract fun getSocket(): BluetoothSocket?
abstract fun cancel()
}

Listing 8.13    Die abstrakte Klasse »SocketThread«

Der Code zum Ermitteln einer BluetoothSocket-Instanz für Clients sieht recht ähnlich aus, ist aber kürzer. Auch ClientSocketThread leitet von SocketThread ab. Als Erstes wird die BluetoothDevice-Methode createRfcommSocketToServiceRecord() aufgerufen. Ist dies erfolgreich, können Sie mit connect() die Verbindung herstellen. Die Methode cancel() wird von anderen Programmteilen aufgerufen, wenn die durch diese Klasse zur Verfügung gestellte BluetoothSocket nicht mehr benötigt wird. Sie gibt Ressourcen frei.

package com.thomaskuenneth.androidbuch.bluetoothchatdemo

import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import android.util.Log
import java.io.IOException
import java.util.*

private val TAG = ClientSocketThread::class.simpleName
class ClientSocketThread(device: BluetoothDevice, uuid: UUID) :
SocketThread() {

private var socket: BluetoothSocket? = null

init {
name = TAG!!
try {
socket = device.createRfcommSocketToServiceRecord(uuid)
} catch (e: IOException) {
Log.e(TAG, "createRfcommSocketToServiceRecord() failed", e)
}
}

override fun run() {
try {
socket?.connect()
} catch (connectException: IOException) {
cancel()
}
}

override fun getSocket(): BluetoothSocket? {
return socket
}

override fun cancel() {
socket?.use {
socket?.close()
}
socket = null
}
}

Listing 8.14    Die Klasse »ClientSocketThread«

Ist Ihnen aufgefallen, dass meine beiden Klassen ClientSocketThread und ServerSocketThread ein Objekt des Typs java.util.UUID erhalten? Damit wird der von einer App zur Verfügung gestellte Bluetooth-Dienst eindeutig identifiziert. Sehen wir uns nun die Hauptklasse der Chat-App genauer an (Listing 8.15).

Wie üblich wird in onCreate() die Benutzeroberfläche geladen und angezeigt. In onStart() prüfe ich, ob ACCESS_FINE_LOCATION schon erteilt wurde. Ist das der Fall, wird die private Methode startOrFinish() aufgerufen. Falls nicht, wird die Berechtigung angefordert. isBluetoothEnabled() prüft, ob Bluetooth vorhanden und eingeschaltet ist. Allerdings fehlt der Code zum Aktivieren. Sie müssen diesen gegebenenfalls von Hand ergänzen. Steht Bluetooth nicht zur Verfügung (weil es ausgeschaltet wurde oder das Gerät nicht über die nötige Hardware verfügt), beendet sich die App.

Spannend wird es in der ebenfalls privaten Methode connect(). Sie iteriert über das Ergebnis von bondedDevices und ermittelt anhand ihrer Namen das lokale und entfernte Gerät. Anschließend werden vier Threads gestartet. Zwei davon sind kurzlebig. Sowohl ServerSocketThread als auch ClientSocketThread versuchen, wie Sie bereits wissen, eine BluetoothSocket-Instanz zu ermitteln. Je nachdem, ob die App auf dem lokalen oder dem entfernten Gerät zuerst gestartet wurde, ist der erste oder zweite Thread erfolgreich. Die Threads, die den Variablen serverThread und clientThread zugewiesen werden, sind langlebiger. Über sie wird der eigentliche Chat abgewickelt.

package com.thomaskuenneth.androidbuch.bluetoothchatdemo

import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.io.*
import java.util.*
import kotlin.concurrent.thread

private const val DEVICE1 = "..."
private const val DEVICE2 = "..."
private const val REQUEST_FINE_LOCATION = 321
private val MY_UUID = UUID.fromString("dc4f9aa6-ce43-4709-bd2e-7845a3e705f1")
private val TAG = BluetoothChatDemoActivity::class.simpleName
class BluetoothChatDemoActivity : AppCompatActivity() {
private val adapter = BluetoothAdapter.getDefaultAdapter()

private var serverThread: Thread? = null
private var clientThread: Thread? = null

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

override fun onStart() {
super.onStart()
if (checkSelfPermission(
Manifest.permission.ACCESS_FINE_LOCATION) !=
PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_FINE_LOCATION
)
} else {
startOrFinish()
}
}

override fun onPause() {
super.onPause()
serverThread?.interrupt()
serverThread = null
clientThread?.interrupt()
clientThread = null
}

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

private fun startOrFinish() {
if (isBluetoothEnabled()) {
connect()
} else {
finish()
}
}

private fun isBluetoothEnabled(): Boolean {
val enabled = adapter?.isEnabled ?: false
if (!enabled) {
Toast.makeText(this, R.string.enable_bluetooth,
Toast.LENGTH_LONG).show()
}
return enabled
}

private fun connect() {
val myName = adapter?.name
val otherName = if (DEVICE1 == myName) DEVICE2 else DEVICE1
for (device in adapter?.bondedDevices ?:
emptyList<BluetoothDevice>()) {
if (otherName == device.name) {
val serverSocketThread = ServerSocketThread(adapter, TAG, MY_UUID)
serverThread = createAndStartThread(serverSocketThread)
val clientSocketThread = ClientSocketThread(device, MY_UUID)
clientThread = createAndStartThread(clientSocketThread)
input.isEnabled = true
break
}
}
}

private fun createAndStartThread(t: SocketThread): Thread
= thread {
var keepRunning = true
try {
t.start()
Log.d(TAG, "joining " + t.name)
t.join()
t.getSocket()?.run {
Log.d(TAG, String.format("connection type %d for %s",
connectionType, t.name)
)
input.setOnEditorActionListener { _: TextView?, _: Int,
_: KeyEvent? ->
send(outputStream, input.text.toString())
runOnUiThread { input.setText("") }
true
}
while (keepRunning) {
receive(inputStream)?.let {
runOnUiThread { output?.append(it) }
}
}
}
} catch (thr: Throwable) { // InterruptedException, IOException
Log.e(TAG, thr.message, thr)
keepRunning = false
} finally {
Log.d(TAG, "calling cancel() of " + t.name)
t.cancel()
}
}

private fun send(stream: OutputStream, text: String) {
stream.use {
stream.write(text.toByteArray())
}
}

private fun receive(stream: InputStream): String? {
stream.use {
val num = stream.available()
if (num > 0) {
val buffer = ByteArray(num)
var bytesToRead = num
while (bytesToRead > 0) {
val read = stream.read(buffer, num - bytesToRead, bytesToRead)
if (read == -1) {
break
}
bytesToRead -= read
}
return String(buffer)
}
}
return null
}
}

Listing 8.15    Die Klasse »BluetoothChatDemoActivity«

Um zu verstehen, was dort passiert, sehen wir uns die Methode createAndStartThread() genauer an. Ihr wird ein noch nicht laufender Thread übergeben, der eine BluetoothSocket-Instanz ermittelt. Die ist ja für Client und Server unterschiedlich. Nach Start des Threads mit start() warte ich mit join(), bis er beendet wurde. Jetzt kann ich die Methode getSocket() aufrufen. Dieser ganze Aufwand ist nötig, weil nicht absehbar ist, ob die App als Bluetooth-Client oder -Server fungieren soll. Ein join() kehrt zurück, das andere wartet.

BluetoothSocket-Instanzen stellen die Methoden getOutputStream() und getInputStream() zur Verfügung. In idiomatischem Kotlin-Code schreibt man stattdessen inputStream und outputStream. Sie werden verwendet, um Daten zu schreiben oder zu lesen. Wie das funktioniert, ist in den kurzen privaten Methoden send() und receive() zu sehen. Letztlich reduziert sich die Aufgabe darauf, Strings und ByteArrays umzuwandeln. Bitte denken Sie daran, in Ihrer App die Methode runOnUiThread() aufzurufen, wenn Sie in einem eigenen Thread auf Bedienelemente zugreifen möchten.

[»]  Hinweis

Um den Programmcode kurz zu halten, habe ich weitgehend auf eine Fehlerbehandlung verzichtet. Beispielsweise funktioniert das Chatten nicht mehr, wenn Sie eines der beiden Geräte drehen. Um das Problem zu beheben, könnten Sie in regelmäßigen Abständen einen kurzen Text senden. Den müsste die App beim Eintreffen ignorieren. Schlägt der Sendevorgang fehl, wissen Sie, dass keine Verbindung mehr besteht. Sie könnten diese dann erneut initiieren.

Wenn BluetoothChatDemoActivity verlassen wird, müssen alle laufenden Threads mit interrupt() gestoppt werden. Das geschieht in onPause(). Hierzu fangen die Threads unter anderem InterruptedException und IOException ab, sorgen für das Beenden der run()-Methode und erledigen Aufräumarbeiten (cancel()). Damit möchte ich meinen Rundgang durch klassisches Bluetooth beenden. Im folgenden Abschnitt sehen wir uns das sehr stromsparende Bluetooth Low Energy an.

 
Zum Seitenanfang

8.3.3    Bluetooth Low Energy Zur vorigen ÜberschriftZur nächsten Überschrift

Bluetooth Low Energy (BLE) ist seit Android 4.3 (API-Level 18) in die Plattform integriert. Der Stromverbrauch ist im Vergleich zu klassischem Bluetooth deutlich geringer. Deshalb wird die Technologie gern im Internet der Dinge (Internet of Things, IoT) eingesetzt. Anwendungsfälle sind beispielsweise die Übertragung kleiner Datenmengen sowie die Interaktion mit Näherungssensoren wie Google Beacons[ 13 ](https://developers.google.com/beacons/) und iBeacons[ 14 ](https://developer.apple.com/ibeacon/). Wie Sie auf BLE-Geräte zugreifen, demonstriere ich Ihnen anhand meines Beispiels BLEScannerDemo. Die App ist in Abbildung 8.10 zu sehen. Sie sucht nach Geräten in Reichweite und zeigt deren Adressen als Liste an. Tippen Sie einen Eintrag an, um technische Informationen zu dem Gerät anzuzeigen.

Die App »BLEScannerDemo«

Abbildung 8.10    Die App »BLEScannerDemo«

Auch für Bluetooth LE muss Ihre App android.permission.BLUETOOTH und android.permission.ACCESS_FINE_LOCATION anfordern. Um Geräte zu finden, ist auch android.permission.BLUETOOTH_ADMIN erforderlich. Ferner sollten Sie im Manifest Ihrer App mit

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

sicherstellen, dass Bluetooth vorhanden ist. Lassen Sie uns nun einen Blick auf die Hauptklasse BLEScannerActivity werfen. Sie ist in Listing 8.16 zu sehen. Ihr grundsätzlicher Aufbau ähnelt den vorangehenden Beispielen. Zuerst wird geprüft, ob der Benutzer die Berechtigung ACCESS_FINE_LOCATION erteilt hat. Ist dies der Fall, wird in isBluetoothEnabled() die Referenz auf ein Objekt des Typs BluetoothAdapter ermittelt. Allerdings verwende ich diesmal die Methode getAdapter() der Klasse BluetoothManager. Ein entsprechendes Objekt wird mit getSystemService(BluetoothManager::class.java) ermittelt. Welche Variante Sie wählen, hängt davon ab, ob Sie weitere Methoden von BluetoothManager aufrufen wollen.

In der privaten Methode scan() wird die Suche nach Geräten gestartet bzw. beendet. Hierfür wird ein Objekt des Typs BluetoothLeScanner verwendet. Werden Geräte gefunden, ruft Android die Methoden onScanResult() oder onBatchScanResults() der Klasse ScanCallback auf. Meine Implementierungen verzweigen in die private Methode updateData(). Sie erweitert die Geräteliste um einen Eintrag, sofern das Gerät nicht schon vorher hinzugefügt wurde. Tippen Sie ein Listenelement an, werden in der unteren Bildschirmhälfte technische Informationen angezeigt. Dies geschieht in der Methode info().

package com.thomaskuenneth.androidbuch.blescannerdemo

import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Bundle
import android.util.Log
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

private const val REQUEST_FINE_LOCATION = 321
private val TAG = BLEScannerDemoActivity::class.simpleName
class BLEScannerDemoActivity : AppCompatActivity() {

private val scanCallback = object : ScanCallback() {
override fun onScanFailed(errorCode: Int) {
Toast.makeText(this@BLEScannerDemoActivity,
getString(R.string.error, errorCode), Toast.LENGTH_LONG).show()
}

override fun onScanResult(callbackType: Int,
result: ScanResult?) {
updateData(result)
}

override fun onBatchScanResults(results: List<ScanResult?>) {
for (result in results) {
updateData(result)
}
}
}

private val gattCallback = object : BluetoothGattCallback() {
override fun onServicesDiscovered(gatt: BluetoothGatt,
status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
logGattServices(gatt)
} else {
Log.d(TAG, "onServicesDiscovered: $status")
}
gatt.close()
}

override fun onConnectionStateChange(gatt: BluetoothGatt,
status: Int, newState: Int) {
Log.d(TAG, "Status der Verbindung: $newState")
}
}

private lateinit var listAdapter: ArrayAdapter<String>
private val scanResults = HashMap<String?, ScanResult?>()
private var adapter: BluetoothAdapter? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
listAdapter = ArrayAdapter(this,
android.R.layout.simple_list_item_1)
lv.adapter = listAdapter
lv.setOnItemClickListener { _, _, pos, _ ->
val address = listAdapter.getItem(pos)
val result = scanResults[address]
info(result)
}
}

override fun onStart() {
super.onStart()
adapter = null
listAdapter.clear()
scanResults.clear()
tv.text = ""
if (checkSelfPermission(ACCESS_FINE_LOCATION) !=
PERMISSION_GRANTED) {
requestPermissions(arrayOf(ACCESS_FINE_LOCATION),
REQUEST_FINE_LOCATION)
} else {
startOrFinish()
}
}

override fun onPause() {
super.onPause()
if (adapter != null) {
scan(false)
}
}

override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray) {
if (requestCode == REQUEST_FINE_LOCATION &&
grantResults.isNotEmpty() &&
grantResults[0] == PERMISSION_GRANTED) {
startOrFinish()
} else {
finish()
}
}

private fun startOrFinish() {
if (isBluetoothEnabled()) {
scan(true)
} else {
finish()
}
}

private fun isBluetoothEnabled(): Boolean {
adapter = null
var enabled = false
getSystemService(BluetoothManager::class.java)?.let {
adapter = it.adapter
enabled = adapter?.isEnabled ?: false
}
if (!enabled) {
Toast.makeText(this, R.string.enable_bluetooth, Toast.LENGTH_LONG)
.show()
}
return enabled
}

private fun scan(enable: Boolean) {
val scanner = adapter?.bluetoothLeScanner
if (enable) {
scanner?.startScan(scanCallback)
} else {
scanner?.stopScan(scanCallback)
}
}

private fun updateData(result: ScanResult?) {
result?.device?.address.let {
if (!scanResults.containsKey(it)) {
listAdapter.add(it)
listAdapter.notifyDataSetChanged()
}
scanResults.put(it, result)
}
}

private fun info(result: ScanResult?) {
tv.text = result?.toString()
val device = result?.device
val gatt = device?.connectGatt(this,
true, gattCallback)
val started = gatt?.discoverServices()
Log.d(TAG, "discoverServices(): $started")
}

private fun logGattServices(gatt: BluetoothGatt) {
gatt.services.forEach {
Log.d(TAG, "Service " + it.uuid.toString())
}
}
}

Listing 8.16    Die Klasse »BLEScannerActivity«

Auch BLE-Geräte basieren auf Profilen. Ein Profil beschreibt, wie ein Gerät in einem bestimmten Anwendungsfall arbeitet. Bei der Messung der Herzfrequenz werden andere Daten übertragen als beim Ermitteln des Akkustandes. Daraus ergibt sich, dass Geräte mehrere Profile implementieren können. Das Generic Attribute Profile (GATT) ist eine allgemeine Spezifikation, um kleine Datenmengen zu senden und zu empfangen. Diese werden Attribute genannt. GATT bildet die Basis für die meisten aktuellen BLE-Profile. Attribute werden mit dem Attribute Protocol (ATT) übertragen. Sie gibt es in verschiedenen Ausprägungen, beispielsweise Charakteristiken und Services. Ein Service fasst Charakteristiken zusammen. Beispielsweise gehört zum Service Herzfrequenzmonitor die Charakteristik Herzfrequenzmessung.

Um nach Diensten zu suchen, rufen Sie die BluetoothDevice-Methode connectGatt() mit einem Objekt des Typs BluetoothGattCallback auf. Meine Implementierung überschreibt zum Beispiel die Methode onServicesDiscovered(). Wurden Dienste gefunden (status == BluetoothGatt.GATT_SUCCESS), verzweige ich nach logGattServices(). Nach der Verwendung muss die BluetoothGatt-Instanz mit close() geschlossen werden.

Welche Services und Charakteristiken ein BLE-Gerät zur Verfügung stellt, sollte dessen Dokumentation zu entnehmen sein. Dort ist dann hoffentlich auch beschrieben, welche Werte Sie mit Ihrer App setzen und abfragen können. Damit verlassen wir den Bereich der Nahfunktechnik. Im folgenden Abschnitt zeige ich Ihnen, wie Sie Fingerabdrucksensoren und Irisscanner verwenden, um Nutzer Ihrer App zu authentifizieren.

 


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