Devices with Android 12 keep bluetooth LE connection even when app is closed
Asked Answered
T

2

6

I'm experiencing an issue, where I can connect to bluetooth device once, but after I disconnect, I no longer see that device when scanning for bluetooth devices. If I completely close the app, device is still undiscoverable, but if I turn off the phone, device becomes discoverable again.

I also noticed that this issue is happending on pixel, huawei and xiomi devices, but seems to work on samsung running android 12.

My assumption is, that there's some strange functionality in android 12 that somehow keeps connection alive separately from the app. In my app I call this code to disconnect:

gatt.close()

Are there any other ways I could make sure device is completely disconnected?

EDIT: Calling

bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)

after disconnect and close still returns my connected device.

EDIT2: I'm able to reproduce this issue with following code:

    private var gatt: BluetoothGatt? = null
    @SuppressLint("MissingPermission")
    fun onDeviceClick(macAddress: String) {
        logger.i(TAG, "onDeviceClick(macAddress=$macAddress)")
        val bluetoothManager: BluetoothManager =
            context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        if (gatt != null) {
            logger.i(TAG, "Disconnecting")
            gatt?.close()
            gatt = null
            printConnectedDevices(bluetoothManager)
            return
        }
        printConnectedDevices(bluetoothManager)
        val btDevice = bluetoothManager.adapter.getRemoteDevice(macAddress)
        logger.d(TAG, "Device to connect: $btDevice")
        gatt = btDevice.connectGatt(context, false, object : BluetoothGattCallback() {
            override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
                super.onConnectionStateChange(gatt, status, newState)
                logger.d(TAG, "Connection state changed to status: $status, sate: $newState")
                when (newState) {
                    BluetoothProfile.STATE_CONNECTED -> {
                        logger.d(TAG, "Connected")
                        printConnectedDevices(bluetoothManager)
                    }
                    BluetoothProfile.STATE_DISCONNECTED -> {
                        logger.d(TAG, "Disconnected")
                        printConnectedDevices(bluetoothManager)
                    }
                }
            }
        })
    }

    @SuppressLint("MissingPermission")
    private fun printConnectedDevices(bluetoothManager: BluetoothManager) {
        val btDevices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
        logger.d(TAG, "Currently connected devices: $btDevices")
    }

Just call onDeviceClick once to connect to device and click again to disconnect. After disconnect I can see in my logs, that for pixel phone, my bluetooth dongle is still shown as connected:

I/SelectDeviceViewModel: onDeviceClick(macAddress=00:1E:42:35:F0:4D)
D/SelectDeviceViewModel: Currently connected devices: []
D/SelectDeviceViewModel: Device to connect: 00:1E:42:35:F0:4D
D/BluetoothGatt: connect() - device: 00:1E:42:35:F0:4D, auto: false
D/BluetoothGatt: registerApp()
D/BluetoothGatt: registerApp() - UUID=ae98a387-cfca-43db-82f0-45fd141979ee
D/BluetoothGatt: onClientRegistered() - status=0 clientIf=12
D/BluetoothGatt: onClientConnectionState() - status=0 clientIf=12 device=00:1E:42:35:F0:4D
D/SelectDeviceViewModel: Connection state changed to status: 0, sate: 2
D/SelectDeviceViewModel: Connected
D/SelectDeviceViewModel: Currently connected devices: [00:1E:42:35:F0:4D]
D/BluetoothGatt: onConnectionUpdated() - Device=00:1E:42:35:F0:4D interval=6 latency=0 timeout=500 status=0
D/BluetoothGatt: onConnectionUpdated() - Device=00:1E:42:35:F0:4D interval=36 latency=0 timeout=500 status=0
D/BluetoothGatt: onConnectionUpdated() - Device=00:1E:42:35:F0:4D interval=9 latency=0 timeout=600 status=0
I/SelectDeviceViewModel: onDeviceClick(macAddress=00:1E:42:35:F0:4D)
I/SelectDeviceViewModel: Disconnecting
D/BluetoothGatt: close()
D/BluetoothGatt: unregisterApp() - mClientIf=12
D/SelectDeviceViewModel: Currently connected devices: [00:1E:42:35:F0:4D]

EDIT3 Log on samsung where everything works:

