Add auto-connect support (#206)

* Add autoConnect support

* Enhance autoConnect functionality for Bluetooth devices

- Added support for managing a set of auto-connect devices in both Android and iOS implementations.
- Updated connection and disconnection logic to handle auto-reconnect scenarios appropriately.
- Improved cleanup processes to prevent unwanted reconnections when devices are manually disconnected.

* Refactor connection button and add auto-reconnect toggle

- Updated the connection button layout for better UI consistency.
- Introduced an auto-reconnect toggle to manage automatic reconnections when devices become available.
- Enhanced connection and disconnection logic to respect the auto-connect setting.

* Update version to 1.2.0 and add CHANGELOG for new features

- Bumped version to 1.2.0.
- Added CHANGELOG.md detailing new features including support for `autoConnect` and UI updates for automatic reconnection.

* Improve readme

* Refactor variable naming for clarity in Bluetooth connection logic

- Changed variable name from `isAutoConnect` to `shouldAutoConnect` for better readability and understanding of its purpose in the connection state handling.

* Refactor connection handling in UniversalBlePlugin

- Simplified connection logic by always cleaning up internal state before sending connection change callbacks.
- Ensured GATT resources are only closed when autoConnect is disabled, allowing for better management of Bluetooth connections.

* Refactor disconnection handling in UniversalBlePlugin

- Introduced a new method `handlePeripheralDisconnection` to encapsulate disconnection logic, improving code readability and maintainability.
- Updated the `centralManager` methods to utilize the new disconnection handler, ensuring consistent behavior across connection state changes.

* Update darwin/Classes/UniversalBlePlugin.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Foti Dim <fotios.dimanidis@a2zebra.de>
Co-authored-by: Navideck Labs <130186950+navidecklabs@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Foti Dim
2026-01-16 07:06:24 +01:00
committed by GitHub
parent a30e84a70c
commit 8557a46d05
28 changed files with 323 additions and 106 deletions
@@ -517,7 +517,7 @@ interface UniversalBlePlatformChannel {
fun startScan(filter: UniversalScanFilter?)
fun stopScan()
fun isScanning(): Boolean
fun connect(deviceId: String)
fun connect(deviceId: String, autoConnect: Boolean?)
fun disconnect(deviceId: String)
fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Long, callback: (Result<Unit>) -> Unit)
fun discoverServices(deviceId: String, withDescriptors: Boolean, callback: (Result<List<UniversalBleService>>) -> Unit)
@@ -686,8 +686,9 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val deviceIdArg = args[0] as String
val autoConnectArg = args[1] as Boolean?
val wrapped: List<Any?> = try {
api.connect(deviceIdArg)
api.connect(deviceIdArg, autoConnectArg)
listOf(null)
} catch (exception: Throwable) {
UniversalBlePigeonUtils.wrapError(exception)
@@ -61,6 +61,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
private val subscriptionResultFutureList = mutableListOf<SubscriptionResultFuture>()
private val pairResultFutures = mutableMapOf<String, (Result<Boolean>) -> Unit>()
private val rssiResultFutureList = mutableListOf<RssiResultFuture>()
private val autoConnectDevices = mutableSetOf<String>()
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
UniversalBlePlatformChannel.setUp(flutterPluginBinding.binaryMessenger, this)
@@ -213,7 +214,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
return safeScanner.isScanning()
}
override fun connect(deviceId: String) {
override fun connect(deviceId: String, autoConnect: Boolean?) {
// If already connected, send connected message,
// if connecting, do nothing
deviceId.findGatt()?.let {
@@ -232,24 +233,31 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
val shouldAutoConnect = autoConnect ?: false
if (shouldAutoConnect) {
autoConnectDevices.add(deviceId)
} else {
autoConnectDevices.remove(deviceId)
}
val remoteDevice = bluetoothManager.adapter.getRemoteDevice(deviceId)
val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
remoteDevice.connectGatt(
context,
false,
shouldAutoConnect,
this,
BluetoothDevice.TRANSPORT_LE
)
} else {
remoteDevice.connectGatt(context, false, this)
remoteDevice.connectGatt(context, shouldAutoConnect, this)
}
gatt.saveCacheIfNeeded()
}
override fun disconnect(deviceId: String) {
autoConnectDevices.remove(deviceId)
val gatt = deviceId.findGatt()
if (gatt == null) {
cleanUpConnection(deviceId)
mainThreadHandler?.post {
callbackChannel?.onConnectionChanged(deviceId, false, null) {}
}
@@ -955,15 +963,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
private fun cleanConnection(gatt: BluetoothGatt) {
gatt.removeCache()
gatt.disconnect()
private fun cleanUpConnection(deviceId: String) {
val deviceDisconnectedError: FlutterError = createFlutterError(
UniversalBleErrorCode.DEVICE_DISCONNECTED,
"Device Disconnected",
)
readResultFutureList.removeAll {
if (it.deviceId == gatt.device.address) {
if (it.deviceId == deviceId) {
it.result(Result.failure(deviceDisconnectedError))
true
} else {
@@ -971,7 +977,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
writeResultFutureList.removeAll {
if (it.deviceId == gatt.device.address) {
if (it.deviceId == deviceId) {
it.result(Result.failure(deviceDisconnectedError))
true
} else {
@@ -979,7 +985,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
subscriptionResultFutureList.removeAll {
if (it.deviceId == gatt.device.address) {
if (it.deviceId == deviceId) {
it.result(Result.failure(deviceDisconnectedError))
true
} else {
@@ -987,7 +993,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
mtuResultFutureList.removeAll {
if (it.deviceId == gatt.device.address) {
if (it.deviceId == deviceId) {
it.result(Result.failure(deviceDisconnectedError))
true
} else {
@@ -995,7 +1001,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
discoverServicesFutureList.removeAll {
if (it.deviceId == gatt.device.address) {
if (it.deviceId == deviceId) {
it.result(Result.failure(deviceDisconnectedError))
true
} else {
@@ -1003,7 +1009,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
rssiResultFutureList.removeAll {
if (it.deviceId == gatt.device.address) {
if (it.deviceId == deviceId) {
it.result(Result.failure(deviceDisconnectedError))
true
} else {
@@ -1012,6 +1018,12 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
}
private fun cleanConnection(gatt: BluetoothGatt) {
gatt.removeCache()
gatt.disconnect()
cleanUpConnection(gatt.device.address)
}
private fun onBondStateUpdate(deviceId: String, bonded: Boolean, error: String? = null) {
val future = pairResultFutures.remove(deviceId)
future?.let { it(Result.success(bonded)) }
@@ -1134,14 +1146,27 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
) {}
}
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
cleanConnection(gatt)
val deviceId = gatt.device.address
val shouldAutoConnect = autoConnectDevices.contains(deviceId)
// Always clean up internal state (futures, etc.)
cleanUpConnection(deviceId)
// Send connection changed callback
mainThreadHandler?.post {
callbackChannel?.onConnectionChanged(
gatt.device.address, false, status.parseHciErrorCode()
deviceId, false, status.parseHciErrorCode()
) {}
}
UniversalBleLogger.logDebug("Closing gatt for ${gatt.device.name}")
gatt.close()
if (!shouldAutoConnect) {
// Only close GATT resources when autoConnect is disabled
gatt.removeCache()
gatt.disconnect()
UniversalBleLogger.logDebug("Closing gatt for ${gatt.device.name}")
gatt.close()
}
// When autoConnect is enabled, keep GATT open for Android to reconnect
}
}