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

@@ -22,6 +22,8 @@ android {
buildConfigField "String", "API_BASE_URL", "\"${props['apiBaseUrl']}\"" buildConfigField "String", "API_BASE_URL", "\"${props['apiBaseUrl']}\""
buildConfigField "String", "API_KEY", "\"${props['apiKey']}\"" 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 { buildTypes {

View File

@@ -17,6 +17,9 @@
android:name="android.permission.ACCESS_FINE_LOCATION" android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" /> android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission <uses-permission
android:name="android.permission.BLUETOOTH_SCAN" android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" android:usesPermissionFlags="neverForLocation"
@@ -39,7 +42,6 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SensorTestingApp"> android:theme="@style/Theme.SensorTestingApp">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -47,6 +49,7 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name=".BLEService" android:foregroundServiceType="connectedDevice" />
</application> </application>
</manifest> </manifest>

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 package com.example.sensortestingapp
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Service
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevice.BOND_BONDED import android.bluetooth.BluetoothDevice.BOND_BONDED
@@ -14,12 +15,14 @@ import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings import android.bluetooth.le.ScanSettings
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.ParcelUuid
import android.util.Log import android.util.Log
import java.util.UUID import java.util.UUID
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -198,7 +201,7 @@ fun ByteArray.toHexString(): String =
joinToString(separator = "", prefix = "0x") { String.format("%02X", it) } joinToString(separator = "", prefix = "0x") { String.format("%02X", it) }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) { class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>() private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>()
private val operationQueue = ConcurrentLinkedQueue<BleOperationType>() private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
@@ -209,6 +212,8 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build() .build()
private val scanFilters = listOf( ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(BuildConfig.SERVICE_UUID)).build())
private val bleScanner by lazy { private val bleScanner by lazy {
bleAdapter.bluetoothLeScanner bleAdapter.bluetoothLeScanner
} }
@@ -274,6 +279,8 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
broadcastReceiver, broadcastReceiver,
IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED) IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
) )
} }
var isScanning = false var isScanning = false
@@ -307,7 +314,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
fun startScan() { fun startScan() {
if (!isScanning) { if (!isScanning) {
isScanning = true 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) { with(operation) {
Log.w("ConnectionManager", "Connecting to ${device.address}") Log.w("ConnectionManager", "Connecting to ${device.address}")
device.connectGatt( device.connectGatt(
context, false, callback, BluetoothDevice.TRANSPORT_LE context, true, callback, BluetoothDevice.TRANSPORT_LE
) )
} }
return return
@@ -520,6 +527,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
is SetNotification -> with(operation) { is SetNotification -> with(operation) {
val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId); val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId);
if (characteristic == null) { if (characteristic == null) {
Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!") Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!")
signalEndOfOperation(operation) signalEndOfOperation(operation)

View File

@@ -25,9 +25,8 @@ import java.util.UUID
import java.util.stream.Collectors import java.util.stream.Collectors
// Kirby service uuid: 6e400001-b5a3-f393-e1a9-e50e24dcac9e private val SERVICE_UUID = UUID.fromString(BuildConfig.SERVICE_UUID)
private val DEMO_SERVICE_UUID = UUID.fromString("6e400001-b5a3-f393-e1a9-e50e24dcac9e") private val CHAR_UUID = UUID.fromString(BuildConfig.CHAR_UUID)
private val DEMO_CHAR_UUID = UUID.fromString("6e400005-b5a3-f393-e1a9-e50e24dcac9e")
enum class DeviceStatus { enum class DeviceStatus {
CONNECTED, BONDED, SUBSCRIBED, MISSING CONNECTED, BONDED, SUBSCRIBED, MISSING
@@ -40,8 +39,27 @@ class KirbyDevice(
private val bleDevice: BluetoothDevice, private val bleDevice: BluetoothDevice,
private val onStateChange: (device: KirbyDevice) -> Unit, private val onStateChange: (device: KirbyDevice) -> Unit,
) : BleListener(bleDevice.address), DeviceListEntry { ) : BleListener(bleDevice.address), DeviceListEntry {
private val queue : RequestQueue = Volley.newRequestQueue(context) 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) { override fun onScanResult(callbackType: Int, result: ScanResult) {
rssi = result.rssi rssi = result.rssi
@@ -51,6 +69,7 @@ class KirbyDevice(
override fun onConnect(gatt: BluetoothGatt) { override fun onConnect(gatt: BluetoothGatt) {
statuses.add(DeviceStatus.CONNECTED) statuses.add(DeviceStatus.CONNECTED)
statuses.remove(DeviceStatus.MISSING) statuses.remove(DeviceStatus.MISSING)
onStateChange(this) onStateChange(this)
} }
@@ -65,14 +84,6 @@ class KirbyDevice(
onStateChange(this) onStateChange(this)
} }
override fun onSuccessfulCharRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
addMeasurement(characteristic)
onStateChange(this)
}
override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
addMeasurement(characteristic) addMeasurement(characteristic)
onStateChange(this) onStateChange(this)
@@ -82,6 +93,7 @@ class KirbyDevice(
gatt: BluetoothGatt, gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor, descriptor: BluetoothGattDescriptor,
) { ) {
statuses.add(DeviceStatus.SUBSCRIBED) statuses.add(DeviceStatus.SUBSCRIBED)
onStateChange(this) onStateChange(this)
} }
@@ -116,25 +128,23 @@ class KirbyDevice(
override var rssi: Int? = null override var rssi: Int? = null
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) { 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 hexPayload = characteristic.value.toHexString() val base64Payload = Base64.getEncoder().encodeToString(characteristic.value)
val payload = Payload(hexPayload) Log.i("BleListener", "Char received: $payload")
val base64Payload = Base64.getEncoder().encodeToString(characteristic.value) measurements.add(payload)
Log.i("BleListener", "Demo char received: $payload") publishMeasurement(base64Payload)
measurements.add(payload)
publishMeasurement(base64Payload)
}
} }
private fun publishMeasurement(payload: String) { private fun publishMeasurement(payload: String) {
val accessKey = BuildConfig.API_BASE_URL val accessKey = BuildConfig.API_KEY
val url = BuildConfig.API_KEY val url = BuildConfig.API_BASE_URL
val eui = "0000${bleDevice.address}" val eui = "0000${bleDevice.address.replace(":", "")}"
val postData = JSONObject() val postData = JSONObject()
try { try {
Log.i("POST", "Transmitting for $eui: $payload")
postData.put("accessKey", accessKey) postData.put("accessKey", accessKey)
postData.put("metricPayload", payload) postData.put("metricPayload", payload)
postData.put("eui", eui) postData.put("eui", eui)
@@ -145,7 +155,7 @@ class KirbyDevice(
val request = JsonObjectRequest( val request = JsonObjectRequest(
Request.Method.POST, url, postData, Request.Method.POST, url, postData,
{ response -> { response ->
Log.i("sendDataResponse","Response is: $response") Log.i("sendDataResponse", "Response is: $response")
} }
) { error -> error.printStackTrace() } ) { error -> error.printStackTrace() }
@@ -257,7 +267,7 @@ class KirbyDevice(
} }
override fun execute() { override fun execute() {
connectionManager.readChar(bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID) readIaq()
} }
}) })
@@ -272,9 +282,7 @@ class KirbyDevice(
} }
override fun execute() { override fun execute() {
connectionManager.enableNotification( subscribe()
bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID
)
} }
}) })
} }
@@ -307,7 +315,7 @@ class KirbyDevice(
override fun execute() { override fun execute() {
connectionManager.disableNotification( 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 { fun bytesToUInt16(arr: ByteArray, start: Int): Int {
return ByteBuffer.wrap(arr, start, 2) return ByteBuffer.wrap(arr, start, 2)
.order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF .order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF
@@ -371,21 +378,18 @@ private fun payloadToMeasurements(payload: Payload): List<Measurement> {
override fun getIcon(): Int? { override fun getIcon(): Int? {
return R.drawable.baseline_access_time_24 return R.drawable.baseline_access_time_24
} }
}, }, object : Measurement {
object : Measurement { override fun getLabel(): String {
override fun getLabel(): String { return "Payload"
return "Payload"
}
override fun getFormattedValue(): String {
return payload.payload
}
override fun getIcon(): Int? {
return null
}
} }
) 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) setSupportActionBar(binding.toolbar)
BLEService.startService(applicationContext, "hello ble service")
binding.fab.setOnClickListener { view -> binding.fab.setOnClickListener { view ->
if (!hasRequiredRuntimePermissions()) { if (!hasRequiredRuntimePermissions()) {
@@ -103,10 +103,10 @@ class MainActivity : AppCompatActivity() {
} }
private fun isKirbyDevice(device: BluetoothDevice): Boolean { private fun isKirbyDevice(device: BluetoothDevice): Boolean {
Log.i("kby", "found device ${device.name}")
return (device.name ?: "").lowercase().contains("kirby") return (device.name ?: "").lowercase().contains("kirby")
} }
private fun setupDevicesList() { private fun setupDevicesList() {
binding.devicesList.apply { binding.devicesList.apply {
adapter = deviceListAdapter adapter = deviceListAdapter
@@ -134,7 +134,8 @@ class MainActivity : AppCompatActivity() {
private fun addBondedDevices(): Unit { private fun addBondedDevices(): Unit {
bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach { 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 { 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 } val i = kirbyDevices.indexOfFirst { d -> d === it }
runOnUiThread { runOnUiThread {
deviceListAdapter.notifyItemChanged(i) deviceListAdapter.notifyItemChanged(i)
} }
} }
connectionManager.register(device) connectionManager.register(device)
connectionManager.connect(bleDevice)
connectionManager.discoverServices(bleDevice)
kirbyDevices.add(device) kirbyDevices.add(device)
deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1) deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1)
return device return device