I/SelectDeviceViewModel: onDeviceClick(macAddress=00:1E:42:35:F0:4D)
D/SelectDeviceViewModel: Currently connected devices: []
D/SelectDeviceViewModel: Device to connect: 00:1E:42:35:F0:4D
I/BluetoothAdapter: STATE_ON
D/BluetoothGatt: connect() - device: 00:1E:42:35:F0:4D, auto: false
I/BluetoothAdapter: isSecureModeEnabled
D/BluetoothGatt: registerApp()
D/BluetoothGatt: registerApp() - UUID=931b9526-ffae-402a-a4b4-3f01edc76e46
D/BluetoothGatt: onClientRegistered() - status=0 clientIf=17
D/BluetoothGatt: onTimeSync() - eventCount=0 offset=346
D/BluetoothGatt: onClientConnectionState() - status=0 clientIf=17 device=00:1E:42:35:F0:4D
D/SelectDeviceViewModel: Connection state changed to status: 0, sate: 2
D/SelectDeviceViewModel: Connected
D/SelectDeviceViewModel: Currently connected devices: [00:1E:42:35:F0:4D]
D/BluetoothGatt: onConnectionUpdated() - Device=00:1E:42:35:F0:4D interval=6 latency=0 timeout=500 status=0
D/BluetoothGatt: onConnectionUpdated() - Device=00:1E:42:35:F0:4D interval=38 latency=0 timeout=500 status=0
D/BluetoothGatt: onConnectionUpdated() - Device=00:1E:42:35:F0:4D interval=9 latency=0 timeout=600 status=0
I/SelectDeviceViewModel: onDeviceClick(macAddress=00:1E:42:35:F0:4D)
I/SelectDeviceViewModel: Disconnecting
D/BluetoothGatt: close()
D/BluetoothGatt: unregisterApp() - mClientIf=17
D/SelectDeviceViewModel: Currently connected devices: []

EDIT4 I've tried modifying above code to first call disconnect() and only call close() when bluetooth connection state changes to disconnected, but it still had the same issue.

Tuba answered 25/7, 2022 at 10:33 Comment(7)
As far as I know, you have to call cancelConnection to disconnect the remote device.Valli
@Risto, sorry for misleading variable name, this is a client application, so it uses BluetoothGatt instead of BluetoothGattServer.Tuba
Ah, ok. But then disconnect needs to be called to disconnect the remote device.Valli
Maybe this answer helps a little bit.Valli
This is really awkward. Looks like Samsung handles some issues of Google's BLE stack :). If the Pixel phone is of Google, it must have pure, unmodified Google's BLE stack. So once we solve this issue it will work for most of the mobiles in the market.Terrorize
Personally, I like Googles approach much more than Samsungs ugly aggravation. And the behaviour of first disconnecting and then closing the server when you're done is exactly the behaviour that Google also reproduces in its sample implementation.Valli
I have this problem with ESP32-C3 device, with my own firmware. What device or microcontroller are you using?Preconceive
C
2
  1. Make sure you call close() on the correct BluetoothGatt object. You haven't posted the full code so it's impossible to say if this is done correctly.

  2. When you "completely close the app", make sure you "force quit" the app rather than just closing all activities, since closing all activities might still leave the app process running. When an app process quits for any reason, the Bluetooth stack will automatically close the process's BluetoothGatt objects that haven't been closed.

  3. Any app can have a BluetoothGatt object that references a connection. All these references must be disconnected or closed in order for the disconnect attempt to take place. BLE debug apps might hold connections active, so make sure to close all such apps.

  4. If the device still doesn't disconnect after following the above, try to inspect hci snoop log or logcat for clues.

Canova answered 26/7, 2022 at 1:21 Comment(1)
I've made sure it's single and same instance of gatt object. I do use force quit. I don't think I use other bluetooth apps, but it wouldn't be a surprise if OS itself has some apps using this connection. I'll try hci logs.Tuba
T
1

Calling gatt.close only is not enough. In order to disconnect properly from the gatt server; you need to call BlueotoothGatt.disconnect first, then in onConnectionStateChange callback you must call the BluetoothGatt.close.

Some where in your activity

// Somewhere in your activity where you want to disconnect
// might be adequate in onPause callback
if(bleAdapter != null && bleAdapter.isConnected) {
    bleAdapter.disconnect(); // In bleAdapter you're supposed to hold a reference to the gatt object.
}

In your BLE adapter implementation

private void disconnect() {;
    if (bluetooth_adapter == null || bluetooth_gatt == null) {
        Log.d("disconnect: bluetooth_adapter|bluetooth_gatt null");
        return;
    }
    if (bluetooth_gatt != null) {
        bluetooth_gatt.disconnect();
    }
}

