Warm tip: This article is reproduced from serverfault.com, please click

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

发布于 2020-12-03 00:40:43

I am implementing a service that uses the autoconnect feature of bluetoothGatt to connect to the device and monitor it while it is being connected.

I work on the assumption that the device is already bonded (a coworker is responsible for that part) so autoconnect should not have any problems

my code is as follows:

//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)
}

my code that stops the connection is

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

my code is based (and affected) by this really good article I read:

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

I have also created a receiver for boot events that will call

 context.startService(Intent(context, BTService::class.java))

just to make sure that the service is created at least once and the bt receiver is registered

my questions are:

a) is there a chance that my service will be destroyed while it is not in foreground mode? i.e. when the device is not near by and bluetoothGat.connect is suspending while autoconnecting? is it enough for me to return START_STICKY from onStartCommand() to make sure that even when my service is destroyed it will start again?

b) if there is such a case, is there a way to at least recreate the service so the btReceiver is at least registered?

c) when should close() be called on bluetoothGatt in case of autoconnect = true? only when creating a new connection (in my example where I call Injections.buildConnection)? do I also call it when the bluetoothadapter is disabled? or can I reuse the same connection and bluetoothGatt if the user turns the bluetooth adapter off and on again?

d) is there a way to find out if autoconnect has failed and will not try again? and is there a way to actually test and reproduce such an effect? the article mentioned above says it can happen when the batteries of the peripheral are almost empty, or when you are on the edge of the Bluetooth range

thanks in advance for any help you can provide

Questioner
Cruces
Viewed
0
Emil 2020-12-11 08:10:03

a-b) If your app does not have an activity or a service that is in the foreground, the system may kill it at anytime. Pending or active BLE connections doesn't affect the system's point of view when to kill the app whatsoever. (When it comes to scanning for advertisements, the story is completely different though.)

The general approach to make sure autoConnects stay alive is to have a foreground service running at all the time. So don't stop it while the device is currently not connected, if you want to have a pending connection. There is no point in using Job Scheduler, WorkManagers etc. since having a foreground service should be enough to keep the app process alive, and pending/active connections are kept alive as long as the app is. The app does not use any cpu% at all when waiting for pending BLE connections. However some Chinese phone makers are known to not follow the Android documentation, by sometimes killing apps even though they have running foreground services.

c) Each BluetoothGatt object represents and refers to an object inside the Bluetooth process running on the same phone. By default the system allows a total of 32 such objects (last time I checked). In order to release these precious resources, you call close(). If you forget, you will have a leak, meaning your app or some other app might not be able to create a BluetoothGatt object. (When app processes exit, their BluetoothGatt objects are however closed automatically). The API is a bit strangely designed, that there is both a disconnect method and a close method. But anyway, the disconnect method gracefully initiates a disconnection of the connection and you will then get an onConnectionStateChange callback telling when the disconnection is complete. You must however call close in order to free the resource, or call connect if you'd like to re-connect, or you can take an action a bit later. Calling close on a connected BluetoothGatt object will also disconnect, but you won't get any callback due to the object is being destroyed at the same time.

Since all BluetoothGatt objects represents objects in the Bluetooth process, these will "die" or stop working when you turn off Bluetooth, since that involves shutting down the Bluetooth process. This means you need to recreate all BluetoothGatt objects when Bluetooth is restarted. You can call close on the old objects, but it won't do anything since they're dead. Since the documentation doesn't say anything about this, I suggest you call close anyway to be on the safe side if the behaviour is changed in the future.

d) To detect if a connectGatt call fails and will not try again, you can listen to the onConnectionStateChange callback. If this gives an error code, such as 257, it usually means that the system has reached maximum number of connections, or maximum number of some resource. You can test this out by simply initiating pending connections to a bunch of different Bluetooth device addresses.

I would not trust the statement that new connection attempts would be aborted if the peripheral is low on battery or being on the "edge of Bluetooth range". I'd be glad to see a pin point to Android's Bluetooth source code where this happens, since I really believe this is not true at all.