feat: foreground service & automatic reconnecting

This commit is contained in:
Stefan Zollinger
2023-10-26 11:26:00 +02:00
parent 8a49edd6fb
commit b2d81d6fcd
6 changed files with 145 additions and 56 deletions

View File

@@ -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)
}
}
}

View File

@@ -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<BluetoothDevice, BluetoothGatt>()
private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
@@ -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)

View File

@@ -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<Measurement> {
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
}
}
)
}

View File

@@ -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