In diesem Beitrag zeige ich dir wie du dir eine eigene kleine Android App zum suchen von iBeacons erstellen kannst.
Den iBeacon habe ich dir bereits kurz im ersten Teil Bluetooth 4.0 iBeacon – Teil1 vorgestellt.
Eine Android App zum suchen von Beacons bzw. Bluetooth 4.0 (BLE) Geräte gibt es im Google PlayStore so einige. Auch ich habe bereits eine App dazu geschrieben jedoch habe ich diese wieder aus dem Google PlayStore entfernt da diese nicht konform mit den Richtlinien von Google war und die Anpassung in keinem verhältnis zu der Nutzeranzahl lag.
Im nachfolgenden möchte ich dir nun erläutern wie du dir deine eigene App zum suchen und auswerten von Bluetooth 4.0 Geräte erstellst. Die App welche dabei herauskommt hat die selben funktionalitäten wie die App welche ich bereits im PlayStore veröffentlicht habe, jedoch nicht vom Design her. Das Design wird sehr einfach gehalten um diesen Beitrag nicht unnötig kompliziert zu machen denn das suchen und auswerten der Geräte wird schon etwas kompliziert (aber alles im machbaren) werden.
benötigte Resourcen für dieses Projekt
Für die Entwicklung der App benötigst du:
- ein Android Handy*,
- ein Datenkabel
- die Anwendung Android Studio,
- einen iBeacon oder vergleichbares Bluetooth 4.0 Gerät (zbsp. eine Fitnessuhr)
* Das Android Handy muss mindestens den Bluetooth Standard 4.0 (Bluetooth Low Energy) untersützen.
Da wir mir Bluetooth kommunizieren wollen müssen wir auf echte Hardware zurückgreifen, denn der Emulator von Android Studio kann keine Bluetoothkommunikation simulieren (nicht einmal über den Host).
Ich setze zunächst voraus das du die Anwendung Android Studio installiert und dein Handy in den Entwicklermodus gestellt hast.
Aufbau des Projektes
Das Projekt möchte ich gerne in mehrere kleine Schritte unterteilen wobei jeder Schritt in sich geschlossen ist und ein lauffähiges “Produkt” hervorbringt. Ich biete dir nach jedem fertigen Zwischenschritt die App zum Download an so kannst du dir auch ggf. etwas Schreibarbeit sparen.
Hinweis: Werden Texte an Schaltflächen oder Textelemente (TextView, EditText usw.) vergeben so werden diese internationalisiert abgelegt, zum einen auf deutsch und auf englisch.
Schritt 1 – einrichten des Projektes
Zunächst erstellen wir uns ein neues Projekt in Android Studio.
Zunächst wähle ich eine leere Vorlage aus (blau markiert) und bestätige den Dialog mit der Schaltfläche “Next”. Als nächstes konfigurieren wir unser Projekt und setzen neben die Pfade zur Ablage der Dateien auch die Zielumgebung. Da ich ein Huawai P20 Lite mit Android 9 habe verwende ich auch die “Minimum SDK” “API 28: Android 9.0 (Pie)”. Solltest du ein älteres Handy verwenden so musst du dieses entsprechend anpassen.
Wenn du deine App später im Google Playstore veröffentlichen möchtest dann solltest du auch hier bedenken das nicht jeder die aktuellste Android Version hat und du somit potentielle Nutzer deiner App aussperrst.
Wir haben nun ein leeres Projekt mit einer Activity eingerichtet, dieses können wir bereits auf unser Gerät installieren.
Download
Schritt 2 – GUI Elemente für das Suchen implementieren
Im zweiten Schritt implementieren wir folgende GUI Elemente:
- eine Schaltfläche (Common >Button),
- ID – searchBtn
- Text – “Suchen…”
- eine Liste (Legacy > ListView)
- ID – deviceListView
Über den Button starten bzw. stoppen wir die Suche nach Geräte und in der Liste sollen die gefundenen Geräte angezeigt werden.
ersetzen des Layouts
Bevor wir die GUI Elemente in unser Layout einbauen werden wir das automatisch erzeugte ConstraintLayout
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
gegen ein LinearLayout mit der Ausrichtung “vertikal” austauschen.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity">
Den schließenden XML Tag ändert die Anwendung Android Studio selbständig (wie ich finde ein nettes Feature).
platzieren der GUI Elemente
Nun können wir die beiden GUI Elemente per Drag’n Drop platzieren.
der Button
Als erstes den Button, diesen findest du unter der Kategorie “Buttons”.
Der Text wird internationalisiert in den Sprachen deutsch und als alternative englisch abgelegt. Des Weiteren erhält der Button einen Abstand von 15dp nach oben (layout_marginTop) und nach unten (layout_marginBottom).
<Button android:id="@+id/searchBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="15dp" android:layout_marginBottom="15dp" android:text="@string/search" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
die ListeView
Nachdem wir den Button platziert haben setzen wir die ListView. Der ListView setzen wir zusätzlich einen inneren Abstand (padding) von 10dp.
<ListView android:id="@+id/deviceListView" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp" />
Unser Layout im gesamten:
Dateiname: “activity_main.xml”
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <Button android:id="@+id/searchBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="15dp" android:layout_marginBottom="15dp" android:text="@string/search" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ListView android:id="@+id/deviceListView" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp" /> </LinearLayout>
Als nächstes müssen wir für unsere ListView ein Layout für die Einträge erstellen. Wir wollen zunächst nur eine laufende Nummer und den Namen sowie die MAC-Adresse anzeigen lassen.
Dateiname: “bleinfo_layout.xml”
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <TextView android:id="@+id/idTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="0.25" /> <TextView android:id="@+id/nameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="0.85" /> </LinearLayout>
Schritt 3 – Objekt zum halten der Daten eines Bluetooth Gerätes
Bevor wir unsere ListView mit Daten befüllen können, müssen wir uns ein Objekt zum halten der Daten eines Bluetooth Gerätes definieren. Da wir zunächst nur den Namen und die MAC-Adresse in unserer Liste anzeigen lassen wollen benötigen wir die Felder name & macadresse vom Typ String.
public class BLEInfo { private String name; private String macAdresse; public BLEInfo(String name, String macAdresse) { this.name = name; this.macAdresse = macAdresse; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getMacAdresse() { return macAdresse; } public void setMacAdresse(String macAdresse) { this.macAdresse = macAdresse; } }
Schritt 4 – Erzeugen einer Liste mit Beispielwerten und anzeigen in der ListView
Bevor wir nun nach Bluetooth Geräten in der Umgebung suchen wollen wir unsere ListView zunächst mit Beispielwerten befüllen. Dazu legen wir uns in der Klasse “MainActivity.java” in der Methode “onCreate” eine Liste mit BLEInfo Objekten an.
List<BLEInfo> values = new ArrayList<>(); for (int i = 0; i < 200; i++) values.add(new BLEInfo("Band1", String.valueOf(i)));
Wir haben hier in der MAC-Adresse zwar eine einzelne Zahl stehen aber für einen Test ist das erstmal ausreichend.
Nun müssen wir diese Beispielwerte “irgendwie” in unserer Liste anzeigen. Dazu müssen wir uns einen neuen ArrayAdapter schreiben welcher unser Layout sowie unsere Beispielwerte enthält.
Da unsere App zunächst nur über einen geringen Funktionsumfang verfügt erstelle ich diesen ArrayAdapter als innere Klasse in MainActivity.java.
Die Klasse muss den ArrayAdapter erweitern und als Generischen Typen übergeben wir unser BLEInfo Objekt.
private class DeviceArrayAdapter extends ArrayAdapter<BLEInfo> {...}
Damit wir später auf die Liste der BLEInfo Objekte zurückgreifen können, legen wir uns ein entsprechendes Feld an.
private List<BLEInfo> bleInfoList;
Im Konstruktor (welchen wir implementieren müssen) passen wir lediglich an das der Context und die Liste zusätzlich an unsere bestehenden Felder weitergereicht werden.
public DeviceArrayAdapter(Context context, List<BLEInfo> bleInfoList) { super(context, R.layout.bleinfo_layout, bleInfoList); this.ctx = context; this.bleInfoList = bleInfoList; }
Nun müssen wir die Methode “getView” überschreiben, diese wird angesteuert wenn die ListView angezeigt bzw. aktualisiert wird.
@Override public View getView(int position, View convertView, ViewGroup parent) {...}
Zunächst laden wir unser Layout welches wir bereits im Schritt 3 erstellt haben.
LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.bleinfo_layout, parent, false);
Als Parameter der Methode “getView” haben wir “int position” da die Methode durch die Liste der BLEInfo Objekte iteriert können wir mit diesem Index das passende Objekt aus unserer Liste laden und in die entsprechenden Textfelder die Werte schreiben.
BLEInfo bleInfo = bleInfoList.get(position); TextView idTextView = view.findViewById(R.id.idTextView); idTextView.setText(String.valueOf(position)); TextView nameTextView = view.findViewById(R.id.nameTextView); nameTextView.setText(getString(R.string.namemacadresse, bleInfo.getName(), bleInfo.getMacAdresse()));
Die gesamte innere Klasse “DeviceArrayAdapater”:
private class DeviceArrayAdapter extends ArrayAdapter<BLEInfo> { private Context ctx; private List<BLEInfo> bleInfoList; public DeviceArrayAdapter(Context context, List<BLEInfo> bleInfoList) { super(context, R.layout.bleinfo_layout, bleInfoList); this.ctx = context; this.bleInfoList = bleInfoList; } @Override public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.bleinfo_layout, parent, false); BLEInfo bleInfo = bleInfoList.get(position); TextView idTextView = view.findViewById(R.id.idTextView); idTextView.setText(String.valueOf(position)); TextView nameTextView = view.findViewById(R.id.nameTextView); nameTextView.setText(getString(R.string.namemacadresse, bleInfo.getName(), bleInfo.getMacAdresse())); return view; } }
Nun müssen wir zu unserer Methode “onCreate” zurück kehren und dort an der ListView den neuen DeviceArrayAdapter erzeugen und setzen.
ListView deviceListView = findViewById(R.id.deviceListView); DeviceArrayAdapter deviceArrayAdapter = new DeviceArrayAdapter(this, values); deviceListView.setAdapter(deviceArrayAdapter);
Wenn wir nun unsere App starten sehen wir einen Button sowie eine Liste mit 200 Einträgen (0..199).
Download
Hier nun der Download mit den Layouts und der Klassen zum bequemen Download als “Android Studio Projekt”.
Schritt 5 – suchen von Bluetooth Geräte
Berechtigungen zum Zugriff auf den Bluetoothadapter erhalten
In diesem Schritt wollen wir uns nun daran machen nach Bluetooth Geräte in der Umgebung zu suchen. Damit wir dieses mit unserer App können müssen wir zunächst die Berechtigung zum Zugriff auf den Bluetoothadapter des Gerätes erhalten. Diese Berechtigung (permission) setzen wir in der Datei “AndroidManifest.xml”.
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
Zusätzlich benötigen wir noch die Berechtigung zum abrufen des Standortes:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Diese Berechtigungen werden bei der Installation der App aus dem GooglePlayStore abgefragt.
Während der Entwicklung der App müssen wir jedoch die Berechtigung zum Zugriff auf den Standort manuell in den Einstellungen setzen.
Da der Benutzer hier auch die Berechtigung untersagen kann müssen wir dieses später vor der Verwendung des Bluetoothadapters prüfen.
Bibliothek zum Zugriff auf den Bluetoothadapter
Es gibt diverse Bibliotheken für den einfachen Zugriff auf den Bluetoothadapter. Man muss hier eigentlich keine externe Bibliothek nutzen aber der Zugriff und das lesen ist doch sehr kompliziert gelöst.
Ich bin bei der Suche nach einer entsprechenden Bibliothek bei http://polidea.github.io/RxAndroidBle/ hängen geblieben welche ich dir hier etwas näher bringen möchte. Leider gibt es zu dieser Bibliothek sehr wenig Dokumentation aber auf GitHub findet man ein Repository mit einigen Beispielen somit kann man sich dort einiges anschauen.
Wir müssen diese Bibliothek in der Datei “build.gradle (Module: app)” im Abschnitt “dependency” hinzufügen.
implementation "com.polidea.rxandroidble2:rxandroidble:1.11.1"
clickEvent auf dem Button anlegen
Die Suche nach Bluetooth Geräte soll durch die Schaltfläche “Suchen…” gestartet und auch gestoppt werden dazu benötigen wir zunächst ein Event.
Button searchBtn = findViewById(R.id.searchBtn); searchBtn.setOnClickListener(this::startSearch);
Solltest du dein Android Projekt wie in diesem Beitrag gezeigt eingerichtet haben so sollte der Text “this::startSearch” rot unterstrichen sein. Dieses liegt daran dass, der Wert für “language Level” auf Java 7 eingestellt ist und dort keine Lambda Expressions existieren bzw. nicht verwendet werden können. Hier unterstützt uns die Entwicklungsumgebung und schlägt bereits vor dass, das Wert auf “8” angepasst werden soll.
Diese bestätigen wir und das Projekt wird neu gebaut.
Nun legen wir uns die Methode “startSearch” an:
private void startSearch(View v){ }
aktivieren von Bluetooth
Damit wir Bluetooth Geräte suchen & finden können muss natürlich Bluetooth aktiviert sein, dieses prüfen wir bzw. wir fordern den Benutzer dazu auf diesen Adapter zu aktivieren. Die kleine Funktion lagere ich in eine extra Funktion aus denn unsere eigentliche Funktion wird sowieso schon sehr umfangreich.
private void requestBluetooth(){ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); int REQUEST_ENABLE_BT = 1; startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }
Den Aufruf dieser Funktion fügen wir in die Aktion (Funktion “startSearch”) des Buttons ein.
private void startSearch(View v){ requestBluetooth(); }
Wenn wir nun die App starten und die Schaltfläche “Suchen…” betätigen, dann wird die nachfolgende Abfrage angezeigt.
Dieser Dialog wird jedoch nur angezeigt wenn der Bluetoothadapter deaktiviert ist, sollte dieser bereits aktiviert sein dann wir dieser Dialog NICHT angezeigt.
prüfen ob Bluetooth aktiviert ist
Wir können in diesem Dialog 2 Schaltflächen betätigen zum einen “Zulassen” und “Ablehnen” wir können aber nicht davon ausgehen das der Benutzer der App immer auf “Zulassen” klickt. Somit müssen wir danach prüfen ob wirklich der Bluetoothadapter aktiviert ist.
private boolean isBluetoothActive() { BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mBluetoothAdapter == null) { return false; } else if (!mBluetoothAdapter.isEnabled()) { return false; } return true; }
Das Ergebnis dieser Funktion wollen wir wiederum in unserer Aktion der Schaltfläche verwenden aber nur wenn “Boolean.False” geliefert wird.
private void startSearch(View v) { requestBluetooth(); if(!isBluetoothActive()){ Snackbar.make(v, R.string.msg_bluetooth_not_active, Snackbar.LENGTH_LONG).show(); return; } }
Der einfachheitshalber geben wir hier nur ein Hinweis über die Snackbar zurück. Für die Snackbar benötigst du eine zusätzliche Dependency, diese wird jedoch durch die Entwicklungsumgebung für dich hinzugefügt (Alternativ kannst du jedoch auch ein “Toast” nehmen.)
Man könnte auch hier einen ausführlichen Fehlerdialog implementieren jedoch wollen wir uns zunächst darauf konzentrieren Bluetooth Geräte zu finden.
Nachdem aufrufen des Hinweises verlassen wir die Aktion denn wir haben ja kein Bluetooth zur Verfügung.
suchen nach Bluetooth Geräte
Wie bereits erwähnt wollen wir die Bibliothek “RxAndroidBle” verwenden. Zunächst legen wir uns ein Feld für den RxBleClient an.
private RxBleClient rxBleClient;
diesen Client initialisieren wir in der Methode “onCreate”.
@Override protected void onCreate(Bundle savedInstanceState) { ... rxBleClient = RxBleClient.create(this); }
Neben dem RxBleClient benötigen wir ein zusätzliches Feld für ein Disposeable. Dieses Objekt verwenden wir um nach den eigentlichen Geräten zu suchen.
private Disposable scanDisposable;
In unserer Funktion zum suchen nach Bluetooth Geräte prüfen wir zunächst ob dieses Objekt initialisiert ist (also ungleich NULL). Wenn dieses so ist dann soll der Scan abgebrochen und die Funktion verlassen werden. Wenn dieses NICHT so ist dann prüfen wir ob eine RuntimePermission zum Scannen nach Bluetooth Geräte exisitiert. (Wenn nicht wir dieser gegeben.)
private void startSearch(View v) { ... if (scanDisposable != null) { scanDisposable.dispose(); return; } else { if (!rxBleClient.isScanRuntimePermissionGranted()) { ActivityCompat.requestPermissions(this, new String[]{rxBleClient.getRecommendedScanRuntimePermissions()[0]}, REQUEST_PERMISSION_BLE_SCAN); } ... }
Nehmen wir nun an wir klicken zum ersten mal auf die Schaltfläche “Suchen…” so soll gesucht werden.
scanDisposable = rxBleClient.scanBleDevices( new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .build(), new ScanFilter.Builder().build()).doFinally(this::dispose) .subscribe(this::addScanResult, throwable -> { Log.e(TAG, throwable.getMessage()); Snackbar.make(v, throwable.getMessage(), Snackbar.LENGTH_LONG).show(); });
Wir benötigen zwei suchsätzliche Methoden, zum einen dispose und zum anderen addScanResult.
private void dispose() { scanDisposable = null; } private void addScanResult(ScanResult scanResult) { }
Die Funktion dispose wird aufgerufen wenn der Scan abgeschlossen ist und die Funktion addScanResult wenn ein Gerät gefunden wurde. Die Funktion addScanResult erhält als Parameter das ScanResult mit den Informationen zum gefundenen Gerät.
Nachdem wir nun echte Bluetooth Geräte finden müssen wir unsere Beispielwerte aus dem Layout entfernen und die Liste öffentlich machen. Des Weiteren muss der DeviceArrayAdapter auch zugänglich sein denn diesen wollen wir ja aktualisieren sobald ein Gerät der Liste hinzugefügt wird.
private List<BLEInfo> values = new ArrayList<>(); private DeviceArrayAdapter deviceArrayAdapter; @Override protected void onCreate(Bundle savedInstanceState) { ... ListView deviceListView = findViewById(R.id.deviceListView); deviceArrayAdapter = new DeviceArrayAdapter(this, values); deviceListView.setAdapter(deviceArrayAdapter); ... }
aufnehmen eines gefunden Bluetooth Gerätes in die Liste
Bevor wir die Daten aus einem ScanResult in die Liste übernehmen prüfen wir erstein mal ob dieses Gerät vielleicht bereits gefunden und somit verarbeitet wurde. Hier nutzen wir die MAC-Adresse des Gerätes denn diese ist einzigartig.
RxBleDevice bleDevice = scanResult.getBleDevice(); boolean deviceExist = false; for(BLEInfo bleInfo: values){ if(bleInfo.getMacAdresse().equalsIgnoreCase(bleDevice.getMacAddress())){ deviceExist = true; break; } }
Wenn also nun das Gerät bereits gefunden und somit verarbeitet wurde dann soll die Funktion wieder verlassen werden.
if(deviceExist){ return; }
Ansonsten sollen die Daten (Name & MAC-Adresse) aufgenommen werden.
String name = bleDevice.getName(); String macAdresse = bleDevice.getMacAddress(); values.add(new BLEInfo(name, macAdresse));
Es kann aber passieren das der Name NULL ist somit müssen wir zusätzlich prüfen ob der Name existiert und wenn nicht eine vernünftige Bezeichnung oder zumindest ein Symbol pflegen. Ich möchte wenn der Name NULL ist einfach nur einen Bindestrich anzeigen.
if (name == null) { name = "-"; }
Nachdem wir die neuen Funktion hinzugefügt haben müssen wir nun den DeviceArrayAdapter aktualisieren. Hier müssen wir lediglich die Funktion
deviceArrayAdapter.notifyDataSetChanged();
aufrufen.
Video
Wenn wir nun die App starten und auf die Schaltfläche “Suchen…” klicken dann wird nach den Bluetooth 4.0 Geräte gesucht und in die Liste aufgenommen.
Download
Hier nun die gesamte App als Android Studio Projekt zum Download.
Fazit & Ausblick
In diesem Beitrag habe ich dir gezeigt wie einfach es ist eine Android App zu schreiben welche nach Bluetooth 4.0 Geräte sucht und Informationen in einer ListView anzeigt. Als nächstes könnte man nun diese App dahin weiter entwickeln das die Signalstärke und Distanz in einem Dialog angezeigt werden.
Hallo,
eine sehr gute Einleitung. Wäre es auch möglich nach dem man die Bluetooth-Geräte herausgefunden hat, diese auch zu lokalisieren also den Standort ungefähr ausfinden?
Viele Grüße
Hi,
das Gerät zu finden und den Abstand zu diesem in Meter zu berechnen ist generell möglich, jedoch kann keine Richtung ermittelt werden.
Es ist also mehr ein Kreis um einen herum welcher in Frage kommt. Man würde also min. 3 solcher Geräte für eine Standortbestimmung benötigen.
Gruß,
Stefan
Vielen dank für die schnelle Antwort. Kommt in den nächsten Teilen zu diesem Thema etwas ? Wie man das am besten Implementieren kann.