// And finally in your BluetoothGattCallback.onConnectionStateChange implementation you call the close method on your BluetoothGatt object reference
private final BluetoothGattCallback gatt_callback = new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        Log.d(TAG, "onConnectionStateChange: status=" + status);
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            Log.d(TAG, "onConnectionStateChange: CONNECTED");
            connected = true;
        }
        else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            Log.d(TAG, "onConnectionStateChange: DISCONNECTED");
            connected = false;
            if (bluetooth_gatt != null) {
                Log.d(TAG,"Closing and destroying BluetoothGatt object");
                bluetooth_gatt.close();
                bluetooth_gatt = null;
            }
        }
    }

    // Other callbacks
};

Update: My test results on the minimal reproducible example

I converted your MRE kotlin code to java and the code for the following 2 cases is:

// This listener is defined in the onCreate callback
binding.fab.setOnClickListener(view -> {
    final String target = "50:8C:B1:69:E5:63";
    BluetoothManager bluetoothManager = getSystemService(BluetoothManager.class);
    if(gatt != null) {
        Log.d(TAG, "Disconnecting");
        gatt.close();
        gatt = null;
        printConnectedDevices(bluetoothManager);
        return;
    }
    printConnectedDevices(bluetoothManager);
    BluetoothDevice bd = bluetoothManager.getAdapter().getRemoteDevice(target);
    if(bd != null) {
        gatt = bd.connectGatt(this, false, new BluetoothGattCallback() {
            @Override
            public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
                Log.d(TAG, "onConnectionStateChange: status "+status+", new state "+newState);
                if(newState == BluetoothProfile.STATE_CONNECTED) {
                    Log.d(TAG, "onConnectionStateChange: Connected");
                    printConnectedDevices(bluetoothManager);
                }
                else if(newState == BluetoothProfile.STATE_DISCONNECTED) {
                    Log.d(TAG, "onConnectionStateChange: Disconnected");
                    printConnectedDevices(bluetoothManager);
                }
            }
        });
    }
});

private void printConnectedDevices(BluetoothManager bluetoothManager) {
    @SuppressLint("MissingPermission") List<BluetoothDevice> devices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
    if(devices != null && !devices.isEmpty()) {
        for(BluetoothDevice bd: devices) {
            Log.d(TAG, "printConnectedDevices: "+bd.getAddress());
        }
    } else {
        Log.d(TAG, "printConnectedDevices: no devices");
    }
}

Here are the results for 2 devices...

Motorola G5S Plus - Android 11 (Running LineageOS 18.1)

D/MainActivity: printConnectedDevices: no devices
D/BluetoothGatt: connect() - device: 50:8C:B1:69:E5:63, auto: false
D/BluetoothGatt: registerApp()
D/BluetoothGatt: registerApp() - UUID=2c71d08a-1fb3-411c-8d85-26f49749c932
D/BluetoothGatt: onClientRegistered() - status=0 clientIf=6
D/BluetoothGatt: onClientConnectionState() - status=0 clientIf=6 device=50:8C:B1:69:E5:63
D/MainActivity: onConnectionStateChange: status 0, new state 2
D/MainActivity: onConnectionStateChange: Connected
D/MainActivity: printConnectedDevices: 50:8C:B1:69:E5:63
D/BluetoothGatt: onConnectionUpdated() - Device=50:8C:B1:69:E5:63 interval=6 latency=0 timeout=500 status=0
D/BluetoothGatt: onConnectionUpdated() - Device=50:8C:B1:69:E5:63 interval=30 latency=0 timeout=600 status=0
D/MainActivity: Disconnecting
D/BluetoothGatt: close()
D/BluetoothGatt: unregisterApp() - mClientIf=6
D/MainActivity: printConnectedDevices: no devices

Samsung Tab A8 SM-T290 - Android 12 Generic System Image

