关于在 android 上创建自动连接到 BLE 设备的服务的问题

Questions on creating a service that connects automatically to a BLE device on android

我正在实施一项服务,该服务使用 bluetoothGattautoconnect 功能连接到设备并在连接时对其进行监控。

我假设设备已经绑定(同事负责这部分),所以自动连接应该没有任何问题

我的代码如下:

//the callback is for the class I have created that actually does the connection
class BTService: Service(), CoroutineScope, BTConnection.Callback {
    private val btReceiver by lazy { BluetoothStateReceiver(this::btStateChange) } //receiver for bt adapter changes

    private var connection:BTConnection? = null
    private var readJob:Job? = null

    override fun onCreate() {
        buildNotificationChannels()
        registerReceiver(btReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) //since I can't register this receiver in AndroidManifest any more I did it here
    }

    private fun btStateChange(enabled: Boolean) {
        if (enabled)
            startConnecting()
        else
            stopConnection()
    }

    private fun startConnecting() {
        
        val address = prefs.address //get the current saved address
        val current = connection //get the current connection

        //try to stop the current connection if it is different than the one we want to set up
        if (current != null && !current.address.equals(address, true))
            current.stop()

        if (address.isNullOrBlank())
            return
        //then we create a new connection if needed
        val new = if (current == null || !current.address.equals(address, true)) {
            Injections.buildConnection(application, address, this)
        } else {
            current
        }
        connection = new
        new.connect()
    }

    //this is one of the callbacks from BTConnection.Callback
    override fun connected(address: String) {
        if (address != connection?.address) return
        val cn = connection ?: return
        showConnectionNotification()
        val notification = buildForegroundNotification()
        startForeground(FOREGROUND_ID, notification)
        readJob?.cancel()
        readJob = launch {
             cn.dataFlow //this is a flow that will be emmitting read data
             .cancellable() 
             .flowOn(Dispatchers.IO)
             .buffer()
             .onEach(this@BTService::parseData)
             .flowOn(Dispatchers.Default)
        }
    }


    private suspend fun parseData(bytes:ByteArray) { //this is where the parsing and storage etc happens
}

private fun stopConnection() {
    val cn = connection
    connection = null
    cn?.stop()
}
 
override fun disconnected(address: String) { //another callback from the connection class
    showDisconnectNotification()
    stopForeground(true)
}

我停止连接的代码是

fun stop() {
    canceled = true
    if (connected)
        gatt?.disconnect()
    launch(Dispatchers.IO) {
        delay(1000)
        gatt?.close()
        gatt = null
    }
}

我的代码基于(并受到)我阅读的这篇非常好的文章:

https://medium.com/@martijn.van.welie/making-android-ble-work-part-2-47a3cdaade07

我还为将调用

的启动事件创建了一个接收器
 context.startService(Intent(context, BTService::class.java))

只是为了确保服务至少创建一次并且 bt 接收器已注册

我的问题是:

a) 当我的服务不在前台模式时,它是否有可能被破坏?即当设备不在附近并且 bluetoothGat.connect 在自动连接时挂起?我从 onStartCommand() return START_STICKY 是否足以确保即使我的服务被销毁它也会重新启动?

b) 如果有这种情况,有没有办法至少重新创建服务,以便至少注册 btReceiver?

c) 如果 autoconnect = true,什么时候应该在 bluetoothGatt 上调用 close()?仅在创建新连接时(在我调用 Injections.buildConnection 的示例中)?当蓝牙适配器被禁用时我也调用它吗?或者,如果用户关闭蓝牙适配器并再次打开,我可以重复使用相同的连接和 bluetoothGatt 吗?

d) 有没有办法查明自动连接是否失败并且不再重试?有没有办法实际测试和重现这种效果?上面提到的文章说它可能发生当外围设备的电池几乎没电时,或者当你处于蓝牙范围的边缘时

提前感谢您提供的任何帮助

首先,如果您打算将您的应用分发到 Google Play 商店,如果我没记错的话,您需要将最低 api 级别设置为 29,因此您应该使用 JobService 和 JobScheduler 或 WorkManager,而不是 Service。这是为了支持 Oreo(26) 之后的背景限制。

a) 如果你正确地实现了我上面提到的两个选项中的任何一个,你就可以编写一个正确的服务,除非你停止它,否则它不会终止。以下是有关 JobService 的一些资源:(resource1, resource2, resource3)

b) 您可以根据需要在 JobService 的 onStartJob() 方法上重新注册,这将重新创建您的应用程序。

