Skip to content

Commit

Permalink
Android: enforce 2s delay between connect and disconnect
Browse files Browse the repository at this point in the history
  • Loading branch information
chipweinberger committed Jul 23, 2024
1 parent 9c77b29 commit 4015129
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 1 deletion.
43 changes: 42 additions & 1 deletion lib/src/bluetooth_device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ class BluetoothDevice {
// Start listening now, before invokeMethod, to ensure we don't miss the response
Future<BmConnectionStateResponse> futureState = responseStream.first;

// record connection time
if (Platform.isAndroid) {
FlutterBluePlus._connectTimestamp[remoteId] = DateTime.now();
}

// invoke
bool changed = await FlutterBluePlus._invokeMethod('connect', request.toMap());

Expand Down Expand Up @@ -178,7 +183,17 @@ class BluetoothDevice {
/// - [queue] If true, this disconnect request will be executed after all other operations complete.
/// If false, this disconnect request will be executed right now, i.e. skipping to the front
/// of the fbp operation queue, which is useful to cancel an in-progress connection attempt.
Future<void> disconnect({int timeout = 35, bool queue = true}) async {
/// - [androidDelay] Android only. Minimum gap in milliseconds between connect and disconnect to
/// workaround a race condition that leaves connection stranded. A stranded connection in this case
/// refers to a connection that FBP and Android Bluetooth stack are not aware of and thus cannot be
/// disconnected because there is no gatt handle.
/// https://issuetracker.google.com/issues/37121040
/// From testing, 2 second delay appears to be enough.
Future<void> disconnect({
int timeout = 35,
bool queue = true,
int androidDelay = 2000,
}) async {
// Only allow a single disconnect operation at a time
_Mutex dtx = _MutexFactory.getMutexForKey("disconnect");
await dtx.take();
Expand All @@ -203,13 +218,21 @@ class BluetoothDevice {
// Start listening now, before invokeMethod, to ensure we don't miss the response
Future<BmConnectionStateResponse> futureState = responseStream.first;

// Workaround Android race condition: ensure minimum connect disconnect gap is met
await _ensureAndroidDisconnectionDelay(androidDelay);

// invoke
bool changed = await FlutterBluePlus._invokeMethod('disconnect', remoteId.str);

// only wait for disconnection if weren't already disconnected
if (changed) {
await futureState.fbpEnsureAdapterIsOn("disconnect").fbpTimeout(timeout, "disconnect");
}

if (Platform.isAndroid) {
// Disconnected, remove connect timestamp
FlutterBluePlus._connectTimestamp.remove(remoteId);
}
} finally {
dtx.give();
if (queue) {
Expand Down Expand Up @@ -664,6 +687,24 @@ class BluetoothDevice {
return gatt?.characteristics._firstWhereOrNull((chr) => chr.uuid == servicesChangedUuid);
}

/// Workaround race condition between connect and disconnect leaving connection stranded by enforcing a small delay
/// between connect and disconnect call.
/// https://issuetracker.google.com/issues/37121040
Future<void> _ensureAndroidDisconnectionDelay(int androidDelay) async {
if (Platform.isAndroid) {
if (FlutterBluePlus._connectTimestamp.containsKey(remoteId)) {
Duration minGap = Duration(milliseconds: androidDelay);
Duration elapsed = DateTime.now().difference(FlutterBluePlus._connectTimestamp[remoteId]!);
if (elapsed.compareTo(minGap) < 0) {
Duration timeLeft = minGap - elapsed;
print("[FBP] disconnect: enforcing ${minGap.inMilliseconds}ms disconnect gap, delaying "
"${timeLeft.inMilliseconds}ms");
await Future<void>.delayed(timeLeft);
}
}
}
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
Expand Down
1 change: 1 addition & 0 deletions lib/src/flutter_blue_plus.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class FlutterBluePlus {
static final Map<DeviceIdentifier, Map<String, List<int>>> _lastDescs = {};
static final Map<DeviceIdentifier, List<StreamSubscription>> _deviceSubscriptions = {};
static final Map<DeviceIdentifier, List<StreamSubscription>> _delayedSubscriptions = {};
static final Map<DeviceIdentifier, DateTime> _connectTimestamp = {};
static final List<StreamSubscription> _scanSubscriptions = [];
static final Set<DeviceIdentifier> _autoConnect = {};

Expand Down

0 comments on commit 4015129

Please sign in to comment.