diff --git a/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt b/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt new file mode 100644 index 0000000..b88eae5 --- /dev/null +++ b/app/src/main/java/com/example/sensortestingapp/ConnectionManager.kt @@ -0,0 +1,267 @@ +/* + * Copyright 2019 Punch Through Design LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.punchthrough.blestarterappandroid.ble + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothProfile +import android.content.Context +import android.util.Log +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue + +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 DiscoverServicesRequest( + override val device: BluetoothDevice, +) : BleOperationType() + + +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() } + Log.i( + "printGattTable", "\nService ${service.uuid}\nCharacteristics:\n$characteristicsTable" + ) + } +} + + +@SuppressLint("MissingPermission") +object ConnectionManager { + + private val deviceGattMap = ConcurrentHashMap() + private val operationQueue = ConcurrentLinkedQueue() + private var pendingOperation: BleOperationType? = null + + + fun connect(device: BluetoothDevice, context: Context) { + if (device.isConnected()) { + Log.e("ConnectionManager", "Already connected to ${device.address}!") + } else { + enqueueOperation(Connect(device, context.applicationContext)) + } + } + + 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) { + enqueueOperation(MtuRequest(device, mtu.coerceIn(GATT_MIN_MTU_SIZE, GATT_MAX_MTU_SIZE))) + + } + + fun discoverServices(device: BluetoothDevice) { + enqueueOperation(DiscoverServicesRequest(device)) + + } + + // - Beginning of PRIVATE functions + + @Synchronized + private fun enqueueOperation(operation: BleOperationType) { + operationQueue.add(operation) + if (pendingOperation == null) { + doNextOperation() + } + } + + @Synchronized + private fun signalEndOfOperation() { + Log.d("ConnectionManager", "End of $pendingOperation") + pendingOperation = null + if (operationQueue.isNotEmpty()) { + doNextOperation() + } + } + + /** + * Perform a given [BleOperationType]. All permission checks are performed before an operation + * can be enqueued by [enqueueOperation]. + */ + + @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, false, callback, BluetoothDevice.TRANSPORT_LE + ) + } + return + } + + // Check BluetoothGatt availability for other operations + val gatt = deviceGattMap[operation.device] + ?: this@ConnectionManager.run { + Log.e( + "ConnectionManager", + "Not connected to ${operation.device.address}! Aborting $operation operation." + ) + signalEndOfOperation() + return + } + + when (operation) { + is Disconnect -> with(operation) { + Log.w("ConnectionManager", "Disconnecting from ${device.address}") + gatt.close() + deviceGattMap.remove(device) + signalEndOfOperation() + } + + is MtuRequest -> with(operation) { + gatt.requestMtu(mtu) + } + + is DiscoverServicesRequest -> with(operation) { + gatt.discoverServices() + } + + + is Connect -> { + Log.e("ConnectionManager", "Shouldn't get here") + } + } + } + + private val callback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + val deviceAddress = gatt.device.address + + if (status == BluetoothGatt.GATT_SUCCESS) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + Log.w( + "ConnectionManager", + "onConnectionStateChange: connected to $deviceAddress" + ) + deviceGattMap[gatt.device] = gatt + if (pendingOperation is Connect) { + signalEndOfOperation() + } + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + Log.e( + "ConnectionManager", + "onConnectionStateChange: disconnected from $deviceAddress" + ) + 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() + } else { + Log.e("ConnectionManager", "Service discovery failed due to status $status") + teardownConnection(gatt.device) + } + } + + if (pendingOperation is DiscoverServicesRequest) { + signalEndOfOperation() + } + } + + override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { + Log.w( + "ConnectionManager", + "ATT MTU changed to $mtu, success: ${status == BluetoothGatt.GATT_SUCCESS}" + ) + + if (pendingOperation is MtuRequest) { + signalEndOfOperation() + } + } + + + } + + private fun BluetoothDevice.isConnected() = deviceGattMap.containsKey(this) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/sensortestingapp/MainActivity.kt b/app/src/main/java/com/example/sensortestingapp/MainActivity.kt index f98b5e7..e81390e 100644 --- a/app/src/main/java/com/example/sensortestingapp/MainActivity.kt +++ b/app/src/main/java/com/example/sensortestingapp/MainActivity.kt @@ -5,11 +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.BluetoothGatt -import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothManager -import android.bluetooth.BluetoothProfile import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings @@ -30,6 +26,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import com.example.sensortestingapp.databinding.ActivityMainBinding +import com.punchthrough.blestarterappandroid.ble.ConnectionManager private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1 @@ -56,24 +53,6 @@ fun Context.hasRequiredRuntimePermissions(): Boolean { } } -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() } - Log.i( - "printGattTable", "\nService ${service.uuid}\nCharacteristics:\n$characteristicsTable" - ) - } -} @SuppressLint("MissingPermission") class MainActivity : AppCompatActivity() { @@ -103,77 +82,9 @@ class MainActivity : AppCompatActivity() { private val kirbyScanResults = ArrayList(); + private var mtuSizeInBytes = 23 - private val gattCallback = object : BluetoothGattCallback() { - override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - val deviceAddress = gatt.device.address - val deviceName = gatt.device.name - - if (status == BluetoothGatt.GATT_SUCCESS) { - if (newState == BluetoothProfile.STATE_CONNECTED) { - - Log.i( - "BluetoothGattCallback", - "Successfully connected to $deviceAddress (${deviceName})" - ) - - if (!gatt.requestMtu(GATT_REQUESTED_MTU_SIZE)) { - Log.e( - "BluetoothGattCallback", - "Requesting MTU size failed" - ) - } - - - if (!gatt.discoverServices()) { - Log.e( - "BluetoothGattCallback", - "Service discovery did not start" - ) - } - // TODO: Store a reference to BluetoothGatt - } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - Log.w( - "BluetoothGattCallback", - "Successfully disconnected from $deviceAddress (${deviceName})" - ) - gatt.close() - } - } else { - Log.w( - "BluetoothGattCallback", - "Error $status encountered for $deviceAddress (${deviceName})! Disconnecting..." - ) - gatt.close() - } - } - - override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - with(gatt) { - Log.i( - "BluetoothGattCallback", - "Discovered ${services.size} services for ${device.address} (${device.name})" - ) - printGattTable() - } - } - - override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i( - "BluetoothGattCallback", - "mtuSizeInBytes set to ${mtu}" - ) - mtuSizeInBytes = mtu - } else { - Log.w( - "BluetoothGattCallback", - "Unsuccessful MTU change. Leaving mtuSizeInBytes unchanged ${mtuSizeInBytes}" - ) - } - } - } private val scanResultAdapter: ScanResultAdapter by lazy { ScanResultAdapter(kirbyScanResults) { scanResult -> @@ -187,13 +98,9 @@ class MainActivity : AppCompatActivity() { "Start connecting to ${scanResult.device} (${scanResult.device.name})" ) - scanResult.device.connectGatt( - applicationContext, - false, - gattCallback, - BluetoothDevice.TRANSPORT_LE - ) - + ConnectionManager.connect(scanResult.device, applicationContext) + ConnectionManager.requestMtu(scanResult.device, Int.MAX_VALUE) + ConnectionManager.discoverServices(scanResult.device) } }