D/MainActivity: printConnectedDevices: no devices
D/BluetoothGatt: connect() - device: 50:8C:B1:69:E5:63, auto: false
D/BluetoothGatt: registerApp()
D/BluetoothGatt: registerApp() - UUID=558b17e5-6770-4f46-a4d8-4f8d0024d86c
D/BluetoothGatt: onClientRegistered() - status=0 clientIf=5
D/BluetoothGatt: onClientConnectionState() - status=0 clientIf=5 device=50:8C:B1:69:E5:63
D/MainActivity: onConnectionStateChange: status 0, new state 2
D/MainActivity: onConnectionStateChange: Connected
D/MainActivity: printConnectedDevices: 50:8C:B1:69:E5:63
D/BluetoothGatt: onConnectionUpdated() - Device=50:8C:B1:69:E5:63 interval=6 latency=0 timeout=500 status=0
D/BluetoothGatt: onConnectionUpdated() - Device=50:8C:B1:69:E5:63 interval=30 latency=0 timeout=600 status=0
D/MainActivity: Disconnecting
D/BluetoothGatt: close()
D/BluetoothGatt: unregisterApp() - mClientIf=5
D/MainActivity: printConnectedDevices: no devices
Terrorize answered 25/7, 2022 at 12:56 Comment(16)
That is incorrect. The disconnect() method properly initiates a disconnection attempt for this BluetoothGatt object. The close() method releases the resources of this BluetoothGatt object, which is important when you don't need the object anymore since the system can only have a few BluetoothGatt objects alive. Maybe you have another app on the phone having a connection to the remote device, such as nRF Connect. Make sure you exit all such apps.Canova
@Canova thanks for your feedback. Could you please point out clearly what is incorrect in the example?Terrorize
This is the statement that is not correct: "In order to disconnect properly from the gatt server; you need to call BlueotoothGatt.disconnect first, then in onConnectionStateChange callback you must call the BluetoothGatt.close.". It is enough to call .close() since a close implies a disconnect. What can hold on to a connection is if another BluetoothGatt object or another app also has a connection to the same device.Canova
@Canova I see, thank you again. Now let me tell you the difference between calling the disconnect and close methods. These two methods go the same destination, but through diferent paths. When you call the close without calling disconnect; the gatt service immediately will unregister the callback. So the application will not know anything about the connection changes. If an application needs to take some actions on disconnection then calling close directly will disconnect, but will not serve for the purpose of application.Terrorize
On the other hand, when disconnect method is called first the application will receive onConnectionStateChange callback. This gives the flexibility to handle the disconnection in the UI and release related resources if any. Any professional application will take care of connection state changes appropriately. But if the OP's project is a hoby or workhome project then, as you stated, my statement is incorrect for this kind of projects.Terrorize
If you don't want to show a "disconnecting..." UI (e.g. if no Activities belonging to the app are in the foreground right now), it is perfectly fine to just call .close(). There is nothing unprofessional with that, and can also make the code simpler, potentially avoiding bugs a more complex solution might introduce.Canova
potentially avoiding bugs a more complex solution might introduce. Oh yeah? Isn't the Android BLE library and even BLE specification itself complex by nature? What's wrong in Any professional application will take care of connection state changes appropriately.? Why you needed to reply with There is nothing unprofessional with that against Any professional application will take care of connection state changes appropriately. In this frase I'm talking about taking care of connection state CHANGES appropriately not the disconnection only.Terrorize
Anyway @Canova thanks for the opinions. There is no one solution that suits every situation. If my solution doesn't work you can give an alternative that fits the OP's needs.Terrorize
@Kozmotronik, thanks, I tried your apporach, but it still doesn't work. Even after all disconnect() and close() calls, bluetoothManager.getConnectedDevices(BluetoothProfile.GATT) returns connected device.Tuba
@Tuba do you mind sharing your implementation or at least in form of minimal reproducible example?Terrorize
Unfortunately the vendors can make changes to the Android's BLE stack. Does this problem occur only in API level 31 - Android 12?Terrorize
@Kozmotronik, I've pasted reproducible example. So far I've only noticed issues on Android 12, as Android 10 seems to work just fine.Tuba
@Tuba I've tested your MRE in two devices with no issues. See the updated answers. May be you should turn off the Bluetooth, and then shutdown the phone wait for some moment and then turn on the phone and try connecting and disconnecting again. Do these steps for the phone on which gatt.close doesn't work well.Terrorize
@Kozmotronik, thank you very much for the effort! Unfortunately devices you have don't fall into category where they might experience this issue. It seems it's only Android 12 related, and not on samsung. I've tried your code, I've restarted my phone, but I still got same outcome on google pixel.Tuba
@Tuba yw. Note that the Samsung tablet is running Android 12 GSI not any of Samsung's ROM. But anyway, it is hard to debug since the error occurs only on a specific device. I only could so far. But I would appreciate if you could update your question with the solution in case you find out and solve the issue in future. Because I'm interested to know it.Terrorize
I am facing similar problem. Does anyone knows the answer?Circumlunar

© 2022 - 2024 — McMap. All rights reserved.