From b2d81d6fcd3f3e0914bf7ae8fe386ff947971d21 Mon Sep 17 00:00:00 2001 From: Stefan Zollinger Date: Thu, 26 Oct 2023 11:26:00 +0200 Subject: [PATCH] feat: foreground service & automatic reconnecting --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 5 +- .../example/sensortestingapp/BLEService.kt | 69 ++++++++++++ .../sensortestingapp/ConnectionManager.kt | 14 ++- .../example/sensortestingapp/KirbyDevice.kt | 100 +++++++++--------- .../example/sensortestingapp/MainActivity.kt | 11 +- 6 files changed, 145 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/com/example/sensortestingapp/BLEService.kt diff --git a/app/build.gradle b/app/build.gradle index 1bc459a..1ccc02f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,6 +22,8 @@ android { buildConfigField "String", "API_BASE_URL", "\"${props['apiBaseUrl']}\"" buildConfigField "String", "API_KEY", "\"${props['apiKey']}\"" + buildConfigField "String", "SERVICE_UUID", "\"6e400001-b5a3-f393-e1a9-e50e24dcac9e\"" + buildConfigField "String", "CHAR_UUID", "\"6e400005-b5a3-f393-e1a9-e50e24dcac9e\"" } buildTypes { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7508a96..d4ba815 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,9 @@ android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" /> + + @@ -47,6 +49,7 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/example/sensortestingapp/BLEService.kt b/app/src/main/java/com/example/sensortestingapp/BLEService.kt new file mode 100644 index 0000000..f9eba08 --- /dev/null +++ b/app/src/main/java/com/example/sensortestingapp/BLEService.kt @@ -0,0 +1,69 @@ +package com.example.sensortestingapp + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat + +class BLEService : Service() { + private val CHANNEL_ID = "BLEService Kotlin" + + companion object { + + fun startService(context: Context, message: String) { + val startIntent = Intent(context, + BLEService::class.java) + startIntent.putExtra("inputExtra", message) + ContextCompat.startForegroundService(context, startIntent) + } + + fun stopService(context: Context) { + val stopIntent = Intent(context, BLEService::class.java) + context.stopService(stopIntent) + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + + //do heavy work on a background thread + val input = intent?.getStringExtra("inputExtra") + createNotificationChannel() + val notificationIntent = Intent(this, MainActivity::class.java) + + val pendingIntent: PendingIntent = + Intent(this, MainActivity::class.java).let { notificationIntent -> + PendingIntent.getActivity(this, 0, notificationIntent, + PendingIntent.FLAG_IMMUTABLE) + } + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("BLEService Service Kotlin Example") + .setContentText(input) + //.setSmallIcon(R.drawable.ic_notification) + .setContentIntent(pendingIntent) + .build() + + startForeground(1, notification) + //stopSelf(); + return START_NOT_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val serviceChannel = NotificationChannel(CHANNEL_ID, "BLEService Service Channel", + NotificationManager.IMPORTANCE_DEFAULT) + + val manager = getSystemService(NotificationManager::class.java) + manager!!.createNotificationChannel(serviceChannel) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt b/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt index f6ea2fb..39576ef 100644 --- a/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt +++ b/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt @@ -1,6 +1,7 @@ package com.example.sensortestingapp import android.annotation.SuppressLint +import android.app.Service import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.BOND_BONDED @@ -14,12 +15,14 @@ import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.os.ParcelUuid import android.util.Log import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -198,7 +201,7 @@ fun ByteArray.toHexString(): String = joinToString(separator = "", prefix = "0x") { String.format("%02X", it) } @SuppressLint("MissingPermission") -class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { +class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { private val deviceGattMap = ConcurrentHashMap() private val operationQueue = ConcurrentLinkedQueue() @@ -209,6 +212,8 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() + private val scanFilters = listOf( ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(BuildConfig.SERVICE_UUID)).build()) + private val bleScanner by lazy { bleAdapter.bluetoothLeScanner } @@ -274,6 +279,8 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { broadcastReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED) ) + + } var isScanning = false @@ -307,7 +314,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { fun startScan() { if (!isScanning) { isScanning = true - bleScanner.startScan(null, scanSettings, scanCallback) + bleScanner.startScan( null, scanSettings, scanCallback) } } @@ -428,7 +435,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { with(operation) { Log.w("ConnectionManager", "Connecting to ${device.address}") device.connectGatt( - context, false, callback, BluetoothDevice.TRANSPORT_LE + context, true, callback, BluetoothDevice.TRANSPORT_LE ) } return @@ -520,6 +527,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { is SetNotification -> with(operation) { val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId); + if (characteristic == null) { Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!") signalEndOfOperation(operation) diff --git a/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt b/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt index 3db84e9..541f5d8 100644 --- a/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt +++ b/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt @@ -25,9 +25,8 @@ import java.util.UUID import java.util.stream.Collectors -// Kirby service uuid: 6e400001-b5a3-f393-e1a9-e50e24dcac9e -private val DEMO_SERVICE_UUID = UUID.fromString("6e400001-b5a3-f393-e1a9-e50e24dcac9e") -private val DEMO_CHAR_UUID = UUID.fromString("6e400005-b5a3-f393-e1a9-e50e24dcac9e") +private val SERVICE_UUID = UUID.fromString(BuildConfig.SERVICE_UUID) +private val CHAR_UUID = UUID.fromString(BuildConfig.CHAR_UUID) enum class DeviceStatus { CONNECTED, BONDED, SUBSCRIBED, MISSING @@ -40,8 +39,27 @@ class KirbyDevice( private val bleDevice: BluetoothDevice, private val onStateChange: (device: KirbyDevice) -> Unit, -) : BleListener(bleDevice.address), DeviceListEntry { - private val queue : RequestQueue = Volley.newRequestQueue(context) + ) : BleListener(bleDevice.address), DeviceListEntry { + private val queue: RequestQueue = Volley.newRequestQueue(context) + + fun subscribe() { + + connectionManager.enableNotification( + bleDevice, SERVICE_UUID, CHAR_UUID + ) + } + + fun readIaq() { + connectionManager.readChar(bleDevice, SERVICE_UUID, CHAR_UUID) + } + + override fun onSuccessfulCharRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic + ) { + addMeasurement(characteristic) + onStateChange(this) + } override fun onScanResult(callbackType: Int, result: ScanResult) { rssi = result.rssi @@ -51,6 +69,7 @@ class KirbyDevice( override fun onConnect(gatt: BluetoothGatt) { statuses.add(DeviceStatus.CONNECTED) statuses.remove(DeviceStatus.MISSING) + onStateChange(this) } @@ -65,14 +84,6 @@ class KirbyDevice( onStateChange(this) } - override fun onSuccessfulCharRead( - gatt: BluetoothGatt, - characteristic: BluetoothGattCharacteristic - ) { - addMeasurement(characteristic) - onStateChange(this) - } - override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { addMeasurement(characteristic) onStateChange(this) @@ -82,6 +93,7 @@ class KirbyDevice( gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, ) { + statuses.add(DeviceStatus.SUBSCRIBED) onStateChange(this) } @@ -116,25 +128,23 @@ class KirbyDevice( override var rssi: Int? = null private fun addMeasurement(characteristic: BluetoothGattCharacteristic) { - if (characteristic.service.uuid == DEMO_SERVICE_UUID && characteristic.uuid == DEMO_CHAR_UUID) { - - val hexPayload = characteristic.value.toHexString() - val payload = Payload(hexPayload) - val base64Payload = Base64.getEncoder().encodeToString(characteristic.value) - Log.i("BleListener", "Demo char received: $payload") - measurements.add(payload) - publishMeasurement(base64Payload) - - } + val hexPayload = characteristic.value.toHexString() + val payload = Payload(hexPayload) + val base64Payload = Base64.getEncoder().encodeToString(characteristic.value) + Log.i("BleListener", "Char received: $payload") + measurements.add(payload) + publishMeasurement(base64Payload) } private fun publishMeasurement(payload: String) { - val accessKey = BuildConfig.API_BASE_URL - val url = BuildConfig.API_KEY - val eui = "0000${bleDevice.address}" + val accessKey = BuildConfig.API_KEY + val url = BuildConfig.API_BASE_URL + val eui = "0000${bleDevice.address.replace(":", "")}" val postData = JSONObject() + try { + Log.i("POST", "Transmitting for $eui: $payload") postData.put("accessKey", accessKey) postData.put("metricPayload", payload) postData.put("eui", eui) @@ -145,7 +155,7 @@ class KirbyDevice( val request = JsonObjectRequest( Request.Method.POST, url, postData, { response -> - Log.i("sendDataResponse","Response is: $response") + Log.i("sendDataResponse", "Response is: $response") } ) { error -> error.printStackTrace() } @@ -257,7 +267,7 @@ class KirbyDevice( } override fun execute() { - connectionManager.readChar(bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID) + readIaq() } }) @@ -272,9 +282,7 @@ class KirbyDevice( } override fun execute() { - connectionManager.enableNotification( - bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID - ) + subscribe() } }) } @@ -307,7 +315,7 @@ class KirbyDevice( override fun execute() { connectionManager.disableNotification( - bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID + bleDevice, SERVICE_UUID, CHAR_UUID ) } }) @@ -341,7 +349,6 @@ data class Payload( ) - fun bytesToUInt16(arr: ByteArray, start: Int): Int { return ByteBuffer.wrap(arr, start, 2) .order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF @@ -371,21 +378,18 @@ private fun payloadToMeasurements(payload: Payload): List { override fun getIcon(): Int? { return R.drawable.baseline_access_time_24 } - }, - object : Measurement { - override fun getLabel(): String { - return "Payload" - } - - override fun getFormattedValue(): String { - return payload.payload - } - - override fun getIcon(): Int? { - return null - } - + }, object : Measurement { + override fun getLabel(): String { + return "Payload" } - ) + override fun getFormattedValue(): String { + return payload.payload + } + + override fun getIcon(): Int? { + return null + } + } + ) } \ No newline at end of file diff --git a/app/src/main/java/com/example/sensortestingapp/MainActivity.kt b/app/src/main/java/com/example/sensortestingapp/MainActivity.kt index 329753a..59e0a42 100644 --- a/app/src/main/java/com/example/sensortestingapp/MainActivity.kt +++ b/app/src/main/java/com/example/sensortestingapp/MainActivity.kt @@ -75,7 +75,7 @@ class MainActivity : AppCompatActivity() { setSupportActionBar(binding.toolbar) - + BLEService.startService(applicationContext, "hello ble service") binding.fab.setOnClickListener { view -> if (!hasRequiredRuntimePermissions()) { @@ -103,10 +103,10 @@ class MainActivity : AppCompatActivity() { } private fun isKirbyDevice(device: BluetoothDevice): Boolean { - Log.i("kby", "found device ${device.name}") return (device.name ?: "").lowercase().contains("kirby") } + private fun setupDevicesList() { binding.devicesList.apply { adapter = deviceListAdapter @@ -134,7 +134,8 @@ class MainActivity : AppCompatActivity() { private fun addBondedDevices(): Unit { bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach { - newKirbyDevice(it) + val kirbyDevice = newKirbyDevice(it) + kirbyDevice.subscribe() } } @@ -179,13 +180,15 @@ class MainActivity : AppCompatActivity() { private fun newKirbyDevice(bleDevice: BluetoothDevice): KirbyDevice { - val device = KirbyDevice( this.applicationContext, connectionManager, bleDevice) { + val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice) { val i = kirbyDevices.indexOfFirst { d -> d === it } runOnUiThread { deviceListAdapter.notifyItemChanged(i) } } connectionManager.register(device) + connectionManager.connect(bleDevice) + connectionManager.discoverServices(bleDevice) kirbyDevices.add(device) deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1) return device