From bed1da4cb8fddb30ef8bab4a0501ddd33348432a Mon Sep 17 00:00:00 2001 From: Thaddeus Ternes Date: Sat, 17 Oct 2026 10:02:34 -0500 Subject: [PATCH] Close the Android GATT client when the link drops with no command in flight A failed connect, an explicit disconnect, and a drop during a queued command each close the peripheral's BluetoothGatt, but a drop the app did not request while no command was running left it registered with the Bluetooth stack. AdapterManager evicts the peripheral on every DeviceDisconnected, so that client was never reused: the next connection built a new Peripheral and a new BluetoothGatt, and the orphan stayed registered. Android's GATT app table is shared with scanners and holds about 30 registrations, so a device that drops and reconnects repeatedly exhausts it. After that every registration fails with status 133, connects fail immediately, scans fail to start, and dropped devices never come back until the app process exits. Close the client in the main callback when it reports STATE_DISCONNECTED and the pending command, if any, has not already closed it. The retry and command paths clear the reference when they close it, so a client is never closed twice. --- .../com/nonpolynomial/btleplug/android/impl/Peripheral.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/Peripheral.java b/src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/Peripheral.java index f502c3c8..de82c1c2 100644 --- a/src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/Peripheral.java +++ b/src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/Peripheral.java @@ -648,6 +648,11 @@ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState Peripheral.this.dispatchToCommandCallback("onConnectionStateChange", () -> Peripheral.this.commandCallback.onConnectionStateChange(gatt, status, newState)); } + // Close it, or the client stays registered with Android and leaks. + if (newState == BluetoothGatt.STATE_DISCONNECTED && Peripheral.this.gatt == gatt) { + gatt.close(); + Peripheral.this.gatt = null; + } suppressDisconnectNotification = Peripheral.this.suppressDisconnectNotification; // A failed connect closes the gatt and clears `connected` even if the state was CONNECTED. nowConnected = Peripheral.this.connected;