diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 85289d7..2f77153 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,16 +3,21 @@
xmlns:tools="http://schemas.android.com/tools">
-
-
-
-
-
diff --git a/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt b/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt
index 0ff5f37..f6ea2fb 100644
--- a/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt
+++ b/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt
@@ -3,15 +3,23 @@ package com.example.sensortestingapp
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothDevice.BOND_BONDED
+import android.bluetooth.BluetoothDevice.BOND_BONDING
+import android.bluetooth.BluetoothDevice.BOND_NONE
import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGatt.GATT_SUCCESS
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothProfile
+import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
import android.bluetooth.le.ScanCallback
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.util.Log
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
@@ -62,6 +70,18 @@ data class DiscoverServicesRequest(
override val device: BluetoothDevice,
) : BleOperationType()
+data class BondRequest(
+ override val device: BluetoothDevice,
+) : BleOperationType()
+
+data class UnbondRequest(
+ override val device: BluetoothDevice,
+) : BleOperationType()
+
+data class ReadRemoteRssi(
+ override val device: BluetoothDevice,
+) : BleOperationType()
+
open class BleListener(private val deviceAddress: String?) {
@@ -74,6 +94,8 @@ open class BleListener(private val deviceAddress: String?) {
open fun onConnect(gatt: BluetoothGatt) {}
+ open fun onConnectToBondedFailed(gatt: BluetoothGatt) {}
+
open fun onDisconnect(gatt: BluetoothGatt) {}
open fun onSuccessfulCharRead(
@@ -112,6 +134,18 @@ open class BleListener(private val deviceAddress: String?) {
}
+ open fun onBonded(device: BluetoothDevice) {
+
+ }
+
+ open fun onUnbonded(device: BluetoothDevice) {
+
+ }
+
+ open fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int) {
+
+ }
+
}
@@ -179,6 +213,69 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
bleAdapter.bluetoothLeScanner
}
+ init {
+ val broadcastReceiver = object : BroadcastReceiver() {
+
+ private fun successfulBondingAttempt(prev: Int, curr: Int): Boolean {
+ return prev == BOND_BONDING && curr == BOND_BONDED || prev == BOND_NONE && curr == BOND_BONDED
+ }
+
+ private fun unsuccessfulBondingAttempt(prev: Int, curr: Int): Boolean {
+ return prev == BOND_BONDING && curr == BOND_NONE
+ }
+
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
+ val device =
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)!!
+ val previousBondState =
+ intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1)
+ val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
+ val bondTransition = "${previousBondState.toBondStateDescription()} to " +
+ bondState.toBondStateDescription()
+ Log.i(
+ "ConnectionManager",
+ "${device.address} bond state changed | $bondTransition"
+ )
+
+ if (bondState == BOND_BONDED) {
+ notifyListeners(device.address) { it.onBonded(device) }
+ }
+ if (bondState == BOND_NONE) {
+ notifyListeners(device.address) { it.onUnbonded(device) }
+ }
+
+ val operation = pendingOperation
+ if (
+ (operation is BondRequest && (successfulBondingAttempt(
+ previousBondState, bondState
+ ) || unsuccessfulBondingAttempt(
+ previousBondState, bondState
+ )) || operation is UnbondRequest && bondState == BOND_NONE)
+ && operation.device.address == device.address
+ ) {
+ signalEndOfOperation(operation)
+ }
+
+ }
+
+ }
+
+ private fun Int.toBondStateDescription() = when (this) {
+ BOND_BONDED -> "BONDED"
+ BOND_BONDING -> "BONDING"
+ BOND_NONE -> "NOT BONDED"
+ else -> "ERROR: $this"
+ }
+ }
+
+ context.registerReceiver(
+ broadcastReceiver,
+ IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
+ )
+ }
+
var isScanning = false
set(value) {
field = value
@@ -260,6 +357,18 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
enqueueOperation(SetNotification(device, service, char, false))
}
+ fun bond(device: BluetoothDevice) {
+ enqueueOperation(BondRequest(device))
+ }
+
+ fun unbond(device: BluetoothDevice) {
+ enqueueOperation(UnbondRequest(device))
+ }
+
+ fun readRemoteRssi(device: BluetoothDevice) {
+ enqueueOperation(ReadRemoteRssi(device))
+ }
+
// - Beginning of PRIVATE functions
@@ -325,10 +434,52 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
return
}
+ if (operation is BondRequest) {
+ if (operation.device.bondState == BOND_NONE) {
+ if (!operation.device.createBond()) {
+ Log.e(
+ "ConnectionManager", "createBond() returned false " +
+ "for device ${operation.device.address}"
+ )
+ signalEndOfOperation(operation)
+ }
+ } else {
+ Log.e(
+ "ConnectionManager",
+ "BondRequest for device ${operation.device.address} aborted. " +
+ "operation.device.bondState (${operation.device.bondState}) == BOND_NONE (${BOND_NONE})"
+ )
+ signalEndOfOperation(operation)
+ }
+ return
+ }
+
+ if (operation is UnbondRequest) {
+ if (operation.device.bondState == BOND_BONDED) {
+ val method = operation.device.javaClass.getMethod("removeBond")
+ val removeBondSuccessful = method.invoke(operation.device) as Boolean
+ if (!removeBondSuccessful) {
+ Log.e(
+ "ConnectionManager", "removeBond() returned false " +
+ "for device ${operation.device.address}"
+ )
+ signalEndOfOperation(operation)
+ }
+ } else {
+ Log.e(
+ "ConnectionManager",
+ "UnbondRequest for device ${operation.device.address} aborted. " +
+ "operation.device.bondState (${operation.device.bondState}) == BOND_BONDED (${BOND_BONDED})"
+ )
+ signalEndOfOperation(operation)
+ }
+ return
+ }
+
// Check BluetoothGatt availability for other operations
val gatt = deviceGattMap[operation.device]
?: this@ConnectionManager.run {
- Log.e(
+ Log.w(
"ConnectionManager",
"Not connected to ${operation.device.address}! Aborting $operation operation."
)
@@ -419,11 +570,15 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
}
}
+ is ReadRemoteRssi -> {
+ gatt.readRemoteRssi()
+ }
+
// necessary because of IDE type inference bug
// https://youtrack.jetbrains.com/issue/KTIJ-20749/Exhaustive-when-check-does-not-take-into-account-the-values-excluded-by-previous-if-conditions
- is Connect -> {
- Log.e("ConnectionManager", "Shouldn't get here")
+ is Connect, is BondRequest, is UnbondRequest -> {
+ Log.e("ConnectionManager", "Shouldn't get here: $operation")
}
}
}
@@ -431,8 +586,24 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
private val callback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
val deviceAddress = gatt.device.address
+ val operation = pendingOperation
- if (status == BluetoothGatt.GATT_SUCCESS) {
+ if (operation is Connect
+ && operation.device.bondState == BOND_BONDED
+ && deviceAddress == operation.device.address
+ && status == 133
+ && newState == STATE_DISCONNECTED
+ ) {
+ Log.i(
+ "ConnectionManager",
+ "onConnectionStateChange: Timeout when connecting to bonded device $deviceAddress. " +
+ "Device probably not in vicinity."
+ )
+ notifyListeners(gatt.device.address) {
+ it.onConnectToBondedFailed(gatt)
+ }
+ signalEndOfOperation(operation)
+ } else if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.i(
"ConnectionManager",
@@ -442,11 +613,11 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
notifyListeners(gatt.device.address) {
it.onConnect(gatt)
}
- val operation = pendingOperation
+
if (operation is Connect) {
signalEndOfOperation(operation)
}
- } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+ } else if (newState == STATE_DISCONNECTED) {
Log.e(
"ConnectionManager",
"onConnectionStateChange: disconnected from $deviceAddress"
@@ -579,6 +750,20 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
}
}
+ override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
+ if (status == GATT_SUCCESS) {
+ notifyListeners(gatt.device.address) { it.onReadRemoteRssi(gatt, rssi) }
+ } else {
+ Log.e(
+ "BluetoothGattCallback",
+ "ReadRemoteRssi failed for ${gatt.device.address}, error: $status"
+ )
+ }
+ val op = pendingOperation
+ if (op is ReadRemoteRssi && gatt.device.address == op.device.address) {
+ signalEndOfOperation(op)
+ }
+ }
}
diff --git a/app/src/main/java/com/example/sensortestingapp/DeviceListAdapter.kt b/app/src/main/java/com/example/sensortestingapp/DeviceListAdapter.kt
index 97084d3..d986f83 100644
--- a/app/src/main/java/com/example/sensortestingapp/DeviceListAdapter.kt
+++ b/app/src/main/java/com/example/sensortestingapp/DeviceListAdapter.kt
@@ -31,7 +31,7 @@ interface Measurement {
interface DeviceListEntry {
val address: String
- var rssi: Int
+ var rssi: Int?
val name: String?
@@ -80,14 +80,17 @@ class DeviceListAdapter(
@SuppressLint("RestrictedApi")
fun bind(result: DeviceListEntry) {
+ val rssi = result.rssi
deviceNameView.text = result.name ?: ""
deviceProgress.visibility = if (result.hasRunningOp) VISIBLE else INVISIBLE
macAddressView.text = result.address
- signalStrengthView.text = "${result.rssi ?: "-"} dBm"
+ signalStrengthView.text = "${rssi ?: "-"} dBm"
var signalStrengthIcon = R.drawable.signal_strength_weak
- if (result.rssi >= -55) {
+ if (rssi == null) {
+ signalStrengthIcon = R.drawable.signal_strength_none
+ } else if (rssi >= -55) {
signalStrengthIcon = R.drawable.signal_strength_strong
- } else if (result.rssi >= -80) {
+ } else if (rssi >= -80) {
signalStrengthIcon = R.drawable.signal_strength_medium
}
signalStrengthView.setCompoundDrawablesWithIntrinsicBounds(
diff --git a/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt b/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt
index d8cb813..018e474 100644
--- a/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt
+++ b/app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt
@@ -2,6 +2,7 @@ package com.example.sensortestingapp
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothDevice.BOND_BONDED
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
@@ -15,6 +16,7 @@ import java.util.EnumSet
import java.util.UUID
import java.util.stream.Collectors
+
private val DEMO_SERVICE_UUID = UUID.fromString("00000000-0001-11E1-9AB4-0002A5D5C51B")
private val DEMO_CHAR_UUID = UUID.fromString("00140000-0001-11E1-AC36-0002A5D5C51B")
@@ -92,26 +94,30 @@ private fun demoPayloadToMeasurements(payload: DemoPayload): List {
}
enum class DeviceStatus {
- DISCOVERED, CONNECTED, BONDED, SUBSCRIBED
+ CONNECTED, BONDED, SUBSCRIBED, MISSING
}
@SuppressLint("MissingPermission")
class KirbyDevice(
private val connectionManager: ConnectionManager,
private val bleDevice: BluetoothDevice,
- initialRssi: Int,
- override var hasRunningOp: Boolean = false,
private val onStateChange: (device: KirbyDevice) -> Unit
) : BleListener(bleDevice.address), DeviceListEntry {
override fun onScanResult(callbackType: Int, result: ScanResult) {
- statuses.add(DeviceStatus.DISCOVERED)
+ rssi = result.rssi
onStateChange(this)
}
override fun onConnect(gatt: BluetoothGatt) {
statuses.add(DeviceStatus.CONNECTED)
+ statuses.remove(DeviceStatus.MISSING)
+ onStateChange(this)
+ }
+
+ override fun onConnectToBondedFailed(gatt: BluetoothGatt) {
+ statuses.add(DeviceStatus.MISSING)
onStateChange(this)
}
@@ -131,7 +137,6 @@ class KirbyDevice(
override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
addMeasurement(characteristic)
- //statuses.add(DeviceStatus.SUBSCRIBED)
onStateChange(this)
}
@@ -153,11 +158,25 @@ class KirbyDevice(
onStateChange(this)
}
- override var rssi = initialRssi
- set(value) {
- field = value
- onStateChange(this)
- }
+ override fun onBonded(device: BluetoothDevice) {
+ statuses.add(DeviceStatus.BONDED)
+ onStateChange(this)
+ }
+
+ override fun onUnbonded(device: BluetoothDevice) {
+ statuses.remove(DeviceStatus.BONDED)
+ onStateChange(this)
+ }
+
+ override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int) {
+ this.rssi = rssi
+ onStateChange(this)
+ }
+
+ override var hasRunningOp: Boolean = false
+
+ override var rssi: Int? = null
+
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) {
if (characteristic.service.uuid == DEMO_SERVICE_UUID && characteristic.uuid == DEMO_CHAR_UUID) {
@@ -171,6 +190,12 @@ class KirbyDevice(
private val statuses = EnumSet.noneOf(DeviceStatus::class.java)
+ init {
+ if (bleDevice.bondState == BOND_BONDED) {
+ statuses.add(DeviceStatus.BONDED)
+ }
+ }
+
override val address: String
get() = bleDevice.address
@@ -181,10 +206,6 @@ class KirbyDevice(
override val status: String?
get() = statuses.stream().map { it.name }.collect(Collectors.joining(", "))
- // override fun getRssi(): Int? {
-// return rssi
-// }
-
override fun getMeasurements(): List {
if (measurements.isEmpty()) {
return emptyList()
@@ -195,7 +216,23 @@ class KirbyDevice(
override fun getActions(): List {
val actions = mutableListOf()
- if (statuses.contains(DeviceStatus.DISCOVERED) && !statuses.contains(DeviceStatus.CONNECTED)
+ if (!statuses.contains(DeviceStatus.BONDED)) {
+ actions.add(object : Action {
+ override fun getLabel(): String {
+ return "Bond"
+ }
+
+ override fun getIcon(): Int {
+ return R.drawable.action_icon_bond
+ }
+
+ override fun execute() {
+ connectionManager.bond(bleDevice)
+ }
+ })
+ }
+
+ if (!statuses.contains(DeviceStatus.CONNECTED)
) {
actions.add(object : Action {
override fun getLabel(): String {
@@ -208,6 +245,7 @@ class KirbyDevice(
override fun execute() {
connectionManager.connect(bleDevice)
+ connectionManager.readRemoteRssi(bleDevice)
connectionManager.discoverServices(bleDevice)
}
})
@@ -260,6 +298,21 @@ class KirbyDevice(
}
})
}
+
+ actions.add(object : Action {
+ override fun getLabel(): String {
+ return "Update Signal Strength"
+ }
+
+ override fun getIcon(): Int {
+ return R.drawable.action_icon_update_signal_strength
+ }
+
+ override fun execute() {
+ connectionManager.readRemoteRssi(bleDevice)
+ }
+ })
+
}
if (statuses.contains(DeviceStatus.SUBSCRIBED)) {
@@ -280,6 +333,22 @@ class KirbyDevice(
})
}
+ if (statuses.contains(DeviceStatus.BONDED)
+ ) {
+ actions.add(object : Action {
+ override fun getLabel(): String {
+ return "Unbond"
+ }
+
+ override fun getIcon(): Int {
+ return R.drawable.action_icon_unbond
+ }
+
+ override fun execute() {
+ connectionManager.unbond(bleDevice)
+ }
+ })
+ }
return actions;
}
diff --git a/app/src/main/java/com/example/sensortestingapp/MainActivity.kt b/app/src/main/java/com/example/sensortestingapp/MainActivity.kt
index 1c5ba41..9d5e523 100644
--- a/app/src/main/java/com/example/sensortestingapp/MainActivity.kt
+++ b/app/src/main/java/com/example/sensortestingapp/MainActivity.kt
@@ -5,6 +5,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanResult
import android.content.Context
@@ -22,8 +23,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.example.sensortestingapp.databinding.ActivityMainBinding
@@ -91,21 +90,24 @@ class MainActivity : AppCompatActivity() {
}
+ private fun isKirbyDevice(device: BluetoothDevice): Boolean {
+ return (device.name ?: "").lowercase().contains("kirby")
+ }
+
private fun setupDevicesList() {
binding.devicesList.apply {
adapter = deviceListAdapter
- layoutManager = LinearLayoutManager(
- this@MainActivity,
- RecyclerView.VERTICAL,
- false
- )
- //isNestedScrollingEnabled = false
}
val animator = binding.devicesList.itemAnimator
if (animator is SimpleItemAnimator) {
animator.supportsChangeAnimations = false
}
+
+ bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach {
+ newKirbyDevice(it)
+ }
+
//addDummyDevices()
}
@@ -131,7 +133,7 @@ class MainActivity : AppCompatActivity() {
}
override fun onScanResult(callbackType: Int, result: ScanResult) {
- if ((result.device.name ?: "").lowercase().contains("kirby")) {
+ if (isKirbyDevice(result.device)) {
Log.i(
"ScanCallback",
@@ -139,9 +141,7 @@ class MainActivity : AppCompatActivity() {
)
val kirbyDevice = kirbyDevices.find { it.address == result.device.address }
if (kirbyDevice == null) {
- newKirbyDevice(callbackType, result)
- } else {
- kirbyDevice.rssi = result.rssi
+ newKirbyDevice(result.device).onScanResult(callbackType, result)
}
}
@@ -150,8 +150,8 @@ class MainActivity : AppCompatActivity() {
return mngr
}
- private fun newKirbyDevice(callbackType: Int, scanResult: ScanResult): KirbyDevice {
- val device = KirbyDevice(connectionManager, scanResult.device, scanResult.rssi) {
+ private fun newKirbyDevice(bleDevice: BluetoothDevice): KirbyDevice {
+ val device = KirbyDevice(connectionManager, bleDevice) {
val i = kirbyDevices.indexOfFirst { d -> d === it }
runOnUiThread {
deviceListAdapter.notifyItemChanged(i)
@@ -159,7 +159,6 @@ class MainActivity : AppCompatActivity() {
}
connectionManager.register(device)
kirbyDevices.add(device)
- device.onScanResult(callbackType, scanResult)
deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1)
return device
}
@@ -169,8 +168,6 @@ class MainActivity : AppCompatActivity() {
requestRelevantRuntimePermissions()
} else {
connectionManager.startScan()
- // kirbyDevices.clear()
- //deviceListAdapter.notifyDataSetChanged()
}
}
@@ -332,7 +329,7 @@ class MainActivity : AppCompatActivity() {
@SuppressLint("MissingPermission")
class DummyListEntry(override val address: String) : DeviceListEntry {
- override var rssi: Int = -30
+ override var rssi: Int? = -30
override val name: String = "Device 123"
override val status: String
diff --git a/app/src/main/res/drawable/action_icon_bond.xml b/app/src/main/res/drawable/action_icon_bond.xml
new file mode 100644
index 0000000..ffc7f47
--- /dev/null
+++ b/app/src/main/res/drawable/action_icon_bond.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/action_icon_unbond.xml b/app/src/main/res/drawable/action_icon_unbond.xml
new file mode 100644
index 0000000..2fbc2cf
--- /dev/null
+++ b/app/src/main/res/drawable/action_icon_unbond.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/action_icon_update_signal_strength.xml b/app/src/main/res/drawable/action_icon_update_signal_strength.xml
new file mode 100644
index 0000000..2e7e1a5
--- /dev/null
+++ b/app/src/main/res/drawable/action_icon_update_signal_strength.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/signal_strength_none.xml b/app/src/main/res/drawable/signal_strength_none.xml
new file mode 100644
index 0000000..8c2c9f7
--- /dev/null
+++ b/app/src/main/res/drawable/signal_strength_none.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+