chore: rename project
This commit is contained in:
779
app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt
Normal file
779
app/src/main/java/com/logitech/vc/kirbytest/ConnectionManager.kt
Normal file
@@ -0,0 +1,779 @@
|
||||
package com.logitech.vc.kirbytest
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
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.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
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
val CCC_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
|
||||
|
||||
private const val GATT_MIN_MTU_SIZE = 23
|
||||
|
||||
/** Maximum BLE MTU size as defined in gatt_api.h. */
|
||||
private const val GATT_MAX_MTU_SIZE = 517
|
||||
|
||||
|
||||
/** Abstract sealed class representing a type of BLE operation */
|
||||
sealed class BleOperationType {
|
||||
abstract val device: BluetoothDevice
|
||||
}
|
||||
|
||||
/** Connect to [device] and perform service discovery */
|
||||
data class Connect(override val device: BluetoothDevice, val context: Context) : BleOperationType()
|
||||
|
||||
/** Disconnect from [device] and release all connection resources */
|
||||
data class Disconnect(override val device: BluetoothDevice) : BleOperationType()
|
||||
|
||||
|
||||
/** Request for an MTU of [mtu] */
|
||||
data class MtuRequest(
|
||||
override val device: BluetoothDevice,
|
||||
val mtu: Int
|
||||
) : BleOperationType()
|
||||
|
||||
|
||||
data class ReadChar(
|
||||
override val device: BluetoothDevice,
|
||||
val serviceId: UUID,
|
||||
val charId: UUID
|
||||
) : BleOperationType()
|
||||
|
||||
|
||||
data class SetNotification(
|
||||
override val device: BluetoothDevice,
|
||||
val serviceId: UUID,
|
||||
val charId: UUID,
|
||||
val enable: Boolean
|
||||
) : BleOperationType()
|
||||
|
||||
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?) {
|
||||
|
||||
open fun isRelevantMessage(address: String?): Boolean {
|
||||
if (deviceAddress != null && deviceAddress == address) {
|
||||
return true;
|
||||
}
|
||||
return deviceAddress == null
|
||||
}
|
||||
|
||||
open fun onConnect(gatt: BluetoothGatt) {}
|
||||
|
||||
open fun onConnectToBondedFailed(gatt: BluetoothGatt) {}
|
||||
|
||||
open fun onDisconnect(gatt: BluetoothGatt) {}
|
||||
|
||||
open fun onSuccessfulCharRead(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic
|
||||
) {
|
||||
}
|
||||
|
||||
open fun onCharChange(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic
|
||||
) {
|
||||
}
|
||||
|
||||
open fun onSubscribe(
|
||||
gatt: BluetoothGatt,
|
||||
descriptor: BluetoothGattDescriptor,
|
||||
) {
|
||||
}
|
||||
|
||||
open fun onUnsubscribe(
|
||||
gatt: BluetoothGatt,
|
||||
descriptor: BluetoothGattDescriptor,
|
||||
) {
|
||||
}
|
||||
|
||||
open fun onScanningStateChange(isScanning: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
open fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
|
||||
}
|
||||
|
||||
open fun onQueueSizeChange(groupedOps: Map<String, List<BleOperationType>>) {
|
||||
|
||||
}
|
||||
|
||||
open fun onBonded(device: BluetoothDevice) {
|
||||
|
||||
}
|
||||
|
||||
open fun onUnbonded(device: BluetoothDevice) {
|
||||
|
||||
}
|
||||
|
||||
open fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun BluetoothGatt.printGattTable() {
|
||||
if (services.isEmpty()) {
|
||||
Log.i(
|
||||
"printGattTable",
|
||||
"No service and characteristic available, call discoverServices() first?"
|
||||
)
|
||||
return
|
||||
}
|
||||
services.forEach { service ->
|
||||
val characteristicsTable = service.characteristics.joinToString(
|
||||
separator = "\n|--",
|
||||
prefix = "|--"
|
||||
) {
|
||||
"${it.uuid.toString()} | " +
|
||||
"readable: ${it.isReadable()}, " +
|
||||
"writable: ${it.isWritable()}, " +
|
||||
"writableWithoutResponse: ${it.isWritableWithoutResponse()}, " +
|
||||
"notifiable: ${it.isNotifiable()}, " +
|
||||
"indicatable: ${it.isIndicatable()}, "
|
||||
}
|
||||
Log.i(
|
||||
"printGattTable", "\nService ${service.uuid}\nCharacteristics:\n$characteristicsTable"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun BluetoothGattCharacteristic.isReadable(): Boolean =
|
||||
containsProperty(BluetoothGattCharacteristic.PROPERTY_READ)
|
||||
|
||||
fun BluetoothGattCharacteristic.isWritable(): Boolean =
|
||||
containsProperty(BluetoothGattCharacteristic.PROPERTY_WRITE)
|
||||
|
||||
fun BluetoothGattCharacteristic.isWritableWithoutResponse(): Boolean =
|
||||
containsProperty(BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)
|
||||
|
||||
fun BluetoothGattCharacteristic.isIndicatable(): Boolean =
|
||||
containsProperty(BluetoothGattCharacteristic.PROPERTY_INDICATE)
|
||||
|
||||
fun BluetoothGattCharacteristic.isNotifiable(): Boolean =
|
||||
containsProperty(BluetoothGattCharacteristic.PROPERTY_NOTIFY)
|
||||
|
||||
fun BluetoothGattCharacteristic.containsProperty(property: Int): Boolean =
|
||||
properties and property != 0
|
||||
|
||||
fun ByteArray.toHexString(): String =
|
||||
joinToString(separator = "", prefix = "0x") { String.format("%02X", it) }
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
|
||||
private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>()
|
||||
private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
|
||||
private var pendingOperation: BleOperationType? = null
|
||||
private var listeners = ArrayList<BleListener>()
|
||||
|
||||
private val scanSettings = ScanSettings.Builder()
|
||||
.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
|
||||
}
|
||||
|
||||
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>(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
|
||||
notifyListeners(null) {
|
||||
it.onScanningStateChange(value)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
val scanCallback = object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
Log.d(
|
||||
"ScanCallback",
|
||||
"Found BLE device with address ${result.device.address} (name: ${result.device.name}, rssi: ${result.rssi})"
|
||||
)
|
||||
notifyListeners(result.device.address) { it.onScanResult(callbackType, result) }
|
||||
}
|
||||
|
||||
override fun onScanFailed(errorCode: Int) {
|
||||
Log.e("ScanCallback", "onScanFailed: code $errorCode")
|
||||
}
|
||||
}
|
||||
|
||||
fun register(listener: BleListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
|
||||
fun startScan() {
|
||||
if (!isScanning) {
|
||||
isScanning = true
|
||||
bleScanner.startScan( null, scanSettings, scanCallback)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopScan() {
|
||||
bleScanner.stopScan(scanCallback)
|
||||
isScanning = false
|
||||
}
|
||||
|
||||
|
||||
fun connect(device: BluetoothDevice) {
|
||||
if (device.isConnected()) {
|
||||
Log.e("ConnectionManager", "Already connected to ${device.address}!")
|
||||
} else {
|
||||
enqueueOperation(Connect(device, context))
|
||||
}
|
||||
}
|
||||
|
||||
fun teardownConnection(device: BluetoothDevice) {
|
||||
if (device.isConnected()) {
|
||||
enqueueOperation(Disconnect(device))
|
||||
} else {
|
||||
Log.e(
|
||||
"ConnectionManager",
|
||||
"Not connected to ${device.address}, cannot teardown connection!"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestMtu(device: BluetoothDevice, mtu: Int = Int.MAX_VALUE) {
|
||||
enqueueOperation(MtuRequest(device, mtu.coerceIn(GATT_MIN_MTU_SIZE, GATT_MAX_MTU_SIZE)))
|
||||
|
||||
}
|
||||
|
||||
fun discoverServices(device: BluetoothDevice) {
|
||||
enqueueOperation(DiscoverServicesRequest(device))
|
||||
}
|
||||
|
||||
fun readChar(device: BluetoothDevice, service: UUID, char: UUID) {
|
||||
enqueueOperation(ReadChar(device, service, char))
|
||||
}
|
||||
|
||||
fun enableNotification(device: BluetoothDevice, service: UUID, char: UUID) {
|
||||
enqueueOperation(SetNotification(device, service, char, true))
|
||||
}
|
||||
|
||||
fun disableNotification(device: BluetoothDevice, service: UUID, char: UUID) {
|
||||
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
|
||||
|
||||
private fun notifyListeners(address: String?, notifier: (listener: BleListener) -> Unit) {
|
||||
listeners.filter { n -> n.isRelevantMessage(address) }.forEach(notifier)
|
||||
}
|
||||
|
||||
private fun notifyListenersOfQueueChange(address: String) {
|
||||
notifyListeners(address) { listener ->
|
||||
listener.onQueueSizeChange(
|
||||
operationQueue.groupBy { o -> o.device.address }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Synchronized
|
||||
private fun enqueueOperation(operation: BleOperationType) {
|
||||
if (isScanning) {
|
||||
stopScan()
|
||||
}
|
||||
operationQueue.add(operation)
|
||||
notifyListenersOfQueueChange(operation.device.address)
|
||||
if (pendingOperation == null) {
|
||||
doNextOperation()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun signalEndOfOperation(op: BleOperationType) {
|
||||
Log.d("ConnectionManager", "End of $op")
|
||||
pendingOperation = null
|
||||
notifyListenersOfQueueChange(op.device.address)
|
||||
if (operationQueue.isNotEmpty()) {
|
||||
doNextOperation()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun doNextOperation() {
|
||||
if (pendingOperation != null) {
|
||||
Log.e(
|
||||
"ConnectionManager",
|
||||
"doNextOperation() called when an operation is pending! Aborting."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val operation = operationQueue.poll() ?: run {
|
||||
Log.d("ConnectionManager", "Operation queue empty, returning")
|
||||
return
|
||||
}
|
||||
pendingOperation = operation
|
||||
|
||||
// Handle Connect separately from other operations that require device to be connected
|
||||
if (operation is Connect) {
|
||||
with(operation) {
|
||||
Log.w("ConnectionManager", "Connecting to ${device.address}")
|
||||
device.connectGatt(
|
||||
context, true, callback, BluetoothDevice.TRANSPORT_LE
|
||||
)
|
||||
}
|
||||
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.w(
|
||||
"ConnectionManager",
|
||||
"Not connected to ${operation.device.address}! Aborting $operation operation."
|
||||
)
|
||||
signalEndOfOperation(operation)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
when (operation) {
|
||||
is Disconnect -> with(operation) {
|
||||
Log.w("ConnectionManager", "Disconnecting from ${device.address}")
|
||||
gatt.close()
|
||||
deviceGattMap.remove(device)
|
||||
notifyListeners(gatt.device.address) {
|
||||
it.onDisconnect(gatt)
|
||||
}
|
||||
signalEndOfOperation(operation)
|
||||
}
|
||||
|
||||
is MtuRequest -> with(operation) {
|
||||
gatt.requestMtu(mtu)
|
||||
}
|
||||
|
||||
is DiscoverServicesRequest -> with(operation) {
|
||||
gatt.discoverServices()
|
||||
}
|
||||
|
||||
is ReadChar -> with(operation) {
|
||||
val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId)
|
||||
if (characteristic?.isReadable() == true) {
|
||||
gatt.readCharacteristic(characteristic)
|
||||
} else {
|
||||
Log.e("ConnectionManager", "Char $charId (${serviceId}) is not readable!")
|
||||
signalEndOfOperation(operation)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
is SetNotification -> with(operation) {
|
||||
val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId);
|
||||
|
||||
if (characteristic == null) {
|
||||
Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!")
|
||||
signalEndOfOperation(operation)
|
||||
return
|
||||
}
|
||||
val descriptor = characteristic.getDescriptor(CCC_DESCRIPTOR_UUID)
|
||||
|
||||
if (!characteristic.isNotifiable() && !characteristic.isIndicatable()) {
|
||||
Log.e(
|
||||
"ConnectionManager", "Char ${characteristic.uuid} (service: $serviceId)" +
|
||||
" doesn't support notifications/indications"
|
||||
)
|
||||
signalEndOfOperation(operation)
|
||||
return
|
||||
}
|
||||
|
||||
val payload: ByteArray
|
||||
|
||||
if (enable) {
|
||||
if (!gatt.setCharacteristicNotification(characteristic, true)) {
|
||||
Log.e(
|
||||
"ConnectionManager", "setCharacteristicNotification to true " +
|
||||
"failed for ${characteristic.uuid} (service: $serviceId)"
|
||||
)
|
||||
signalEndOfOperation(operation)
|
||||
return
|
||||
}
|
||||
payload =
|
||||
if (characteristic.isIndicatable())
|
||||
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
|
||||
else BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
|
||||
|
||||
} else {
|
||||
if (!gatt.setCharacteristicNotification(characteristic, false)) {
|
||||
Log.e(
|
||||
"ConnectionManager", "setCharacteristicNotification to false " +
|
||||
"failed for ${characteristic.uuid} (service: $serviceId)"
|
||||
)
|
||||
signalEndOfOperation(operation)
|
||||
return
|
||||
}
|
||||
payload = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
|
||||
}
|
||||
|
||||
gatt.let { gatt ->
|
||||
descriptor.value = payload
|
||||
gatt.writeDescriptor(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
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, is BondRequest, is UnbondRequest -> {
|
||||
Log.e("ConnectionManager", "Shouldn't get here: $operation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val callback = object : BluetoothGattCallback() {
|
||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||
val deviceAddress = gatt.device.address
|
||||
val operation = pendingOperation
|
||||
|
||||
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",
|
||||
"onConnectionStateChange: connected to $deviceAddress"
|
||||
)
|
||||
deviceGattMap[gatt.device] = gatt
|
||||
notifyListeners(gatt.device.address) {
|
||||
it.onConnect(gatt)
|
||||
}
|
||||
|
||||
if (operation is Connect) {
|
||||
signalEndOfOperation(operation)
|
||||
}
|
||||
} else if (newState == STATE_DISCONNECTED) {
|
||||
Log.e(
|
||||
"ConnectionManager",
|
||||
"onConnectionStateChange: disconnected from $deviceAddress"
|
||||
)
|
||||
notifyListeners(gatt.device.address) {
|
||||
it.onDisconnect(gatt)
|
||||
}
|
||||
teardownConnection(gatt.device)
|
||||
}
|
||||
} else {
|
||||
Log.e(
|
||||
"ConnectionManager",
|
||||
"onConnectionStateChange: status $status encountered for $deviceAddress!"
|
||||
)
|
||||
teardownConnection(gatt.device)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||
with(gatt) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
Log.w(
|
||||
"ConnectionManager",
|
||||
"Discovered ${services.size} services for ${device.address}."
|
||||
)
|
||||
printGattTable()
|
||||
notifyListeners(gatt.device.address) {
|
||||
it.onConnect(gatt)
|
||||
}
|
||||
} else {
|
||||
Log.e("ConnectionManager", "Service discovery failed due to status $status")
|
||||
teardownConnection(gatt.device)
|
||||
}
|
||||
}
|
||||
|
||||
val operation = pendingOperation
|
||||
if (operation is DiscoverServicesRequest) {
|
||||
signalEndOfOperation(operation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
||||
Log.w(
|
||||
"ConnectionManager",
|
||||
"ATT MTU changed to $mtu, success: ${status == BluetoothGatt.GATT_SUCCESS}"
|
||||
)
|
||||
|
||||
val operation = pendingOperation
|
||||
if (operation is MtuRequest) {
|
||||
signalEndOfOperation(operation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCharacteristicRead(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
status: Int
|
||||
) {
|
||||
with(characteristic) {
|
||||
when (status) {
|
||||
BluetoothGatt.GATT_SUCCESS -> {
|
||||
Log.i(
|
||||
"ConnectionManager",
|
||||
"Read characteristic $uuid (service: ${service.uuid}): ${value.toHexString()}"
|
||||
)
|
||||
notifyListeners(gatt.device.address) { listener ->
|
||||
listener.onSuccessfulCharRead(
|
||||
gatt,
|
||||
characteristic
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothGatt.GATT_READ_NOT_PERMITTED -> {
|
||||
Log.e(
|
||||
"BluetoothGattCallback",
|
||||
"Read not permitted for $uuid (service: ${service.uuid})!"
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.e(
|
||||
"BluetoothGattCallback",
|
||||
"Characteristic read failed for $uuid (service: ${service.uuid}), error: $status"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val op = pendingOperation
|
||||
if (op is ReadChar && op.charId == characteristic.uuid && op.serviceId == characteristic.service.uuid) {
|
||||
signalEndOfOperation(op)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDescriptorWrite(
|
||||
gatt: BluetoothGatt,
|
||||
descriptor: BluetoothGattDescriptor,
|
||||
status: Int
|
||||
) {
|
||||
val op = pendingOperation
|
||||
if (op is SetNotification &&
|
||||
descriptor.uuid == CCC_DESCRIPTOR_UUID
|
||||
&& op.charId == descriptor.characteristic.uuid && op.serviceId == descriptor.characteristic.service.uuid
|
||||
) {
|
||||
|
||||
if (!descriptor.value.contentEquals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
|
||||
notifyListeners(gatt.device.address) {
|
||||
it.onSubscribe(gatt, descriptor)
|
||||
}
|
||||
} else {
|
||||
notifyListeners(gatt.device.address) {
|
||||
it.onUnsubscribe(gatt, descriptor)
|
||||
}
|
||||
}
|
||||
signalEndOfOperation(op)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCharacteristicChanged(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic
|
||||
) {
|
||||
notifyListeners(gatt.device.address) { listener ->
|
||||
listener.onCharChange(
|
||||
gatt,
|
||||
characteristic
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun BluetoothDevice.isConnected() = deviceGattMap.containsKey(this)
|
||||
}
|
||||
Reference in New Issue
Block a user