diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt index 05d65b956..70ff6e102 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt @@ -15,10 +15,10 @@ * along with this program. If not, see . */ +@file:Suppress("MissingPermission") + package com.geeksville.mesh.service -import android.Manifest -import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback @@ -27,13 +27,10 @@ import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.content.Context -import android.content.pm.PackageManager import android.os.Build import android.os.DeadObjectException import android.os.Handler import android.os.Looper -import androidx.annotation.RequiresPermission -import androidx.core.content.ContextCompat import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.Logging import com.geeksville.mesh.concurrent.CallbackContinuation @@ -46,12 +43,13 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.Runnable import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeoutOrNull import java.io.Closeable import java.util.Random import java.util.UUID +private val Context.bluetoothManager + get() = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager? + // / Return a standard BLE 128 bit UUID from the short 16 bit versions fun longBLEUUID(hexFour: String): UUID = UUID.fromString("0000$hexFour-0000-1000-8000-00805f9b34fb") @@ -124,88 +122,38 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD */ private val mHandler: Handler = Handler(Looper.getMainLooper()) - /** - * Attempts an emergency restart of the Bluetooth adapter. This is a workaround for certain BLE stack issues. It - * checks for necessary permissions (BLUETOOTH_CONNECT on API 31+, BLUETOOTH_ADMIN on older versions) before - * attempting to disable and then re-enable the adapter. - */ - @Suppress("ReturnCount") fun restartBle() { GeeksvilleApplication.analytics.track("ble_restart") // record # of times we needed to use this nasty hack errormsg("Doing emergency BLE restart") - - val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager - val adapter = bluetoothManager?.adapter - - if (adapter == null) { - errormsg("BluetoothAdapter not available for BLE restart.") - return - } - - val hasPermission = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == - PackageManager.PERMISSION_GRANTED - } else { - ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADMIN) == - PackageManager.PERMISSION_GRANTED - } - - if (!hasPermission) { - errormsg("Missing Bluetooth permission (CONNECT or ADMIN) for BLE restart.") - return - } - - if (adapter.isEnabled) { - warn("Attempting to disable Bluetooth adapter.") - if (!adapter.disable()) { - errormsg("adapter.disable() failed.") - return - } - // TODO: display some kind of UI about restarting BLE - mHandler.postDelayed( - object : Runnable { - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - override fun run() { - if (!adapter.isEnabled) { - warn("Attempting to re-enable Bluetooth adapter.") - if (!adapter.enable()) { - errormsg("adapter.enable() failed.") + context.bluetoothManager?.adapter?.let { adp -> + if (adp.isEnabled) { + adp.disable() + // TODO: display some kind of UI about restarting BLE + mHandler.postDelayed( + object : Runnable { + override fun run() { + if (!adp.isEnabled) { + adp.enable() } else { - info("Bluetooth adapter re-enabled.") + mHandler.postDelayed(this, 2500) } - } else { - // Adapter might have been re-enabled by user or another process, or disable() is async and - // hasn't completed. - // Or, isEnabled check post-disable was too quick. - // If it's still enabled, we retry enabling check later, assuming disable will eventually - // take effect. - warn("Adapter still enabled, retrying enable check soon.") - mHandler.postDelayed(this, 2500) } - } - }, - 2500, - ) - } else { - info("Bluetooth adapter already disabled, attempting to enable.") - if (!adapter.enable()) { - errormsg("adapter.enable() failed while adapter was already disabled.") - } else { - info("Bluetooth adapter enabled.") + }, + 2500, + ) } } } companion object { + + // Our own custom BLE status codes private const val STATUS_RELIABLE_WRITE_FAILED = 4403 private const val STATUS_TIMEOUT = 4404 private const val STATUS_NOSTART = 4405 private const val STATUS_SIMFAILURE = 4406 } - // Our own custom BLE status codes - /** * Should we automatically try to reconnect when we lose our connection? * @@ -214,13 +162,11 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD * responsible for reconnecting. This also prevents nasty races when sometimes both the upperlayer and this layer * decide to reconnect simultaneously. */ - @Suppress("UnusedPrivateProperty") private val autoReconnect = false private val gattCallback = object : BluetoothGattCallback() { - @SuppressLint("MissingPermission") override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) = exceptionReporter { info("new bluetooth connection state $newState, status $status") @@ -252,10 +198,10 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD info("Lost connection - aborting current work: $currentWork") // If we get a disconnect, just try again otherwise fail all current operations - // Note: if no work is pending (likely) we also just totally teardown and restart - // the connection, because we won't be + // Note: if no work is pending (likely) we also just totally teardown and restart the + // connection, because we won't be // throwing a lost connection exception to any worker. - if (autoConnect && (currentWork == null || currentWork?.isConnect() == true)) { + if (autoReconnect && (currentWork == null || currentWork?.isConnect() == true)) { dropAndReconnect() } else { lostConnection("lost connection") @@ -307,7 +253,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD completeWork(status, Unit) } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) override fun onCharacteristicWrite( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, @@ -324,7 +269,8 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD // After this execute reliable completes - we can continue with normal operations (see // onReliableWriteCompleted) } - } else { // Just a standard write - do the normal flow + } else { + // Just a standard write - do the normal flow completeWork(status, characteristic) } } @@ -332,11 +278,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { // Alas, passing back an Int mtu isn't working and since I don't really care what MTU // the device was willing to let us have I'm just punting and returning Unit - if (isSettingMtu) { - completeWork(status, Unit) - } else { - errormsg("Ignoring bogus onMtuChanged") - } + if (isSettingMtu) completeWork(status, Unit) else errormsg("Ignoring bogus onMtuChanged") } /** @@ -429,9 +371,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD workQueue.add(btCont) // if we don't have any outstanding operations, run first item in queue - if (currentWork == null) { - startNewWork() - } + if (currentWork == null) startNewWork() } } @@ -508,7 +448,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD // / if we are in the first non-automated lowLevel connect. private var currentConnectIsAuto = false - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun lowLevelConnect(autoNow: Boolean): BluetoothGatt? { currentConnectIsAuto = autoNow logAssert(gatt == null) @@ -529,7 +468,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD // see https://stackoverflow.com/questions/40156699/which-correct-flag-of-autoconnect-in-connectgatt-of-ble for // more info. // Otherwise if you pass in false, it will try to connect now and will timeout and fail in 30 seconds. - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun queueConnect(autoConnect: Boolean = false, cont: Continuation, timeout: Long = 0) { this.autoConnect = autoConnect @@ -554,30 +492,16 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD * * So you should expect your callback might be called multiple times, each time to reestablish a new connection. */ - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun asyncConnect(autoConnect: Boolean = false, cb: (Result) -> Unit, lostConnectCb: () -> Unit) { logAssert(workQueue.isEmpty()) - - // If there's already connection work in progress, clear it before starting new connection - // This can happen during reconnection where previous connection work wasn't properly cleared - if (currentWork != null) { - warn("Found existing work during asyncConnect: $currentWork - clearing it") - synchronized(workQueue) { stopCurrentWork() } - } + if (currentWork != null) throw AssertionError("currentWork was not null: $currentWork") lostConnectCallback = lostConnectCb - connectionCallback = - if (autoConnect) { - cb - } else { - null - } + connectionCallback = if (autoConnect) cb else null queueConnect(autoConnect, CallbackContinuation(cb)) } // / Restart any previous connect attempts - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - @Suppress("UnusedPrivateMember") private fun reconnect() { // closeGatt() // Get rid of any old gatt @@ -607,7 +531,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD } // / Drop our current connection and then requeue a connect as needed - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun dropAndReconnect() { lostConnection("lost connection, reconnecting") @@ -632,27 +555,22 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD } } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun connect(autoConnect: Boolean = false) = makeSync { queueConnect(autoConnect, it) } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun queueReadCharacteristic( c: BluetoothGattCharacteristic, cont: Continuation, timeout: Long = 0, ) = queueWork("readC ${c.uuid}", cont, timeout) { gatt!!.readCharacteristic(c) } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun asyncReadCharacteristic(c: BluetoothGattCharacteristic, cb: (Result) -> Unit) = queueReadCharacteristic(c, CallbackContinuation(cb)) - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun readCharacteristic(c: BluetoothGattCharacteristic, timeout: Long = timeoutMsec): BluetoothGattCharacteristic = makeSync { queueReadCharacteristic(c, it, timeout) } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun queueDiscoverServices(cont: Continuation, timeout: Long = 0) { queueWork("discover", cont, timeout) { gatt?.discoverServices() @@ -661,12 +579,10 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD } } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun asyncDiscoverServices(cb: (Result) -> Unit) { queueDiscoverServices(CallbackContinuation(cb)) } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun discoverServices() = makeSync { queueDiscoverServices(it) } /** On some phones we receive bogus mtu gatt callbacks, we need to ignore them if we weren't setting the mtu */ @@ -676,23 +592,19 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD * mtu operations seem to hang sometimes. To cope with this we have a 5 second timeout before throwing an exception * and cancelling the work */ - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun queueRequestMtu(len: Int, cont: Continuation) = queueWork("reqMtu", cont, 10 * 1000) { isSettingMtu = true gatt?.requestMtu(len) ?: false } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun asyncRequestMtu(len: Int, cb: (Result) -> Unit) { queueRequestMtu(len, CallbackContinuation(cb)) } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun requestMtu(len: Int): Unit = makeSync { queueRequestMtu(len, it) } private var currentReliableWrite: ByteArray? = null - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun queueWriteCharacteristic( c: BluetoothGattCharacteristic, v: ByteArray, @@ -704,14 +616,12 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD gatt?.writeCharacteristic(c) ?: false } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun asyncWriteCharacteristic( c: BluetoothGattCharacteristic, v: ByteArray, cb: (Result) -> Unit, ) = queueWriteCharacteristic(c, v, CallbackContinuation(cb)) - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun writeCharacteristic( c: BluetoothGattCharacteristic, v: ByteArray, @@ -722,7 +632,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD * Like write, but we use the extra reliable flow documented here: * https://stackoverflow.com/questions/24485536/what-is-reliable-write-in-ble */ - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun queueWriteReliable(c: BluetoothGattCharacteristic, cont: Continuation, timeout: Long = 0) = queueWork("rwriteC ${c.uuid}", cont, timeout) { logAssert(gatt!!.beginReliableWrite()) @@ -730,21 +639,17 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD gatt?.writeCharacteristic(c) ?: false } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun asyncWriteReliable(c: BluetoothGattCharacteristic, cb: (Result) -> Unit) = queueWriteReliable(c, CallbackContinuation(cb)) - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun writeReliable(c: BluetoothGattCharacteristic): Unit = makeSync { queueWriteReliable(c, it) } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private fun queueWriteDescriptor( c: BluetoothGattDescriptor, cont: Continuation, timeout: Long = 0, ) = queueWork("writeD", cont, timeout) { gatt?.writeDescriptor(c) ?: false } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun asyncWriteDescriptor(c: BluetoothGattDescriptor, cb: (Result) -> Unit) = queueWriteDescriptor(c, CallbackContinuation(cb)) @@ -770,7 +675,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD @Volatile private var isClosing = false /** Close just the GATT device but keep our pending callbacks active */ - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun closeGatt() { gatt?.let { g -> info("Closing our GATT connection") @@ -778,18 +682,16 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD try { g.disconnect() - // Wait for our callback to run and handle the disconnect, with a timeout. - runBlocking { - withTimeoutOrNull(1000) { - while (gatt != null) { - delay(100) - } - } + // Wait for our callback to run and handle hte disconnect + var msecsLeft = 1000 + while (gatt != null && msecsLeft >= 0) { + Thread.sleep(100) + msecsLeft -= 100 } gatt?.let { g2 -> warn("Android onConnectionStateChange did not run, manually closing") - gatt = null // clear gatt before calling close, because close might throw dead object exception + gatt = null // clear gat before calling close, bcause close might throw dead object exception g2.close() } } catch (ex: NullPointerException) { @@ -809,7 +711,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD * Close down any existing connection, any existing calls (including async connects will be cancelled and you'll * need to recall connect to use this againt */ - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun closeConnection() { // Set these to null _before_ calling gatt.disconnect(), because we don't want the old lostConnectCallback to // get called @@ -824,7 +725,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD failAllWork(BLEConnectionClosing()) } - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) /** Close and destroy this SafeBluetooth instance. You'll need to make a new instance before using it again */ override fun close() { closeConnection() @@ -833,7 +733,6 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD } // / asyncronously turn notification on/off for a characteristic - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) fun setNotify(c: BluetoothGattCharacteristic, enable: Boolean, onChanged: (BluetoothGattCharacteristic) -> Unit) { debug("starting setNotify(${c.uuid}, $enable)") notifyHandlers[c.uuid] = onChanged