Skip to content

Commit

Permalink
Merge branch 'jni-rewrite'
Browse files Browse the repository at this point in the history
  • Loading branch information
TheLastGimbus committed Mar 6, 2024
2 parents fe95d79 + ea3bfcc commit 3c74f2b
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 154 deletions.
141 changes: 86 additions & 55 deletions lib/headphones/cubit/headphones_connection_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:the_last_bluetooth/the_last_bluetooth.dart';

import '../../logger.dart';
import '../huawei/freebuds4i.dart';
import '../huawei/freebuds4i_impl.dart';
import '../huawei/freebuds4i_sim.dart';
import 'headphones_cubit_objects.dart';
import 'model_matching.dart';

class HeadphonesConnectionCubit extends Cubit<HeadphonesConnectionState> {
final TheLastBluetooth _bluetooth;
BluetoothConnection? _connection;
StreamSubscription? _btStream;
StreamChannel<Uint8List>? _connection;
StreamSubscription? _btEnabledStream;
StreamSubscription? _devStream;
bool _btEnabledCache = false;
final Map<BluetoothDevice, StreamSubscription> _watchedKnownDevices = {};
static const connectTries = 3;

// I needed a way to tell (from background task) if app is currently running.
Expand All @@ -44,79 +43,105 @@ class HeadphonesConnectionCubit extends Cubit<HeadphonesConnectionState> {

// This is so fucking embarrassing......
// Race conditions??? FUCK YES
static Future<bool> cubitAlreadyRunningSomewhere() async {
static Future<bool> cubitAlreadyRunningSomewhere(
{Duration timeout = const Duration(seconds: 1)}) async {
final ping = IsolateNameServer.lookupPortByName(
HeadphonesConnectionCubit.pingReceivePortName);
if (ping == null) return false;
final pong = ReceivePort(); // this is not right naming, i know
ping.send(pong.sendPort);
return await pong.first.timeout(
const Duration(milliseconds: 50),
onTimeout: () => false,
) as bool;
return await pong.first.timeout(timeout, onTimeout: () => false) as bool;
}

// todo: make this professional
static const sppUuid = "00001101-0000-1000-8000-00805f9b34fb";

Future<void> connect() async => _connect(await _bluetooth.pairedDevices);

// TODO/MIGRATION: This whole big-ass connection/detection loop 🤯
// for example, all placeholders assume we have 4i... not good
Future<void> _connect(List<BluetoothDevice> devices) async {
if (!await _bluetooth.isEnabled()) {
emit(const HeadphonesBluetoothDisabled());
return;
}
if (_connection != null) return; // already connected and working, skip
final otter = devices
.firstWhereOrNull((d) => HuaweiFreeBuds4i.idNameRegex.hasMatch(d.name));
if (otter == null) {
emit(const HeadphonesNotPaired());
return;
}
if (!otter.isConnected) {
// not connected to device at all
emit(const HeadphonesDisconnected(HuaweiFreeBuds4iSimPlaceholder()));
return;
Future<void> connect() async {
if (_connection != null) return;
final connected = _watchedKnownDevices.keys
.firstWhereOrNull((dev) => dev.isConnected.valueOrNull ?? false);
if (connected != null) {
_connect(connected, matchModel(connected)!);
}
emit(const HeadphonesConnecting(HuaweiFreeBuds4iSimPlaceholder()));
}

Future<void> _connect(BluetoothDevice dev, MatchedModel model) async {
final placeholder = model.placeholder;
emit(HeadphonesConnecting(placeholder));
try {
// when Ai Life takes over our socket, the connecting always succeeds at
// 2'nd try 🤔
for (var i = 0; i < connectTries; i++) {
try {
_connection = await _bluetooth.connectRfcomm(otter, sppUuid);
} on PlatformException catch (_) {
_connection = _bluetooth.connectRfcomm(dev, sppUuid);
break;
} catch (_) {
logg.w('Error when connecting socket: ${i + 1}/$connectTries tries');
if (i + 1 >= connectTries) rethrow;
}
}
emit(HeadphonesConnectedOpen(HuaweiFreeBuds4iImpl(_connection!.io)));
await _connection!.io.stream.listen((event) {}).asFuture();
emit(
HeadphonesConnectedOpen(model.builder(_connection!, dev)),
);
await _connection!.stream.listen((event) {}).asFuture();
// when device disconnects, future completes and we free the
// hopefully this happens *before* next stream event with data 🤷
// so that it nicely goes again and we emit HeadphonesDisconnected()
} on PlatformException catch (e, s) {
logg.e("PlatformError while connecting to socket",
error: e, stackTrace: s);
} catch (e, s) {
logg.e("Error while connecting to socket", error: e, stackTrace: s);
}
await _connection?.io.sink.close();
await _connection?.sink.close();
_connection = null;
// if disconnected because of bluetooth, don't emit
// this is because we made async gap when awaiting stream close
if (!_btEnabledCache) return;
if (!(_bluetooth.isEnabled.valueOrNull ?? false)) return;
emit(
((await _bluetooth.pairedDevices)
.firstWhereOrNull(
(d) => HuaweiFreeBuds4i.idNameRegex.hasMatch(d.name))
?.isConnected ??
false)
? const HeadphonesConnectedClosed(HuaweiFreeBuds4iSimPlaceholder())
: const HeadphonesDisconnected(HuaweiFreeBuds4iSimPlaceholder()),
(dev.isConnected.valueOrNull ?? false)
? HeadphonesConnectedClosed(placeholder)
: HeadphonesDisconnected(placeholder),
);
}

Future<void> _pairedDevicesHandle(Iterable<BluetoothDevice> devices) async {
if (!(_bluetooth.isEnabled.valueOrNull ?? false)) {
emit(const HeadphonesBluetoothDisabled());
return;
}

final knownHeadphones = devices
.map((dev) => (device: dev, match: matchModel(dev)))
.where((m) => m.match != null);

if (knownHeadphones.isEmpty) {
emit(const HeadphonesNotPaired());
return;
}

// "Add all devices that are in knownHp but not in _watched
for (final hp in knownHeadphones) {
if (!_watchedKnownDevices.containsKey(hp.device)) {
_watchedKnownDevices[hp.device] =
hp.device.isConnected.listen((connected) {
if (connected) {
if (_connection != null) return; // already connected, skip
_connect(hp.device, hp.match!);
} else {
_connection?.sink.close();
_connection = null;
emit(HeadphonesDisconnected(hp.match!.placeholder));
}
});
}
}
// "Remove any device from _watched that's not in knownHp"
for (final dev in _watchedKnownDevices.keys) {
if (!knownHeadphones.map((e) => e.device).contains(dev)) {
_watchedKnownDevices[dev]!.cancel();
_watchedKnownDevices.remove(dev);
}
}
}

HeadphonesConnectionCubit({required TheLastBluetooth bluetooth})
: _bluetooth = bluetooth,
super(const HeadphonesNotPaired()) {
Expand All @@ -131,17 +156,20 @@ class HeadphonesConnectionCubit extends Cubit<HeadphonesConnectionState> {
}

Future<void> _init() async {
// note: freezes the whole app if two cubits (jni plugins therefore) run
// at the same time
// TODO: Check if already running, in cases when we open *just* when bgn
// it's down here to be sure that we do have device connected so
if (!await Permission.bluetoothConnect.isGranted) {
emit(const HeadphonesNoPermission());
return;
}
_btStream = _bluetooth.adapterInfoStream.listen((event) {
_btEnabledCache = event.isEnabled;
if (!event.isEnabled) emit(const HeadphonesBluetoothDisabled());
_bluetooth.init();
_btEnabledStream = _bluetooth.isEnabled.listen((enabled) {
if (!enabled) emit(const HeadphonesBluetoothDisabled());
});
// logic of connect() is so universal we can use it on every change
_devStream = _bluetooth.pairedDevicesStream.listen(_connect);
_devStream = _bluetooth.pairedDevices.listen(_pairedDevicesHandle);
}

// TODO:
Expand All @@ -157,12 +185,15 @@ class HeadphonesConnectionCubit extends Cubit<HeadphonesConnectionState> {

@override
Future<void> close() async {
await _connection?.sink.close();
await _btEnabledStream?.cancel();
await _devStream?.cancel();
for (final sub in _watchedKnownDevices.values) {
await sub.cancel();
}
await _pingReceivePortSS.cancel();
_pingReceivePort.close();
IsolateNameServer.removePortNameMapping(pingReceivePortName);
await _connection?.io.sink.close();
await _btStream?.cancel();
await _devStream?.cancel();
super.close();
}
}
28 changes: 28 additions & 0 deletions lib/headphones/cubit/model_matching.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'dart:typed_data';

import 'package:stream_channel/stream_channel.dart';
import 'package:the_last_bluetooth/the_last_bluetooth.dart';

import '../framework/bluetooth_headphones.dart';
import '../huawei/freebuds4i.dart';
import '../huawei/freebuds4i_impl.dart';
import '../huawei/freebuds4i_sim.dart';

typedef HeadphonesBuilder = BluetoothHeadphones Function(
StreamChannel<Uint8List> io, BluetoothDevice device);

typedef MatchedModel = ({
HeadphonesBuilder builder,
BluetoothHeadphones placeholder
});

MatchedModel? matchModel(BluetoothDevice matchedDevice) {
final name = matchedDevice.name.value;
return switch (name) {
_ when HuaweiFreeBuds4i.idNameRegex.hasMatch(name) => (
builder: (io, dev) => HuaweiFreeBuds4iImpl(io, dev),
placeholder: const HuaweiFreeBuds4iSimPlaceholder(),
),
_ => null,
};
}
23 changes: 16 additions & 7 deletions lib/headphones/huawei/freebuds4i_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:typed_data';

import 'package:rxdart/rxdart.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:the_last_bluetooth/the_last_bluetooth.dart' as tlb;

import '../../logger.dart';
import '../framework/anc.dart';
Expand All @@ -13,6 +14,8 @@ import 'mbb.dart';
import 'settings.dart';

final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i {
final tlb.BluetoothDevice _bluetoothDevice;

/// Bluetooth serial port that we communicate over
final StreamChannel<Uint8List> _rfcomm;

Expand All @@ -29,7 +32,12 @@ final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i {
/// This watches if we are still missing any info and re-requests it
late StreamSubscription _watchdogStreamSub;

HuaweiFreeBuds4iImpl(this._rfcomm) {
HuaweiFreeBuds4iImpl(this._rfcomm, this._bluetoothDevice) {
// hope this will nicely play with closing, idk honestly
final aliasStreamSub = _bluetoothDevice.alias
.listen((alias) => _bluetoothAliasCtrl.add(alias));
_bluetoothAliasCtrl.onCancel = () => aliasStreamSub.cancel();

_rfcomm.stream.listen((event) {
List<MbbCommand>? commands;
try {
Expand Down Expand Up @@ -172,17 +180,18 @@ final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i {
.where((b) => b >= 0)
.shareValue();

// i could pass btDevice.alias directly here, but Headphones take care
// of closing everything
@override
// TODO: implement bluetoothAlias
ValueStream<String> get bluetoothAlias => BehaviorSubject();
ValueStream<String> get bluetoothAlias => _bluetoothAliasCtrl.stream;

// huh, my past self thought that names will not change... and my future
// (implementing TLB) thought otherwise 🤷🤷
@override
// TODO: implement bluetoothName
String get bluetoothName => '${super.vendor} ${super.name}';
String get bluetoothName => _bluetoothDevice.name.valueOrNull ?? "Unknown";

@override
// TODO: implement macAddress
String get macAddress => throw UnimplementedError();
String get macAddress => _bluetoothDevice.mac;

@override
ValueStream<LRCBatteryLevels> get lrcBattery => _lrcBatteryCtrl.stream;
Expand Down
31 changes: 7 additions & 24 deletions lib/headphones/huawei/freebuds4i_sim.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'package:rxdart/rxdart.dart';

import '../framework/anc.dart';
import '../framework/lrc_battery.dart';
import '../simulators/anc_sim.dart';
import '../simulators/bluetooth_headphones_sim.dart';
import '../simulators/lrc_battery_sim.dart';
Expand Down Expand Up @@ -49,33 +48,17 @@ final class HuaweiFreeBuds4iSim extends HuaweiFreeBuds4i
// all of this
//
// ...or not. I just don't know yet 🤷
final class HuaweiFreeBuds4iSimPlaceholder extends HuaweiFreeBuds4i {
final class HuaweiFreeBuds4iSimPlaceholder extends HuaweiFreeBuds4i
with
BluetoothHeadphonesSimPlaceholder,
LRCBatteryAlwaysFullSimPlaceholder,
AncSimPlaceholder {
const HuaweiFreeBuds4iSimPlaceholder();

@override
ValueStream<AncMode> get ancMode => BehaviorSubject();

@override
ValueStream<int> get batteryLevel => BehaviorSubject();

@override
ValueStream<String> get bluetoothAlias => BehaviorSubject();

@override
String get bluetoothName => '${super.vendor} ${super.name}';

@override
ValueStream<LRCBatteryLevels> get lrcBattery => BehaviorSubject();

@override
String get macAddress => '';

@override
Future<void> setAncMode(AncMode mode) async {}

@override
// TODO: implement settings
ValueStream<HuaweiFreeBuds4iSettings> get settings => BehaviorSubject();

@override
Future<void> setSettings(newSettings) async {}
Future<void> setSettings(HuaweiFreeBuds4iSettings newSettings) async {}
}
8 changes: 8 additions & 0 deletions lib/headphones/simulators/anc_sim.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ mixin AncSim implements Anc {
@override
Future<void> setAncMode(AncMode mode) async => _ancModeCtrl.add(mode);
}

mixin AncSimPlaceholder implements Anc {
@override
ValueStream<AncMode> get ancMode => BehaviorSubject();

@override
Future<void> setAncMode(AncMode mode) async {}
}
15 changes: 15 additions & 0 deletions lib/headphones/simulators/bluetooth_headphones_sim.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,18 @@ mixin BluetoothHeadphonesSim on HeadphonesModelInfo
@override
ValueStream<int> get batteryLevel => Stream.value(100).shareValue();
}

mixin BluetoothHeadphonesSimPlaceholder on HeadphonesModelInfo
implements BluetoothHeadphones {
@override
String get macAddress => "";

@override
String get bluetoothName => '$vendor $name';

@override
ValueStream<String> get bluetoothAlias => BehaviorSubject();

@override
ValueStream<int> get batteryLevel => BehaviorSubject();
}
5 changes: 5 additions & 0 deletions lib/headphones/simulators/lrc_battery_sim.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ mixin LRCBatteryAlwaysFullSim implements LRCBattery {
),
).shareValue();
}

mixin LRCBatteryAlwaysFullSimPlaceholder implements LRCBattery {
@override
ValueStream<LRCBatteryLevels> get lrcBattery => BehaviorSubject();
}
1 change: 1 addition & 0 deletions linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
Loading

0 comments on commit 3c74f2b

Please sign in to comment.