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

Viewed 184

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.

2 Answers

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
  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.

Related