c) 每次使用完外围设备后,都需要关闭与它的gatt连接。这是 BluetoothGatt class

的一个片段
/**
 * Close this Bluetooth GATT client.
 *
 * Application should call this method as early as possible after it is done with
 * this GATT client.
 */
public void close() {

此外,从 BluetoothAdapter class javadoc,您可以看到当 ble 被禁用时所有连接都会正常终止。

 /**
 * Turn off the local Bluetooth adapter—do not use without explicit
 * user action to turn off Bluetooth.
 * <p>This gracefully shuts down all Bluetooth connections, stops Bluetooth
 * system services, and powers down the underlying Bluetooth hardware.
 * <p class="caution"><strong>Bluetooth should never be disabled without
 * direct user consent</strong>. The {@link #disable()} method is
 * provided only for applications that include a user interface for changing
 * system settings, such as a "power manager" app.</p>
 * <p>This is an asynchronous call: it will return immediately, and
 * clients should listen for {@link #ACTION_STATE_CHANGED}
 * to be notified of subsequent adapter state changes. If this call returns
 * true, then the adapter state will immediately transition from {@link
 * #STATE_ON} to {@link #STATE_TURNING_OFF}, and some time
 * later transition to either {@link #STATE_OFF} or {@link
 * #STATE_ON}. If this call returns false then there was an
 * immediate problem that will prevent the adapter from being turned off -
 * such as the adapter already being turned off.
 *
 * @return true to indicate adapter shutdown has begun, or false on immediate error
 */
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public boolean disable() {

d) 我不确定会触发什么回调。要重现,您提到的两项似乎是可以尝试的有效案例。

希望这能帮助您完善您的项目!

a-b) 如果您的应用没有 activity 或前台服务,系统可能会随时终止它。挂起或活动的 BLE 连接不会影响系统何时终止应用程序的观点。 (当谈到扫描广告时,情况就完全不同了。)

确保 autoConnects 保持活动状态的一般方法是始终有一个前台服务 运行。因此,如果您想要挂起连接,请不要在设备当前未连接时停止它。使用 Job Scheduler、WorkManagers 等是没有意义的,因为有一个前台服务应该足以让应用程序进程保持活动状态,并且只要应用程序存在,pending/active 连接就会保持活动状态。在等待挂起的 BLE 连接时,应用程序根本不使用任何 cpu%。然而,一些中国 phone 制造商不遵守 Android 文档,有时会杀死应用程序,即使它们有 运行 前台服务。

c) 每个 BluetoothGatt 对象代表并引用同一 phone 上的蓝牙进程 运行 内的一个对象。默认情况下,系统总共允许 32 个这样的对象(我上次检查过)。为了释放这些宝贵的资源,你调用close()。如果您忘记了,就会发生泄漏,这意味着您的应用程序或其他一些应用程序可能无法创建 BluetoothGatt 对象。 (当应用程序进程退出时,它们的 BluetoothGatt 对象会自动关闭)。 API设计的有点奇怪,既有disconnect方法又有close方法。但无论如何,disconnect 方法会优雅地启动连接断开连接,然后您将收到一个 onConnectionStateChange 回调,告知断开连接何时完成。但是,您必须调用 close 才能释放资源,或者如果您想重新连接,则必须调用 connect,或者您可以稍后再执行操作。在已连接的 BluetoothGatt 对象上调用 close 也会断开连接,但由于对象同时被销毁,您不会得到任何回调。

由于所有 BluetoothGatt 对象都代表蓝牙进程中的对象,因此当您关闭蓝牙时,这些对象将“死亡”或停止工作,因为这涉及关闭蓝牙进程。这意味着您需要在重新启动蓝牙时重新创建所有 BluetoothGatt 对象。您可以在旧对象上调用 close,但它不会做任何事情,因为它们已经死了。由于文档对此没有任何说明,我建议您无论如何调用 close 以防万一将来行为发生变化。

d) 要检测 connectGatt 调用是否失败并且不会重试,您可以监听 onConnectionStateChange 回调。如果这给出错误代码,例如 257,通常意味着系统已达到最大连接数,或某些资源的最大数量。您可以通过简单地启动与一堆不同蓝牙设备地址的挂起连接来测试这一点。

如果外围设备电量不足或处于“蓝牙范围边缘”,新的连接尝试将被中止,我不相信这种说法。我很高兴看到发生这种情况的 Android 的蓝牙源代码的针点,因为我真的相信这根本不是真的。