785 lines
27 KiB
Kotlin
785 lines
27 KiB
Kotlin
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} | " +
|
|
"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 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) {
|
|
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) {
|
|
Log.i("ConnectionManager", "connecting to " +device.address)
|
|
|
|
val isConnecting = pendingOperation != null && pendingOperation is Connect && (pendingOperation as Connect).device.address == device.address
|
|
|
|
if(device.isConnected() or isConnecting or operationQueue.any { it.device.address === device.address && it is Connect }) {
|
|
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) {
|
|
if(operationQueue.any { it.device.address === device.address && it is DiscoverServicesRequest }) {
|
|
return
|
|
}
|
|
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) {
|
|
if(operationQueue.any { it.device.address === device.address && it is ReadRemoteRssi }) {
|
|
return
|
|
}
|
|
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 == 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 == 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 == GATT_SUCCESS}"
|
|
)
|
|
|
|
val operation = pendingOperation
|
|
if (operation is MtuRequest) {
|
|
signalEndOfOperation(operation)
|
|
}
|
|
}
|
|
|
|
override fun onCharacteristicRead(
|
|
gatt: BluetoothGatt,
|
|
characteristic: BluetoothGattCharacteristic,
|
|
status: Int
|
|
) {
|
|
with(characteristic) {
|
|
when (status) {
|
|
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)
|
|
} |