revert(safebluetooth): reverts changes to SafeBluetooth.kt (#3095)

pull/3096/head
James Rich 2025-09-14 13:51:33 -05:00
rodzic a8b5b4a62d
commit 0634859742
1 zmienionych plików z 35 dodań i 136 usunięć

Wyświetl plik

@ -15,10 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@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<Unit>, 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>) -> 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<Unit> { queueConnect(autoConnect, it) }
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private fun queueReadCharacteristic(
c: BluetoothGattCharacteristic,
cont: Continuation<BluetoothGattCharacteristic>,
timeout: Long = 0,
) = queueWork("readC ${c.uuid}", cont, timeout) { gatt!!.readCharacteristic(c) }
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun asyncReadCharacteristic(c: BluetoothGattCharacteristic, cb: (Result<BluetoothGattCharacteristic>) -> 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<Unit>, 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>) -> Unit) {
queueDiscoverServices(CallbackContinuation(cb))
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun discoverServices() = makeSync<Unit> { 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<Unit>) = queueWork("reqMtu", cont, 10 * 1000) {
isSettingMtu = true
gatt?.requestMtu(len) ?: false
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun asyncRequestMtu(len: Int, cb: (Result<Unit>) -> 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<BluetoothGattCharacteristic>) -> 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<Unit>, 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>) -> 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<BluetoothGattDescriptor>,
timeout: Long = 0,
) = queueWork("writeD", cont, timeout) { gatt?.writeDescriptor(c) ?: false }
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
fun asyncWriteDescriptor(c: BluetoothGattDescriptor, cb: (Result<BluetoothGattDescriptor>) -> 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