From f84e7f0ea0fa5fe0c4b44cb6b2c10888046dc273 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 8 Dec 2023 23:18:36 +0100 Subject: [PATCH 01/19] A NEW WORLD WILL BE REBORN --- lib/headphones/README.md | 38 +++++++++++++++++++ .../framework/bluetooth_headphones.dart | 27 +++++++++++++ lib/headphones/framework/headphones_info.dart | 30 +++++++++++++++ lib/headphones/huawei/freebuds4i.dart | 16 ++++++++ lib/headphones/huawei/freebuds4i_impl.dart | 22 +++++++++++ lib/headphones/huawei/freebuds4i_sim.dart | 5 +++ .../simulators/bluetooth_headphones_sim.dart | 23 +++++++++++ 7 files changed, 161 insertions(+) create mode 100644 lib/headphones/README.md create mode 100644 lib/headphones/framework/bluetooth_headphones.dart create mode 100644 lib/headphones/framework/headphones_info.dart create mode 100644 lib/headphones/huawei/freebuds4i.dart create mode 100644 lib/headphones/huawei/freebuds4i_impl.dart create mode 100644 lib/headphones/huawei/freebuds4i_sim.dart create mode 100644 lib/headphones/simulators/bluetooth_headphones_sim.dart diff --git a/lib/headphones/README.md b/lib/headphones/README.md new file mode 100644 index 0000000..e92f2ec --- /dev/null +++ b/lib/headphones/README.md @@ -0,0 +1,38 @@ +# `framework/` + +This folder contains **only** abstract classes with **no** logic - the framework for all implementations to base on + +They will essentially contain all methods and properties that other classes need to implement. + +Let's take `bluetooth_headphones.dart` as an example - it contains stuff that *all* bluetooth headphones (smart, or not smart) - *need* to implement: +- macAddress +- bluetoothName +- bluetoothAlias +- batteryLevel + +Now, some concrete `GenericBluetoothHeaphones` class could implement `batteryLevel` based on classic bluetooth standards, while some `SmartHeadphones` could share the average value of left and right bud. `SmartHeadphones` would probably implement a bunch of other classes, but still have to provide all generic properties of `BluetoothHeadphones`. + +# `simulators/` + +Here are `mixin`s that can help you to quickly bake a simulator of a device + +One headphones have ANC, others show battery for separate buds, others have both + +But all of them implement classes for these properties one-by-one. Luckily, `simulators/` will contain simulating mixins for all these properties πŸŽ‰ + +```dart +// DONE! +final class AirPodsMaxSim extends AirPodsMax with ANCSim {} + +// Also done! +final class CheapBudsSim extends CheapBuds with LRCBatterySim {} + +// Even more done!!! +final class PoshBudsSim extends PoshBuds with ANCSim, LRCBatterySim{} +``` + +This is still kiiiiiinda boilerplate, but there may be properties that cannot be easily simulated with one-fits-all simulator, or you may want to do that manually + +One could still say that `ANC` and `LRCBattery` classes could be simulations themselves, and just re-implement them in actual headphones' implementations - we would then avoid re-writing all of that `with`s - however, I just think it would be confusing, and could lead to weird bugs by someone forgetting to re-implement them. + +And, this also allows us to make and mix multiple `Sim`s - for example, there would be just one fake `ANC`, while having `AlwaysFull`, `DischargingSlowly` and `ChargingFast` for the batteries πŸ‘€ \ No newline at end of file diff --git a/lib/headphones/framework/bluetooth_headphones.dart b/lib/headphones/framework/bluetooth_headphones.dart new file mode 100644 index 0000000..55ec7eb --- /dev/null +++ b/lib/headphones/framework/bluetooth_headphones.dart @@ -0,0 +1,27 @@ +import 'package:rxdart/rxdart.dart'; + +/// A base class for *all* different bluetooth headphones. Whether it be +/// a generic old 10$ headphones, or super smart AirPods with 100 different +/// ANC modes - all of them need to implement at least this +/// +/// Watch out for documentation of each property πŸ‘€ +abstract class BluetoothHeadphones { + /// Very normal mac address, in hex and upper-case + String get macAddress; + + /// Very normal bluetooth name + String get bluetoothName; + + /// Alias that user can set in their OS + /// + /// If they can't, or didn't do so, then... idk honestly whether to emit a + /// null or never emit or emit same as [bluetoothName]... + /// + /// You know what - do same as OS. If OS sends you same as their name - emit + /// this. If OS doesn't give you anything - just don't. + ValueStream get bluetoothAlias; + + /// Generic battery level, probably got from OS - watch out that this may + /// never emit cause **some** OSes/headphones still don't support it + ValueStream get batteryLevel; +} diff --git a/lib/headphones/framework/headphones_info.dart b/lib/headphones/framework/headphones_info.dart new file mode 100644 index 0000000..0a88a7a --- /dev/null +++ b/lib/headphones/framework/headphones_info.dart @@ -0,0 +1,30 @@ +/// Stuff that we need to know about concrete headphones model +/// +/// For example, FreeBuds 4i would implement it like: +/// +/// ```dart +/// class HuaweiFreeBuds4i implements HeadphonesModelInfo { +/// @override +/// String get vendor => "Huawei"; +/// +/// @override +/// String get name => "FreeBuds 4i"; +/// } +/// ``` +abstract class HeadphonesModelInfo { + /// Name of the vendor - simple and concrete + /// + /// - Huawei - not HUAWEI nor huawei + /// - Xiaomi - not redmi or some other bs + /// - Samsung + /// - etc + /// + /// Don't worry, we will not be basing any name-matching on that or anything, + /// this is somewhat for debugging purposes etc + String get vendor; + + /// Name of headphones, without the vendor. As close to what they are named + /// like on the market as possible (sometimes it's FREEBUDS, sometimes + /// Freebuds - but most often FreeBuds - stick to that πŸ‘) + String get name; +} diff --git a/lib/headphones/huawei/freebuds4i.dart b/lib/headphones/huawei/freebuds4i.dart new file mode 100644 index 0000000..6533880 --- /dev/null +++ b/lib/headphones/huawei/freebuds4i.dart @@ -0,0 +1,16 @@ +import '../framework/bluetooth_headphones.dart'; +import '../framework/headphones_info.dart'; + +/// Base abstract class of 4i's. It contains static info like vendor names etc, +/// but no logic whatsoever. +/// +/// It makes both a solid ground for actual implementation (by defining what +/// features they implement), and some basic info for easy simulation +abstract base class HuaweiFreeBuds4i + implements BluetoothHeadphones, HeadphonesModelInfo { + @override + String get vendor => "Huawei"; + + @override + String get name => "FreeBuds 4i"; +} diff --git a/lib/headphones/huawei/freebuds4i_impl.dart b/lib/headphones/huawei/freebuds4i_impl.dart new file mode 100644 index 0000000..e9b09fa --- /dev/null +++ b/lib/headphones/huawei/freebuds4i_impl.dart @@ -0,0 +1,22 @@ +// ignore: implementation_imports +import 'package:rxdart/src/streams/value_stream.dart'; + +import 'freebuds4i.dart'; + +final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { + @override + // TODO: implement batteryLevel + ValueStream get batteryLevel => throw UnimplementedError(); + + @override + // TODO: implement bluetoothAlias + ValueStream get bluetoothAlias => throw UnimplementedError(); + + @override + // TODO: implement bluetoothName + String get bluetoothName => throw UnimplementedError(); + + @override + // TODO: implement macAddress + String get macAddress => throw UnimplementedError(); +} diff --git a/lib/headphones/huawei/freebuds4i_sim.dart b/lib/headphones/huawei/freebuds4i_sim.dart new file mode 100644 index 0000000..44028b7 --- /dev/null +++ b/lib/headphones/huawei/freebuds4i_sim.dart @@ -0,0 +1,5 @@ +import '../simulators/bluetooth_headphones_sim.dart'; +import 'freebuds4i.dart'; + +final class HuaweiFreeBuds4iSim extends HuaweiFreeBuds4i + with BluetoothHeadphonesSim {} diff --git a/lib/headphones/simulators/bluetooth_headphones_sim.dart b/lib/headphones/simulators/bluetooth_headphones_sim.dart new file mode 100644 index 0000000..b921911 --- /dev/null +++ b/lib/headphones/simulators/bluetooth_headphones_sim.dart @@ -0,0 +1,23 @@ +import 'package:rxdart/rxdart.dart'; + +import '../framework/bluetooth_headphones.dart'; +import '../framework/headphones_info.dart'; + +/// A handy mixin that can emulate BluetoothHeadphones properties +/// if a mixed class implements HeadphonesModelInfo +// It is very simple for now - in future, we may simulate the battery dropping etc +mixin BluetoothHeadphonesSim on HeadphonesModelInfo + implements BluetoothHeadphones { + // TODO: Make this random so that it won't mix up in far future + @override + String get macAddress => "AA:BB:CC:DD:EE:FF"; + + @override + String get bluetoothName => vendor + name; + + @override + ValueStream get bluetoothAlias => Stream.value(name).shareValue(); + + @override + ValueStream get batteryLevel => Stream.value(100).shareValue(); +} From 88dec2d5cc00a67317298e4399f4ac973c4540c0 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Sun, 11 Feb 2024 22:56:10 +0100 Subject: [PATCH 02/19] some more info --- lib/headphones/README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/headphones/README.md b/lib/headphones/README.md index e92f2ec..cd77b37 100644 --- a/lib/headphones/README.md +++ b/lib/headphones/README.md @@ -4,7 +4,7 @@ This folder contains **only** abstract classes with **no** logic - the framework They will essentially contain all methods and properties that other classes need to implement. -Let's take `bluetooth_headphones.dart` as an example - it contains stuff that *all* bluetooth headphones (smart, or not smart) - *need* to implement: +Let's take `bluetooth_headphones.dart` as an example - it contains stuff that *all* bluetooth headphones (smart, or not smart) - share in common and thus *need* to implement: - macAddress - bluetoothName - bluetoothAlias @@ -12,13 +12,23 @@ Let's take `bluetooth_headphones.dart` as an example - it contains stuff that *a Now, some concrete `GenericBluetoothHeaphones` class could implement `batteryLevel` based on classic bluetooth standards, while some `SmartHeadphones` could share the average value of left and right bud. `SmartHeadphones` would probably implement a bunch of other classes, but still have to provide all generic properties of `BluetoothHeadphones`. +## `headphones_info.dart` + +At some point, the freebuddy's code will contain some information about select headphones models - I want to avoid this as much as possible, since: +1. This stuff is language-specific, and implementing whole damn localisation for this is a no-no +2. Vendors often can't decide what is the freaking name/brand of their product (it is Xiaomi?? or Redmi? or Mi?? or both!) +3. Many more surprizes like different color of buds available on different markets etc +4. I want freebuddy to be very easy to extend - manually filling their names instead of getting them from bluetooth device name is another boilerplate step + +So, I want to be careful with this, but for now adding their model and vendor name doesn't seem that bad πŸ‘€ + # `simulators/` -Here are `mixin`s that can help you to quickly bake a simulator of a device +`simulator` is my name for a mock - they are here to help you test the app without connecting the headphones, or even without Android/Bluetooth itself -One headphones have ANC, others show battery for separate buds, others have both +The folder contains `mixin`s that can help you to quickly bake a simulator of a device. The simulators themselves are actually located besides actual implementations of given headphones, not here -But all of them implement classes for these properties one-by-one. Luckily, `simulators/` will contain simulating mixins for all these properties πŸŽ‰ +One headphones have ANC, others show battery for separate buds, others have both - but all of them implement classes for these properties one-by-one. Luckily, `simulators/` will contain simulating mixins for all these properties πŸŽ‰ ```dart // DONE! @@ -33,6 +43,6 @@ final class PoshBudsSim extends PoshBuds with ANCSim, LRCBatterySim{} This is still kiiiiiinda boilerplate, but there may be properties that cannot be easily simulated with one-fits-all simulator, or you may want to do that manually -One could still say that `ANC` and `LRCBattery` classes could be simulations themselves, and just re-implement them in actual headphones' implementations - we would then avoid re-writing all of that `with`s - however, I just think it would be confusing, and could lead to weird bugs by someone forgetting to re-implement them. +> One could still say that original/base `ANC` and `LRCBattery` classes could be simulations themselves, and just re-implement them in actual headphones' implementations - we would then avoid re-writing all of that `with`s - however, I just think it would be confusing, and could lead to weird bugs by someone forgetting to re-implement them. And, this also allows us to make and mix multiple `Sim`s - for example, there would be just one fake `ANC`, while having `AlwaysFull`, `DischargingSlowly` and `ChargingFast` for the batteries πŸ‘€ \ No newline at end of file From 7b7bede26054c158c3f0898bf90b4613755c53db Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Mon, 12 Feb 2024 12:06:30 +0100 Subject: [PATCH 03/19] add anc and lrc battery stuff to the framework --- lib/headphones/framework/anc.dart | 13 ++++++ lib/headphones/framework/lrc_battery.dart | 24 ++++++++++ lib/headphones/huawei/freebuds4i.dart | 4 +- lib/headphones/huawei/freebuds4i_impl.dart | 45 ++++++++++++++++++- lib/headphones/huawei/freebuds4i_sim.dart | 4 +- lib/headphones/simulators/anc_sim.dart | 14 ++++++ .../simulators/lrc_battery_sim.dart | 23 ++++++++++ 7 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 lib/headphones/framework/anc.dart create mode 100644 lib/headphones/framework/lrc_battery.dart create mode 100644 lib/headphones/simulators/anc_sim.dart create mode 100644 lib/headphones/simulators/lrc_battery_sim.dart diff --git a/lib/headphones/framework/anc.dart b/lib/headphones/framework/anc.dart new file mode 100644 index 0000000..0add85a --- /dev/null +++ b/lib/headphones/framework/anc.dart @@ -0,0 +1,13 @@ +import 'package:rxdart/rxdart.dart'; + +abstract class Anc { + ValueStream get ancMode; + + Future setAncMode(AncMode mode); +} + +enum AncMode { + noiseCancelling, + off, + transparency, +} diff --git a/lib/headphones/framework/lrc_battery.dart b/lib/headphones/framework/lrc_battery.dart new file mode 100644 index 0000000..50aa9bb --- /dev/null +++ b/lib/headphones/framework/lrc_battery.dart @@ -0,0 +1,24 @@ +// left right case battery +import 'package:rxdart/rxdart.dart'; + +abstract class LRCBattery { + ValueStream get lrcBattery; +} + +class LRCBatteryLevels { + final int? levelLeft; + final int? levelRight; + final int? levelCase; + final bool chargingLeft; + final bool chargingRight; + final bool chargingCase; + + const LRCBatteryLevels( + this.levelLeft, + this.levelRight, + this.levelCase, + this.chargingLeft, + this.chargingRight, + this.chargingCase, + ); +} diff --git a/lib/headphones/huawei/freebuds4i.dart b/lib/headphones/huawei/freebuds4i.dart index 6533880..f920499 100644 --- a/lib/headphones/huawei/freebuds4i.dart +++ b/lib/headphones/huawei/freebuds4i.dart @@ -1,5 +1,7 @@ +import '../framework/anc.dart'; import '../framework/bluetooth_headphones.dart'; import '../framework/headphones_info.dart'; +import '../framework/lrc_battery.dart'; /// Base abstract class of 4i's. It contains static info like vendor names etc, /// but no logic whatsoever. @@ -7,7 +9,7 @@ import '../framework/headphones_info.dart'; /// It makes both a solid ground for actual implementation (by defining what /// features they implement), and some basic info for easy simulation abstract base class HuaweiFreeBuds4i - implements BluetoothHeadphones, HeadphonesModelInfo { + implements BluetoothHeadphones, HeadphonesModelInfo, LRCBattery, Anc { @override String get vendor => "Huawei"; diff --git a/lib/headphones/huawei/freebuds4i_impl.dart b/lib/headphones/huawei/freebuds4i_impl.dart index e9b09fa..47d1112 100644 --- a/lib/headphones/huawei/freebuds4i_impl.dart +++ b/lib/headphones/huawei/freebuds4i_impl.dart @@ -1,9 +1,36 @@ -// ignore: implementation_imports -import 'package:rxdart/src/streams/value_stream.dart'; +import 'dart:typed_data'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_channel/stream_channel.dart'; + +import '../framework/anc.dart'; +import '../framework/lrc_battery.dart'; import 'freebuds4i.dart'; final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { + /// Bluetooth serial port that we communicate over + final StreamChannel _rfcomm; + + // * stream controllers + final _batteryLevelCtrl = BehaviorSubject(); + final _bluetoothAliasCtrl = BehaviorSubject(); + final _bluetoothNameCtrl = BehaviorSubject(); + final _lrcBatteryCtrl = BehaviorSubject(); + final _ancModeCtrl = BehaviorSubject(); + + // stream controllers * + + HuaweiFreeBuds4iImpl(this._rfcomm) { + _rfcomm.stream.listen((event) {}, onDone: () { + // close all streams + _batteryLevelCtrl.close(); + _bluetoothAliasCtrl.close(); + _bluetoothNameCtrl.close(); + _lrcBatteryCtrl.close(); + _ancModeCtrl.close(); + }); + } + @override // TODO: implement batteryLevel ValueStream get batteryLevel => throw UnimplementedError(); @@ -19,4 +46,18 @@ final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { @override // TODO: implement macAddress String get macAddress => throw UnimplementedError(); + + @override + // TODO: implement batteryData + ValueStream get lrcBattery => throw UnimplementedError(); + + @override + // TODO: implement ancMode + ValueStream get ancMode => throw UnimplementedError(); + + @override + Future setAncMode(AncMode mode) { + // TODO: implement setAncMode + throw UnimplementedError(); + } } diff --git a/lib/headphones/huawei/freebuds4i_sim.dart b/lib/headphones/huawei/freebuds4i_sim.dart index 44028b7..67e7cdc 100644 --- a/lib/headphones/huawei/freebuds4i_sim.dart +++ b/lib/headphones/huawei/freebuds4i_sim.dart @@ -1,5 +1,7 @@ +import '../simulators/anc_sim.dart'; import '../simulators/bluetooth_headphones_sim.dart'; +import '../simulators/lrc_battery_sim.dart'; import 'freebuds4i.dart'; final class HuaweiFreeBuds4iSim extends HuaweiFreeBuds4i - with BluetoothHeadphonesSim {} + with BluetoothHeadphonesSim, LRCBatteryAlwaysFullSim, AncSim {} diff --git a/lib/headphones/simulators/anc_sim.dart b/lib/headphones/simulators/anc_sim.dart new file mode 100644 index 0000000..f82f777 --- /dev/null +++ b/lib/headphones/simulators/anc_sim.dart @@ -0,0 +1,14 @@ +import 'package:rxdart/rxdart.dart'; + +import '../framework/anc.dart'; + +/// Simulates anc actually being switched on the headphones +mixin AncSim implements Anc { + final _ancModeCtrl = BehaviorSubject(); + + @override + ValueStream get ancMode => _ancModeCtrl; + + @override + Future setAncMode(AncMode mode) async => _ancModeCtrl.add(mode); +} diff --git a/lib/headphones/simulators/lrc_battery_sim.dart b/lib/headphones/simulators/lrc_battery_sim.dart new file mode 100644 index 0000000..c65ca7e --- /dev/null +++ b/lib/headphones/simulators/lrc_battery_sim.dart @@ -0,0 +1,23 @@ +/// Here are all different mixins for simulating the battery +/// +/// This case particularly shows how *genius* I am with all of those mixins 😌 +/// You can easily emulate battery always full, or discharging slowly, etc + +import 'package:rxdart/rxdart.dart'; + +import '../framework/lrc_battery.dart'; + +/// This always shows battery as 100% full and not charging +mixin LRCBatteryAlwaysFullSim implements LRCBattery { + @override + ValueStream get lrcBattery => Stream.value( + const LRCBatteryLevels( + 100, + 100, + 100, + false, + false, + false, + ), + ).shareValue(); +} From 79ea49f060fd237114938ee69cd73cbad76bc3f4 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Thu, 22 Feb 2024 10:56:45 +0100 Subject: [PATCH 04/19] migrate stuff to *at least* compile and work with mock --- lib/README.md | 15 +++ .../cubit/headphones_connection_cubit.dart | 5 +- .../cubit/headphones_cubit_objects.dart | 4 +- .../cubit/headphones_mock_cubit.dart | 4 +- .../android/appwidgets/battery_appwidget.dart | 19 +-- .../android/background/periodic.dart | 12 +- ...eadphones_connection_ensuring_overlay.dart | 11 +- .../headphones_settings_page.dart | 17 ++- lib/ui/pages/home/controls/anc_card.dart | 25 ++-- lib/ui/pages/home/controls/battery_card.dart | 8 +- .../controls/headphones_controls_widget.dart | 113 +++++++++++------- .../pages/home/controls/headphones_image.dart | 6 +- 12 files changed, 140 insertions(+), 99 deletions(-) create mode 100644 lib/README.md diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..c7d8561 --- /dev/null +++ b/lib/README.md @@ -0,0 +1,15 @@ +# Development notes - philosophies, TODOs etc + +## Migration to framework-ish organisation +TODO: +- [X] Implement basic stuff (ANC + battery) +- [X] Change ui to work at all with new stuff +- [ ] Implement hp settings + This is non trivial cause we have to decide how to make this universal when almost all headphones have this different +- [ ] Change ui BIG to dynamically support *all* headphones by their features instead of concrete model +- [ ] Support multiple diff headphones + - [ ] Basic fix in connection loops to show them at all + - [ ] Basic fix in widget to maybe show their name to distinguish which ones are shown now + - [ ] Some database stuf... ehhhh... to distinguish between them, remember their last time etc + This is potentially waaayyy ahead todo as it requires *serious* decisions that will affect *everything* wayyy ahead in later development, so better not fuck it up + - [ ] Selecting headphones when making new widget diff --git a/lib/headphones/cubit/headphones_connection_cubit.dart b/lib/headphones/cubit/headphones_connection_cubit.dart index 34aaf7d..9d1be80 100644 --- a/lib/headphones/cubit/headphones_connection_cubit.dart +++ b/lib/headphones/cubit/headphones_connection_cubit.dart @@ -10,7 +10,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:the_last_bluetooth/the_last_bluetooth.dart'; import '../../logger.dart'; -import '../huawei/otter/headphones_impl_otter.dart'; +import '../huawei/freebuds4i_impl.dart'; import '../huawei/otter/otter_constants.dart'; import 'headphones_cubit_objects.dart'; @@ -89,8 +89,7 @@ class HeadphonesConnectionCubit extends Cubit { if (i + 1 >= connectTries) rethrow; } } - emit(HeadphonesConnectedOpen( - HeadphonesImplOtter(_connection!.io, otter.alias))); + emit(HeadphonesConnectedOpen(HuaweiFreeBuds4iImpl(_connection!.io))); await _connection!.io.stream.listen((event) {}).asFuture(); // when device disconnects, future completes and we free the // hopefully this happens *before* next stream event with data 🀷 diff --git a/lib/headphones/cubit/headphones_cubit_objects.dart b/lib/headphones/cubit/headphones_cubit_objects.dart index 4f4a022..52fe9a7 100644 --- a/lib/headphones/cubit/headphones_cubit_objects.dart +++ b/lib/headphones/cubit/headphones_cubit_objects.dart @@ -1,4 +1,4 @@ -import '../headphones_base.dart'; +import '../framework/bluetooth_headphones.dart'; abstract class HeadphonesConnectionState {} @@ -13,7 +13,7 @@ class HeadphonesDisconnected extends HeadphonesConnectionState {} class HeadphonesConnecting extends HeadphonesConnectionState {} class HeadphonesConnectedOpen extends HeadphonesConnectionState { - final HeadphonesBase headphones; + final BluetoothHeadphones headphones; HeadphonesConnectedOpen(this.headphones); } diff --git a/lib/headphones/cubit/headphones_mock_cubit.dart b/lib/headphones/cubit/headphones_mock_cubit.dart index 0b5dae9..f421698 100644 --- a/lib/headphones/cubit/headphones_mock_cubit.dart +++ b/lib/headphones/cubit/headphones_mock_cubit.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../headphones_mocks.dart'; +import '../huawei/freebuds4i_sim.dart'; import 'headphones_connection_cubit.dart'; import 'headphones_cubit_objects.dart'; @@ -12,7 +12,7 @@ class HeadphonesMockCubit extends Cubit // i do this because otherwise initial data isn't even emitted and // [BlocListener]s don't work >:( Future.microtask( - () => emit(HeadphonesConnectedOpen(HeadphonesMockPrettyFake()))); + () => emit(HeadphonesConnectedOpen(HuaweiFreeBuds4iSim()))); } @override diff --git a/lib/platform_stuff/android/appwidgets/battery_appwidget.dart b/lib/platform_stuff/android/appwidgets/battery_appwidget.dart index 9a169d7..b4d1360 100644 --- a/lib/platform_stuff/android/appwidgets/battery_appwidget.dart +++ b/lib/platform_stuff/android/appwidgets/battery_appwidget.dart @@ -5,23 +5,24 @@ import 'package:home_widget/home_widget.dart'; import 'package:rxdart/rxdart.dart'; import '../../../headphones/cubit/headphones_cubit_objects.dart'; -import '../../../headphones/headphones_data_objects.dart'; +import '../../../headphones/framework/lrc_battery.dart'; import '../../../logger.dart'; // no better idea for this yet - that's fine -StreamSubscription? _headphonesBatteryStreamSub; +StreamSubscription? _headphonesBatteryStreamSub; void batteryHomeWidgetHearBloc(BuildContext context, HeadphonesConnectionState headphonesConnectionState) async { if (headphonesConnectionState is! HeadphonesConnectedOpen) { await _headphonesBatteryStreamSub?.cancel(); _headphonesBatteryStreamSub = null; - } else { - _headphonesBatteryStreamSub = headphonesConnectionState - .headphones.batteryData - .throttleTime(const Duration(seconds: 1), - trailing: true, leading: false) - .listen((event) async { + } else if (headphonesConnectionState.headphones is LRCBattery) { + _headphonesBatteryStreamSub = + (headphonesConnectionState.headphones as LRCBattery) + .lrcBattery + .throttleTime(const Duration(seconds: 1), + trailing: true, leading: false) + .listen((event) async { logg.d("Updating widget from UI listener: $event"); await updateBatteryHomeWidget(event); }); @@ -29,7 +30,7 @@ void batteryHomeWidgetHearBloc(BuildContext context, } // this is separate so we can use it from f.e. background stuff -Future updateBatteryHomeWidget(HeadphonesBatteryData batteryData) async { +Future updateBatteryHomeWidget(LRCBatteryLevels batteryData) async { await HomeWidget.saveWidgetData('left', batteryData.levelLeft); await HomeWidget.saveWidgetData('right', batteryData.levelRight); await HomeWidget.saveWidgetData('case', batteryData.levelCase); diff --git a/lib/platform_stuff/android/background/periodic.dart b/lib/platform_stuff/android/background/periodic.dart index 36c3858..1b4d9b5 100644 --- a/lib/platform_stuff/android/background/periodic.dart +++ b/lib/platform_stuff/android/background/periodic.dart @@ -8,6 +8,7 @@ import 'package:workmanager/workmanager.dart'; import '../../../di.dart' as di; import '../../../headphones/cubit/headphones_connection_cubit.dart'; import '../../../headphones/cubit/headphones_cubit_objects.dart'; +import '../../../headphones/framework/lrc_battery.dart'; import '../../../logger.dart'; import '../appwidgets/battery_appwidget.dart'; @@ -41,8 +42,15 @@ Future routineUpdateCallback() async { "${headphones.toString()}"); return true; } - final batteryData = - await headphones.headphones.batteryData.first.timeout(commonTimeout); + if (headphones.headphones is! LRCBattery) { + logg.d("Not updating stuff from ROUTINE_UPDATE because connected " + "headphones don't support LRCBattery"); + return true; + } + final batteryData = await (headphones.headphones as LRCBattery) + .lrcBattery + .first + .timeout(commonTimeout); logg.d("udpating widget from bgn: $batteryData"); await updateBatteryHomeWidget(batteryData); await cubit.close(); // remember to close cubit to deregister port name diff --git a/lib/ui/common/headphones_connection_ensuring_overlay.dart b/lib/ui/common/headphones_connection_ensuring_overlay.dart index 08198ab..2518376 100644 --- a/lib/ui/common/headphones_connection_ensuring_overlay.dart +++ b/lib/ui/common/headphones_connection_ensuring_overlay.dart @@ -4,8 +4,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../headphones/cubit/headphones_connection_cubit.dart'; import '../../headphones/cubit/headphones_cubit_objects.dart'; -import '../../headphones/headphones_base.dart'; +import '../../headphones/framework/bluetooth_headphones.dart'; import '../../headphones/headphones_mocks.dart'; +import '../../headphones/huawei/freebuds4i_sim.dart'; import '../pages/disabled.dart'; import '../pages/home/bluetooth_disabled_info_widget.dart'; import '../pages/home/connected_closed_widget.dart'; @@ -28,7 +29,7 @@ import '../pages/home/not_paired_info_widget.dart'; class HeadphonesConnectionEnsuringOverlay extends StatelessWidget { /// Build your widget of desire here - note that headphones may be Mock /// (as always πŸ™„) - final Widget Function(BuildContext context, HeadphonesBase headphones) + final Widget Function(BuildContext context, BluetoothHeadphones headphones) builder; const HeadphonesConnectionEnsuringOverlay({super.key, required this.builder}); @@ -74,7 +75,11 @@ class HeadphonesConnectionEnsuringOverlay extends StatelessWidget { context, state is HeadphonesConnectedOpen ? state.headphones - : HeadphonesMockNever(), + // TODO MIGRATION: Think about this mock - should it be + // headphone-specific or generic or what. Should mocks be + // as easily detected as concrete headphones, to properly mock + // display mock in grayed-out ui when not connected? + : HuaweiFreeBuds4iSim(), ), ), _ => Text(l.pageHomeUnknown), diff --git a/lib/ui/pages/headphones_settings/headphones_settings_page.dart b/lib/ui/pages/headphones_settings/headphones_settings_page.dart index 54d61d0..e0de77e 100644 --- a/lib/ui/pages/headphones_settings/headphones_settings_page.dart +++ b/lib/ui/pages/headphones_settings/headphones_settings_page.dart @@ -2,9 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../common/headphones_connection_ensuring_overlay.dart'; -import 'auto_pause_section.dart'; -import 'double_tap_section.dart'; -import 'hold_section.dart'; class HeadphonesSettingsPage extends StatelessWidget { const HeadphonesSettingsPage({super.key}); @@ -19,12 +16,14 @@ class HeadphonesSettingsPage extends StatelessWidget { builder: (_, h) { return ListView( children: [ - AutoPauseSection(headphones: h), - const Divider(indent: 16, endIndent: 16), - DoubleTapSection(headphones: h), - const Divider(indent: 16, endIndent: 16), - HoldSection(headphones: h), - const SizedBox(height: 64), + // TODO MIGRATION: hp settings not yet implemented + const Text('HP Settings not yet implemented'), + // AutoPauseSection(headphones: h), + // const Divider(indent: 16, endIndent: 16), + // DoubleTapSection(headphones: h), + // const Divider(indent: 16, endIndent: 16), + // HoldSection(headphones: h), + // const SizedBox(height: 64), ], ); }, diff --git a/lib/ui/pages/home/controls/anc_card.dart b/lib/ui/pages/home/controls/anc_card.dart index 243084d..b2a7d37 100644 --- a/lib/ui/pages/home/controls/anc_card.dart +++ b/lib/ui/pages/home/controls/anc_card.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; -import '../../../../headphones/headphones_base.dart'; -import '../../../../headphones/headphones_data_objects.dart'; +import '../../../../headphones/framework/anc.dart'; import '../../../common/constrained_spacer.dart'; /// Card with anc controls class AncCard extends StatelessWidget { - final HeadphonesBase headphones; + final Anc anc; - const AncCard(this.headphones, {super.key}); + const AncCard(this.anc, {super.key}); @override Widget build(BuildContext context) { - return StreamBuilder( - stream: headphones.ancMode, + return StreamBuilder( + stream: anc.ancMode, builder: (context, snapshot) { final mode = snapshot.data; return Card( @@ -27,24 +26,22 @@ class AncCard extends StatelessWidget { constraints: BoxConstraints(maxWidth: 32)), _AncButton( icon: Symbols.noise_control_on, - isSelected: mode == HeadphonesAncMode.noiseCancel, - onPressed: () => - headphones.setAncMode(HeadphonesAncMode.noiseCancel), + isSelected: mode == AncMode.noiseCancelling, + onPressed: () => anc.setAncMode(AncMode.noiseCancelling), ), const ConstrainedSpacer( constraints: BoxConstraints(maxWidth: 32)), _AncButton( icon: Symbols.noise_control_off, - isSelected: mode == HeadphonesAncMode.off, - onPressed: () => headphones.setAncMode(HeadphonesAncMode.off), + isSelected: mode == AncMode.off, + onPressed: () => anc.setAncMode(AncMode.off), ), const ConstrainedSpacer( constraints: BoxConstraints(maxWidth: 32)), _AncButton( icon: Symbols.noise_aware, - isSelected: mode == HeadphonesAncMode.awareness, - onPressed: () => - headphones.setAncMode(HeadphonesAncMode.awareness), + isSelected: mode == AncMode.transparency, + onPressed: () => anc.setAncMode(AncMode.transparency), ), const ConstrainedSpacer( constraints: BoxConstraints(maxWidth: 32)), diff --git a/lib/ui/pages/home/controls/battery_card.dart b/lib/ui/pages/home/controls/battery_card.dart index 58bd414..5e9c2fb 100644 --- a/lib/ui/pages/home/controls/battery_card.dart +++ b/lib/ui/pages/home/controls/battery_card.dart @@ -4,16 +4,16 @@ import 'package:material_color_utilities/material_color_utilities.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../../gen/freebuddy_icons.dart'; -import '../../../../headphones/headphones_base.dart'; +import '../../../../headphones/framework/lrc_battery.dart'; /// Android12-Google-Battery-Widget-style battery card /// /// https://9to5google.com/2022/03/07/google-pixel-battery-widget/ /// https://9to5google.com/2022/09/29/pixel-battery-widget-time/ class BatteryCard extends StatelessWidget { - final HeadphonesBase headphones; + final LRCBattery lrcBattery; - const BatteryCard(this.headphones, {super.key}); + const BatteryCard(this.lrcBattery, {super.key}); @override Widget build(BuildContext context) { @@ -55,7 +55,7 @@ class BatteryCard extends StatelessWidget { ), ); return StreamBuilder( - stream: headphones.batteryData, + stream: lrcBattery.lrcBattery, builder: (context, snapshot) { final b = snapshot.data; return Card( diff --git a/lib/ui/pages/home/controls/headphones_controls_widget.dart b/lib/ui/pages/home/controls/headphones_controls_widget.dart index f82a649..29bb1a3 100644 --- a/lib/ui/pages/home/controls/headphones_controls_widget.dart +++ b/lib/ui/pages/home/controls/headphones_controls_widget.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../../../../headphones/framework/anc.dart'; +import '../../../../headphones/framework/bluetooth_headphones.dart'; +import '../../../../headphones/framework/lrc_battery.dart'; import '../../../../headphones/headphones_base.dart'; +import '../../../../headphones/huawei/freebuds4i.dart'; import '../../../theme/layouts.dart'; import 'anc_card.dart'; import 'battery_card.dart'; @@ -17,7 +21,7 @@ import 'headphones_image.dart'; /// you can give it [HeadphonesMockNever] object, and previous values will stay /// because it won't override them class HeadphonesControlsWidget extends StatelessWidget { - final HeadphonesBase headphones; + final BluetoothHeadphones headphones; const HeadphonesControlsWidget({super.key, required this.headphones}); @@ -25,58 +29,75 @@ class HeadphonesControlsWidget extends StatelessWidget { Widget build(BuildContext context) { final t = Theme.of(context); final tt = t.textTheme; - return Padding( - padding: const EdgeInsets.all(12.0), - child: WindowSizeClass.of(context) == WindowSizeClass.compact - ? Column( - children: [ - Text( - // TODO: This hardcode - headphones.alias ?? 'FreeBuds 4i', - style: tt.headlineMedium, - ), - HeadphonesImage(headphones), - Align( - alignment: Alignment.centerRight, - child: _HeadphonesSettingsButton(headphones), - ), - BatteryCard(headphones), - AncCard(headphones), - ], - ) - : Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Column( - children: [ - Text( - // TODO: This hardcode - headphones.alias ?? 'FreeBuds 4i', - style: tt.headlineMedium, - ), - HeadphonesImage(headphones), - ], + // TODO MIGRATION: Whole big ass branching here in detecting whether hp + // support different features + if (headphones is HuaweiFreeBuds4i) { + return Padding( + padding: const EdgeInsets.all(12.0), + child: WindowSizeClass.of(context) == WindowSizeClass.compact + ? Column( + children: [ + StreamBuilder( + stream: headphones.bluetoothAlias, + builder: (_, snap) => Text( + snap.data ?? headphones.bluetoothName, + style: tt.headlineMedium, + ), ), - ), - Expanded( - child: SingleChildScrollView( + // TODO MIGRATION: Some way to assign images to models + // should base class contain path to img or should there be + // fancy function that does some logic? Such funtion would be + // nice to detect colors (this was requested by some users) + const HeadphonesImage(), + // TODO MIGRATION: hp settings not yet implemented + // Align( + // alignment: Alignment.centerRight, + // child: _HeadphonesSettingsButton(headphones), + // ), + BatteryCard(headphones as LRCBattery), + AncCard(headphones as Anc), + ], + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( child: Column( - mainAxisAlignment: MainAxisAlignment.end, children: [ - Align( - alignment: Alignment.centerRight, - child: _HeadphonesSettingsButton(headphones), + StreamBuilder( + stream: headphones.bluetoothAlias, + builder: (_, snap) => Text( + snap.data ?? headphones.bluetoothName, + style: tt.headlineMedium, + ), ), - BatteryCard(headphones), - AncCard(headphones), + // TODO MIGRATION: Another image + const HeadphonesImage(), ], ), ), - ), - ], - ), - ); + Expanded( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // TODO MIGRATION: hp settings not yet implemented + // Align( + // alignment: Alignment.centerRight, + // child: _HeadphonesSettingsButton(headphones), + // ), + BatteryCard(headphones as LRCBattery), + AncCard(headphones as Anc), + ], + ), + ), + ), + ], + ), + ); + } else { + return const Text("no i dupa"); + } } } diff --git a/lib/ui/pages/home/controls/headphones_image.dart b/lib/ui/pages/home/controls/headphones_image.dart index a5eac0a..df0f1bb 100644 --- a/lib/ui/pages/home/controls/headphones_image.dart +++ b/lib/ui/pages/home/controls/headphones_image.dart @@ -1,16 +1,12 @@ import 'package:flutter/widgets.dart'; -import '../../../../headphones/headphones_base.dart'; - /// Image of the headphones (non-card) /// /// Selects the correct image for given model /// /// ...well, in the future :D class HeadphonesImage extends StatelessWidget { - final HeadphonesBase headphones; - - const HeadphonesImage(this.headphones, {super.key}); + const HeadphonesImage({super.key}); @override Widget build(BuildContext context) { From 5fa635880687702dc79c936c80b51ff283d23943 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Thu, 22 Feb 2024 11:00:04 +0100 Subject: [PATCH 05/19] inital data in anc sim --- lib/headphones/simulators/anc_sim.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/headphones/simulators/anc_sim.dart b/lib/headphones/simulators/anc_sim.dart index f82f777..b4a81cf 100644 --- a/lib/headphones/simulators/anc_sim.dart +++ b/lib/headphones/simulators/anc_sim.dart @@ -4,7 +4,7 @@ import '../framework/anc.dart'; /// Simulates anc actually being switched on the headphones mixin AncSim implements Anc { - final _ancModeCtrl = BehaviorSubject(); + final _ancModeCtrl = BehaviorSubject.seeded(AncMode.off); @override ValueStream get ancMode => _ancModeCtrl; From d44f3e6e07ef9f9a27b827a22c76dd35aa82e0c6 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Thu, 22 Feb 2024 16:19:58 +0100 Subject: [PATCH 06/19] implement image --- lib/README.md | 5 ++++ lib/headphones/framework/headphones_info.dart | 24 ++++++++++++++++++ lib/headphones/huawei/freebuds4i.dart | 8 ++++++ .../controls/headphones_controls_widget.dart | 10 +++----- .../pages/home/controls/headphones_image.dart | 25 +++++++++++-------- 5 files changed, 54 insertions(+), 18 deletions(-) diff --git a/lib/README.md b/lib/README.md index c7d8561..aa4e638 100644 --- a/lib/README.md +++ b/lib/README.md @@ -13,3 +13,8 @@ TODO: - [ ] Some database stuf... ehhhh... to distinguish between them, remember their last time etc This is potentially waaayyy ahead todo as it requires *serious* decisions that will affect *everything* wayyy ahead in later development, so better not fuck it up - [ ] Selecting headphones when making new widget + +## Future features: +- [ ] Detect hp colors and assign proper image +- [ ] ANC control widget + This will require whole big background stuff, so that's far offs \ No newline at end of file diff --git a/lib/headphones/framework/headphones_info.dart b/lib/headphones/framework/headphones_info.dart index 0a88a7a..bf5b44c 100644 --- a/lib/headphones/framework/headphones_info.dart +++ b/lib/headphones/framework/headphones_info.dart @@ -1,3 +1,5 @@ +import 'package:rxdart/rxdart.dart'; + /// Stuff that we need to know about concrete headphones model /// /// For example, FreeBuds 4i would implement it like: @@ -27,4 +29,26 @@ abstract class HeadphonesModelInfo { /// like on the market as possible (sometimes it's FREEBUDS, sometimes /// Freebuds - but most often FreeBuds - stick to that πŸ‘) String get name; + + /// Because image may change dynamically (detecting which color user has + /// based on some magic commands), this must be a stream. + /// + /// The format of this string should be absolute - you can pass it directly + /// to Image.asset and will work πŸ‘ + /// + /// All implementers should emit this *as fast as possible*, preferably + /// at class creation with initial base color image, and emit another correct + /// one when detected + /// + /// Preferably, set it already in base headphones abstract class and override + /// it in implementations if this ever actually changes + /// + /// Thus, consumers (the UI) should trust that it will come quickly and not + /// put any placeholders themselves + /// + /// ...yes, I don't like it either -_-. I thought this class is gonna be very + /// static and hard-coded, but I don't have better idea for it + // NOTE/WARNING: I'm not sure if I should... be closing this, or not? + // Probably yes, but... you know what, I'll wait until it causes some issuesπŸ‘ + ValueStream get imageAssetPath; } diff --git a/lib/headphones/huawei/freebuds4i.dart b/lib/headphones/huawei/freebuds4i.dart index f920499..be8c319 100644 --- a/lib/headphones/huawei/freebuds4i.dart +++ b/lib/headphones/huawei/freebuds4i.dart @@ -1,3 +1,5 @@ +import 'package:rxdart/rxdart.dart'; + import '../framework/anc.dart'; import '../framework/bluetooth_headphones.dart'; import '../framework/headphones_info.dart'; @@ -15,4 +17,10 @@ abstract base class HuaweiFreeBuds4i @override String get name => "FreeBuds 4i"; + + // NOTE/WARNING: Again as in HeadphonesModelInfo - i'm not sure if it's safe + // to just leave it like that, but I will πŸ₯°πŸ₯° + @override + ValueStream get imageAssetPath => + BehaviorSubject.seeded('assets/app_icons/ic_launcher.png'); } diff --git a/lib/ui/pages/home/controls/headphones_controls_widget.dart b/lib/ui/pages/home/controls/headphones_controls_widget.dart index 29bb1a3..c1fb3fb 100644 --- a/lib/ui/pages/home/controls/headphones_controls_widget.dart +++ b/lib/ui/pages/home/controls/headphones_controls_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../../../headphones/framework/anc.dart'; import '../../../../headphones/framework/bluetooth_headphones.dart'; +import '../../../../headphones/framework/headphones_info.dart'; import '../../../../headphones/framework/lrc_battery.dart'; import '../../../../headphones/headphones_base.dart'; import '../../../../headphones/huawei/freebuds4i.dart'; @@ -44,11 +45,7 @@ class HeadphonesControlsWidget extends StatelessWidget { style: tt.headlineMedium, ), ), - // TODO MIGRATION: Some way to assign images to models - // should base class contain path to img or should there be - // fancy function that does some logic? Such funtion would be - // nice to detect colors (this was requested by some users) - const HeadphonesImage(), + HeadphonesImage(headphones as HeadphonesModelInfo), // TODO MIGRATION: hp settings not yet implemented // Align( // alignment: Alignment.centerRight, @@ -71,8 +68,7 @@ class HeadphonesControlsWidget extends StatelessWidget { style: tt.headlineMedium, ), ), - // TODO MIGRATION: Another image - const HeadphonesImage(), + HeadphonesImage(headphones as HeadphonesModelInfo), ], ), ), diff --git a/lib/ui/pages/home/controls/headphones_image.dart b/lib/ui/pages/home/controls/headphones_image.dart index df0f1bb..5019627 100644 --- a/lib/ui/pages/home/controls/headphones_image.dart +++ b/lib/ui/pages/home/controls/headphones_image.dart @@ -1,23 +1,26 @@ import 'package:flutter/widgets.dart'; -/// Image of the headphones (non-card) -/// -/// Selects the correct image for given model -/// -/// ...well, in the future :D +import '../../../../headphones/framework/headphones_info.dart'; + class HeadphonesImage extends StatelessWidget { - const HeadphonesImage({super.key}); + final HeadphonesModelInfo modelInfo; + + const HeadphonesImage(this.modelInfo, {super.key}); @override Widget build(BuildContext context) { - // TODO: Switch image based on headphones return Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 64), - child: Image.asset( - 'assets/app_icons/ic_launcher.png', - fit: BoxFit.contain, - filterQuality: FilterQuality.none, + child: StreamBuilder( + stream: modelInfo.imageAssetPath, + builder: (_, snap) => snap.data != null + ? Image.asset( + snap.data!, + fit: BoxFit.contain, + filterQuality: FilterQuality.none, + ) + : const SizedBox(), ), ), ); From 9e43f9c8556c5a609eb717e6f182533f4bdc7284 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Thu, 22 Feb 2024 18:04:57 +0100 Subject: [PATCH 07/19] =?UTF-8?q?*some*=20detection=20for=20now=20?= =?UTF-8?q?=F0=9F=A4=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/headphones/README.md | 52 +++++++++++++++++++ .../cubit/headphones_connection_cubit.dart | 7 +-- lib/headphones/huawei/freebuds4i.dart | 5 ++ .../huawei/otter/otter_constants.dart | 10 ---- test/unit/device_constants_test.dart | 13 ----- 5 files changed, 61 insertions(+), 26 deletions(-) delete mode 100644 lib/headphones/huawei/otter/otter_constants.dart delete mode 100644 test/unit/device_constants_test.dart diff --git a/lib/headphones/README.md b/lib/headphones/README.md index cd77b37..487bf69 100644 --- a/lib/headphones/README.md +++ b/lib/headphones/README.md @@ -1,3 +1,20 @@ +# Hello! + +Nice to see you! This readme describes how freebuddy structures *the whole headphone stuff*, so it's a must-read for anyone wanting to implement new ones, or just looking around ^_^ + +Detecting paired headphones, managing serial/BLE connection with them, parsing their magic protocols, detecting available features - EVERYTHING is in here!! I did my **BEST** to organise that it's easy to understand, implement new models, find bugs and test πŸ’ͺπŸ’ͺπŸ’ͺ + +## Quick wrap +Here's a quick roadmap of how headphones get connected: +1. Proper cubit watches over bluetooth itself - is it enabled? Is it even available? The ui listens to this cubit and displays proper info (for example, button leading to system bt settings if it's disabled) + + (This qubit may be a fake one to ease up testing without physical headphones - it then emits fake HeadphoneClass and we don't worry about any steps below) +2. TODO: Proper function/switch-case-loop watches over connected devices, distinguishes which ones are headphones that we support, then creates their object by passing them their bluetoothDevice + serial/BLE objects + + Note that this step already manages their serial connection - headphone classes assume they are connected +3. Given headphone class listens to all serial data, parses it and exposes all recognized info though it's streams. Almost every value - battery, name, anc state - has it's own `Stream`, and UI can be split to re-usable `StreamBuilder`s +4. When headphones get disconnected, function from step 2. closes the `StreamChannel` passed to HeadphoneClass, HpClass closes all it's streams and gets forgotten πŸ’€ + # `framework/` This folder contains **only** abstract classes with **no** logic - the framework for all implementations to base on @@ -22,6 +39,40 @@ At some point, the freebuddy's code will contain some information about select h So, I want to be careful with this, but for now adding their model and vendor name doesn't seem that bad πŸ‘€ +# Vendor folders +Folders named by vendor - `huawei/`, `samsung/` etc contain headphones base classes, their implementations and simulators + +## Base classes +Most basically named classes - f.e. `freebuds4i.dart` contain "blueprints/specifiactions" for headphones - have a look: +```dart +abstract base class HuaweiFreeBuds4i + implements BluetoothHeadphones, HeadphonesModelInfo, LRCBattery, Anc {...} +``` + +We clearly see what features this model has - we use this class across UI, not caring how they are implemented + +### Discoverability stuff in base classes +I don't have a better idea for where to put identifiers - that is, name regexes, bluetooth uuids etc (stuff that distinguishes given model from other bluetooth devices) + +...so I'm putting it in static fields plainly in base classes 🀷 and later some big-ass switch will be like: + +```dart +// example pseudocode +switch(device.btName) { + case HuaweiFreeBuds4i.btName: + return HuaweiFreeBuds4iImpl(device.rfComm); + // ... etc +} +``` + +## Implementation classes +Classes named `SomeModelImpl` contain *THE* implementation - all bits and bytes parsing etc. Of course, you can extract some common-vendor stuff outside to share between models - this is up to you + +These classes typically get already-connected bluetooth serial in form of `StreamChannel` + +## Simulator classes +They are ready-to-use fake implementations of base classes - they are made with mixins from `simulators/`, and can be passed to ui instead of actual implementations πŸ‘πŸ‘ + # `simulators/` `simulator` is my name for a mock - they are here to help you test the app without connecting the headphones, or even without Android/Bluetooth itself @@ -39,6 +90,7 @@ final class CheapBudsSim extends CheapBuds with LRCBatterySim {} // Even more done!!! final class PoshBudsSim extends PoshBuds with ANCSim, LRCBatterySim{} +} ``` This is still kiiiiiinda boilerplate, but there may be properties that cannot be easily simulated with one-fits-all simulator, or you may want to do that manually diff --git a/lib/headphones/cubit/headphones_connection_cubit.dart b/lib/headphones/cubit/headphones_connection_cubit.dart index 9d1be80..671f95e 100644 --- a/lib/headphones/cubit/headphones_connection_cubit.dart +++ b/lib/headphones/cubit/headphones_connection_cubit.dart @@ -10,8 +10,8 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:the_last_bluetooth/the_last_bluetooth.dart'; import '../../logger.dart'; +import '../huawei/freebuds4i.dart'; import '../huawei/freebuds4i_impl.dart'; -import '../huawei/otter/otter_constants.dart'; import 'headphones_cubit_objects.dart'; class HeadphonesConnectionCubit extends Cubit { @@ -60,6 +60,7 @@ class HeadphonesConnectionCubit extends Cubit { Future connect() async => _connect(await _bluetooth.pairedDevices); + // TODO/MIGRATION: This whole big-ass connection/detection loop 🀯 Future _connect(List devices) async { if (!await _bluetooth.isEnabled()) { emit(HeadphonesBluetoothDisabled()); @@ -67,7 +68,7 @@ class HeadphonesConnectionCubit extends Cubit { } if (_connection != null) return; // already connected and working, skip final otter = devices - .firstWhereOrNull((d) => OtterConst.btDevNameRegex.hasMatch(d.name)); + .firstWhereOrNull((d) => HuaweiFreeBuds4i.idNameRegex.hasMatch(d.name)); if (otter == null) { emit(HeadphonesNotPaired()); return; @@ -106,7 +107,7 @@ class HeadphonesConnectionCubit extends Cubit { emit( ((await _bluetooth.pairedDevices) .firstWhereOrNull( - (d) => OtterConst.btDevNameRegex.hasMatch(d.name)) + (d) => HuaweiFreeBuds4i.idNameRegex.hasMatch(d.name)) ?.isConnected ?? false) ? HeadphonesConnectedClosed() diff --git a/lib/headphones/huawei/freebuds4i.dart b/lib/headphones/huawei/freebuds4i.dart index be8c319..be95191 100644 --- a/lib/headphones/huawei/freebuds4i.dart +++ b/lib/headphones/huawei/freebuds4i.dart @@ -23,4 +23,9 @@ abstract base class HuaweiFreeBuds4i @override ValueStream get imageAssetPath => BehaviorSubject.seeded('assets/app_icons/ic_launcher.png'); + + // As I said everywhere else - i have no good idea where to put this stuff :/ + // This will be a bit of chaos for now πŸ‘πŸ‘ + static final idNameRegex = + RegExp(r'^(?=(HUAWEI FreeBuds 4i))', caseSensitive: true); } diff --git a/lib/headphones/huawei/otter/otter_constants.dart b/lib/headphones/huawei/otter/otter_constants.dart deleted file mode 100644 index d17df34..0000000 --- a/lib/headphones/huawei/otter/otter_constants.dart +++ /dev/null @@ -1,10 +0,0 @@ -class OtterConst { - static const String name = 'Huawei Freebuds 4i'; - - @Deprecated("This seems to not work... use [btDevNameRegex] instead") - static final btMacRegex = RegExp(r'60:AA:..:..:..:7E', caseSensitive: false); - - // Copied straight from decompiled app - static final btDevNameRegex = - RegExp(r'^(?=(HUAWEI FreeBuds 4i))', caseSensitive: true); -} diff --git a/test/unit/device_constants_test.dart b/test/unit/device_constants_test.dart deleted file mode 100644 index fe92fbf..0000000 --- a/test/unit/device_constants_test.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:freebuddy/headphones/huawei/otter/otter_constants.dart'; - -void main() { - group("Device constants tests", () { - test("Otter name regex match", () { - expect(OtterConst.btDevNameRegex.hasMatch("HUAWEI FreeBuds 4i"), true); - expect(OtterConst.btDevNameRegex.hasMatch("HUAWEI FreeBuds 4i "), true); - expect(OtterConst.btDevNameRegex.hasMatch("huawei freebuds 4i"), false); - expect(OtterConst.btDevNameRegex.hasMatch("HUAWEI FreeBuds Pro"), false); - }); - }); -} From cb722c0c3d50bcb1d760b94861bd79c06ca61524 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Thu, 22 Feb 2024 18:16:45 +0100 Subject: [PATCH 08/19] fix doc --- lib/headphones/huawei/mbb.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/headphones/huawei/mbb.dart b/lib/headphones/huawei/mbb.dart index c29139a..fdec5b1 100644 --- a/lib/headphones/huawei/mbb.dart +++ b/lib/headphones/huawei/mbb.dart @@ -34,7 +34,7 @@ class MbbUtils { sum[1] == payload[payload.length - 1]; } - /// Will throw exception if anything wrong. Otherwise does nothing. + /// Will return exception if anything wrong. Otherwise does nothing. static Exception? verifyIntegrity(Uint8List payload) { // 3 magic bytes, 1 length, 1 service, 1 command, 2 checksum if (payload.length < 3 + 1 + 1 + 1 + 2) { From 4055229f7fecd80b09654f676bbace8537f32819 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Thu, 22 Feb 2024 18:38:08 +0100 Subject: [PATCH 09/19] copy-paste legacy stuff to *the framework* we're getting there @mshpp ;) --- lib/headphones/huawei/freebuds4i_impl.dart | 163 +++++++++++++++++++-- 1 file changed, 153 insertions(+), 10 deletions(-) diff --git a/lib/headphones/huawei/freebuds4i_impl.dart b/lib/headphones/huawei/freebuds4i_impl.dart index 47d1112..347c022 100644 --- a/lib/headphones/huawei/freebuds4i_impl.dart +++ b/lib/headphones/huawei/freebuds4i_impl.dart @@ -1,11 +1,15 @@ +import 'dart:async'; +import 'dart:math'; import 'dart:typed_data'; import 'package:rxdart/rxdart.dart'; import 'package:stream_channel/stream_channel.dart'; +import '../../logger.dart'; import '../framework/anc.dart'; import '../framework/lrc_battery.dart'; import 'freebuds4i.dart'; +import 'mbb.dart'; final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { /// Bluetooth serial port that we communicate over @@ -20,20 +24,150 @@ final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { // stream controllers * + /// This watches if we are still missing any info and re-requests it + late StreamSubscription _watchdogStreamSub; + HuaweiFreeBuds4iImpl(this._rfcomm) { - _rfcomm.stream.listen((event) {}, onDone: () { + _rfcomm.stream.listen((event) { + List? commands; + try { + commands = MbbCommand.fromPayload(event); + } catch (e, s) { + logg.e("mbb parsing error", error: e, stackTrace: s); + } + for (final cmd in commands ?? []) { + // FILTER THE SHIT OUT + if (cmd.serviceId == 10 && cmd.commandId == 13) return; + try { + _evalMbbCommand(cmd); + } on RangeError catch (e, s) { + logg.e('Error while parsing mbb cmd - (probably missing bytes)', + error: e, stackTrace: s); + } + } + }, onDone: () { // close all streams _batteryLevelCtrl.close(); _bluetoothAliasCtrl.close(); _bluetoothNameCtrl.close(); _lrcBatteryCtrl.close(); _ancModeCtrl.close(); + + _watchdogStreamSub.cancel(); + }); + _initRequestInfo(); + _watchdogStreamSub = + Stream.periodic(const Duration(seconds: 3)).listen((_) { + if ([ + batteryLevel.valueOrNull, + // no alias because it's okay to be null πŸ‘ + lrcBattery.valueOrNull, + ancMode.valueOrNull, + ].any((e) => e == null)) { + _initRequestInfo(); + } }); } + // TODO: Do something smart about this for @starw1nd_ :) + void _evalMbbCommand(MbbCommand cmd) { + // TODO/MISSING: Gesture settings + // final lastGestures = _gestureSettingsStreamCtrl.valueOrNull ?? + // const HeadphonesGestureSettings(); + if (cmd.serviceId == 43 && cmd.commandId == 42 && cmd.args.containsKey(1)) { + late AncMode newMode; + // TODO: Add some constants for this globally + // because 0 1 and 2 seem to be constant bytes representing the modes + final modeByte = cmd.args[1]![1]; + if (modeByte == 1) { + newMode = AncMode.noiseCancelling; + } else if (modeByte == 0) { + newMode = AncMode.off; + } else if (modeByte == 2) { + newMode = AncMode.transparency; + } else { + logg.e("Unknown ANC mode: ${cmd.args[1]}"); + return; + } + _ancModeCtrl.add(newMode); + } else if (cmd.serviceId == 1 && + (cmd.commandId == 39 || cmd.commandId == 8) && + cmd.args.length >= 3) { + final level = cmd.args[2]; + final status = cmd.args[3]; + if (level == null || status == null) { + logg.e("Battery data is missing level or status"); + return; + } + _lrcBatteryCtrl.add(LRCBatteryLevels( + level[0] == 0 ? null : level[0], + level[1] == 0 ? null : level[1], + level[2] == 0 ? null : level[2], + status[0] == 1, + status[1] == 1, + status[2] == 1, + )); + } else if (cmd.serviceId == 43 && + cmd.commandId == 17 && + cmd.args.containsKey(1)) { + // TODO/MISSING: Auto pause settings + // _autoPauseStreamCtrl.add(cmd.args[1]![0] == 1); + } else if (cmd.serviceId == 1 && cmd.commandId == 32) { + // TODO/MISSING: Gesture settings + // _gestureSettingsStreamCtrl.add( + // lastGestures.copyWith( + // doubleTapLeft: cmd.args[1] != null + // ? HeadphonesGestureDoubleTap.fromMbbValue(cmd.args[1]![0]) + // : lastGestures.doubleTapLeft, + // doubleTapRight: cmd.args[2] != null + // ? HeadphonesGestureDoubleTap.fromMbbValue(cmd.args[2]![0]) + // : lastGestures.doubleTapRight, + // ), + // ); + } else if ((cmd.serviceId == 43 && cmd.commandId == 23)) { + // TODO/MISSING: Gesture settings + // _gestureSettingsStreamCtrl.add( + // lastGestures.copyWith( + // holdBoth: (cmd.args[1] != null) + // ? HeadphonesGestureHold.fromMbbValue(cmd.args[1]![0]) + // : lastGestures.holdBoth, + // ), + // ); + } else if (cmd.serviceId == 43 && cmd.commandId == 25) { + // TODO/MISSING: Gesture settings + // _gestureSettingsStreamCtrl.add( + // lastGestures.copyWith( + // holdBothToggledAncModes: (cmd.args[1] != null) + // ? gestureHoldFromMbbValue(cmd.args[1]![0]) + // : lastGestures.holdBothToggledAncModes, + // ), + // ); + } + } + + Future _initRequestInfo() async { + await _sendMbb(MbbCommand.requestBattery); + await _sendMbb(MbbCommand.requestAnc); + // TODO/MISSING: Settings + // await _sendMbb(MbbCommand.requestAutoPause); + // await _sendMbb(MbbCommand.requestGestureDoubleTap); + // await _sendMbb(MbbCommand.requestGestureHold); + // await _sendMbb(MbbCommand.requestGestureHoldToggledAncModes); + } + + // TODO: some .flush() for this + Future _sendMbb(MbbCommand comm) async { + logg.t("⬆ Sending mbb cmd: $comm"); + _rfcomm.sink.add(comm.toPayload()); + } + + // TODO: Get this from basic bluetooth object (when we actually have those) + // but this is fairly good for now @override - // TODO: implement batteryLevel - ValueStream get batteryLevel => throw UnimplementedError(); + ValueStream get batteryLevel => _lrcBatteryCtrl.stream + .map((l) => max(l.levelLeft ?? -1, l.levelRight ?? -1)) + .where((b) => b >= 0) + .shareValue(); @override // TODO: implement bluetoothAlias @@ -48,16 +182,25 @@ final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { String get macAddress => throw UnimplementedError(); @override - // TODO: implement batteryData - ValueStream get lrcBattery => throw UnimplementedError(); + ValueStream get lrcBattery => _lrcBatteryCtrl.stream; @override - // TODO: implement ancMode - ValueStream get ancMode => throw UnimplementedError(); + ValueStream get ancMode => _ancModeCtrl.stream; @override - Future setAncMode(AncMode mode) { - // TODO: implement setAncMode - throw UnimplementedError(); + Future setAncMode(AncMode mode) async { + late MbbCommand comm; + switch (mode) { + case AncMode.noiseCancelling: + comm = MbbCommand.ancNoiseCancel; + break; + case AncMode.off: + comm = MbbCommand.ancOff; + break; + case AncMode.transparency: + comm = MbbCommand.ancAware; + break; + } + await _sendMbb(comm); } } From ee0a76fc19dc3639e536a8e9d3a63290cfa7e8c4 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Thu, 22 Feb 2024 23:21:41 +0100 Subject: [PATCH 10/19] tell the whole world that we're not only 4i --- README.md | 25 +++++++++++-------------- lib/README.md | 7 +++++++ pubspec.yaml | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 889a52f..e0642d4 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,13 @@ [![resolved Github issues](https://img.shields.io/github/issues-closed/TheLastGimbus/FreeBuddy?label=resolved%20issues)](https://github.com/TheLastGimbus/FreeBuddy/issues) [![commit activity](https://img.shields.io/github/commit-activity/y/TheLastGimbus/FreeBuddy)](https://github.com/TheLastGimbus/FreeBuddy/graphs/contributors) -Free and open source app for Huawei Freebuds 4i headphones 🎧 +Free (and open source) buddy app for bluetooth headphones 🎧 ## Why 🧐 -I wanted to quickly switch ANC modes on my headphones... +I wanted to quickly switch ANC modes on my headphones... but official apps are usually bloated as hell πŸ˜‘ (at least Huawei's was - enough to motivate me to create *this*) -but official app is... well, bloated as hell πŸ˜‘ - it weights 100mb, and takes (on my phone) 10 seconds to open... - -Also, it doesn't have features like "find lost headphones", etc... +Also, they sometimes lack features that *are possible* to implement - like "find lost headphones", etc... So I got mad and decided to make my own... and here it is 🌈 @@ -21,17 +19,16 @@ So I got mad and decided to make my own... and here it is 🌈 Freebuddy home widget light mode Freebuddy home widget dark mode +## Supported headphones πŸ—‚οΈ + +Right now FreeBuddy only supports handful of models, but this is ment to expand in future 🀞 (psst, if you would like to help, head over to [the.lastgimbus.com/freebuddy/dev/](https://the.lastgimbus.com/freebuddy/dev/)) + +- Huawei FreeBuds 4i + ## Features πŸ”¨ -- [x] Show battery levels -- [x] Switch ANC modes -- [x] Change settings - - [x] Smart wear - - [x] Tap actions -- [x] Cool features: - - [x] Home screen widget with battery charge πŸ”‹ - - [ ] ANC lock - lock on certain ANC mode no matter what - - [ ] Find lost +FreeBuddy can already do everything that stock apps can, while adding own features: + - Home screen widget with battery charge πŸ”‹ ## Download πŸ“¦ diff --git a/lib/README.md b/lib/README.md index aa4e638..8f33912 100644 --- a/lib/README.md +++ b/lib/README.md @@ -1,3 +1,10 @@ +# Hi!!!!!! πŸ‘‹πŸ‘‹πŸ‘‹πŸ‘‹πŸ‘‹ +You're coding and want to help out/explore around here? That's great!!! + +Okay, so - UI code is as pure-flutter-future-stream as possible, and most stuff has comments/documentation, so just look around and read those πŸ‘ + +Interesting rev-eng stuff is in `headphones/` folder, and there is dedicated [headphones/README.md](headphones/README.md) - head there 🫑 + # Development notes - philosophies, TODOs etc ## Migration to framework-ish organisation diff --git a/pubspec.yaml b/pubspec.yaml index 029a993..a15f23a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: freebuddy -description: Open-source app for Huawei Freebuds 4i headphones +description: Free (and open source) buddy app for bluetooth headphones # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. From a9de2407fc18c14ba0f11d71191904ea0824cfed Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 23 Feb 2024 00:14:05 +0100 Subject: [PATCH 11/19] make whole ui model-oblivious you can ctrl-f over whole `ui/` and find no mention of specific hp model (that is, freebuds4i for now) whatsoever :100: --- lib/README.md | 4 +- .../cubit/headphones_connection_cubit.dart | 10 +- .../cubit/headphones_cubit_objects.dart | 36 +++-- .../cubit/headphones_mock_cubit.dart | 3 +- lib/headphones/huawei/freebuds4i.dart | 2 + lib/headphones/huawei/freebuds4i_sim.dart | 33 +++++ .../android/background/periodic.dart | 1 + ...eadphones_connection_ensuring_overlay.dart | 16 +-- .../controls/headphones_controls_widget.dart | 124 ++++++++++-------- 9 files changed, 149 insertions(+), 80 deletions(-) diff --git a/lib/README.md b/lib/README.md index 8f33912..336b6c2 100644 --- a/lib/README.md +++ b/lib/README.md @@ -13,10 +13,12 @@ TODO: - [X] Change ui to work at all with new stuff - [ ] Implement hp settings This is non trivial cause we have to decide how to make this universal when almost all headphones have this different -- [ ] Change ui BIG to dynamically support *all* headphones by their features instead of concrete model +- [X] Change ui BIG to dynamically support *all* headphones by their features instead of concrete model -> pretty much done? - [ ] Support multiple diff headphones - [ ] Basic fix in connection loops to show them at all - [ ] Basic fix in widget to maybe show their name to distinguish which ones are shown now + Currently, widget just creates the cubit, wait until it connects, and gets the battery. What if there are + multiple headphones? Right now it just... gets the first one 🀷 - [ ] Some database stuf... ehhhh... to distinguish between them, remember their last time etc This is potentially waaayyy ahead todo as it requires *serious* decisions that will affect *everything* wayyy ahead in later development, so better not fuck it up - [ ] Selecting headphones when making new widget diff --git a/lib/headphones/cubit/headphones_connection_cubit.dart b/lib/headphones/cubit/headphones_connection_cubit.dart index 671f95e..42ba23d 100644 --- a/lib/headphones/cubit/headphones_connection_cubit.dart +++ b/lib/headphones/cubit/headphones_connection_cubit.dart @@ -12,6 +12,7 @@ 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'; class HeadphonesConnectionCubit extends Cubit { @@ -61,6 +62,7 @@ class HeadphonesConnectionCubit extends Cubit { Future 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 _connect(List devices) async { if (!await _bluetooth.isEnabled()) { emit(HeadphonesBluetoothDisabled()); @@ -75,10 +77,10 @@ class HeadphonesConnectionCubit extends Cubit { } if (!otter.isConnected) { // not connected to device at all - emit(HeadphonesDisconnected()); + emit(const HeadphonesDisconnected(HuaweiFreeBuds4iSimPlaceholder())); return; } - emit(HeadphonesConnecting()); + emit(const HeadphonesConnecting(HuaweiFreeBuds4iSimPlaceholder())); try { // when Ai Life takes over our socket, the connecting always succeeds at // 2'nd try πŸ€” @@ -110,8 +112,8 @@ class HeadphonesConnectionCubit extends Cubit { (d) => HuaweiFreeBuds4i.idNameRegex.hasMatch(d.name)) ?.isConnected ?? false) - ? HeadphonesConnectedClosed() - : HeadphonesDisconnected(), + ? const HeadphonesConnectedClosed(HuaweiFreeBuds4iSimPlaceholder()) + : const HeadphonesDisconnected(HuaweiFreeBuds4iSimPlaceholder()), ); } diff --git a/lib/headphones/cubit/headphones_cubit_objects.dart b/lib/headphones/cubit/headphones_cubit_objects.dart index 52fe9a7..c09c414 100644 --- a/lib/headphones/cubit/headphones_cubit_objects.dart +++ b/lib/headphones/cubit/headphones_cubit_objects.dart @@ -1,21 +1,41 @@ import '../framework/bluetooth_headphones.dart'; -abstract class HeadphonesConnectionState {} +abstract class HeadphonesConnectionState { + const HeadphonesConnectionState(); +} + +class HeadphonesNoPermission extends HeadphonesConnectionState { + const HeadphonesNoPermission(); +} + +class HeadphonesBluetoothDisabled extends HeadphonesConnectionState { + const HeadphonesBluetoothDisabled(); +} -class HeadphonesNoPermission extends HeadphonesConnectionState {} +class HeadphonesNotPaired extends HeadphonesConnectionState { + const HeadphonesNotPaired(); +} -class HeadphonesBluetoothDisabled extends HeadphonesConnectionState {} +class HeadphonesDisconnected extends HeadphonesConnectionState { + final BluetoothHeadphones placeholder; -class HeadphonesNotPaired extends HeadphonesConnectionState {} + const HeadphonesDisconnected(this.placeholder); +} -class HeadphonesDisconnected extends HeadphonesConnectionState {} +class HeadphonesConnecting extends HeadphonesConnectionState { + final BluetoothHeadphones placeholder; -class HeadphonesConnecting extends HeadphonesConnectionState {} + const HeadphonesConnecting(this.placeholder); +} class HeadphonesConnectedOpen extends HeadphonesConnectionState { final BluetoothHeadphones headphones; - HeadphonesConnectedOpen(this.headphones); + const HeadphonesConnectedOpen(this.headphones); } -class HeadphonesConnectedClosed extends HeadphonesConnectionState {} +class HeadphonesConnectedClosed extends HeadphonesConnectionState { + final BluetoothHeadphones placeholder; + + const HeadphonesConnectedClosed(this.placeholder); +} diff --git a/lib/headphones/cubit/headphones_mock_cubit.dart b/lib/headphones/cubit/headphones_mock_cubit.dart index f421698..ef2e1cd 100644 --- a/lib/headphones/cubit/headphones_mock_cubit.dart +++ b/lib/headphones/cubit/headphones_mock_cubit.dart @@ -8,7 +8,8 @@ import 'headphones_cubit_objects.dart'; class HeadphonesMockCubit extends Cubit implements HeadphonesConnectionCubit { - HeadphonesMockCubit() : super(HeadphonesDisconnected()) { + HeadphonesMockCubit() + : super(const HeadphonesDisconnected(HuaweiFreeBuds4iSimPlaceholder())) { // i do this because otherwise initial data isn't even emitted and // [BlocListener]s don't work >:( Future.microtask( diff --git a/lib/headphones/huawei/freebuds4i.dart b/lib/headphones/huawei/freebuds4i.dart index be95191..0517958 100644 --- a/lib/headphones/huawei/freebuds4i.dart +++ b/lib/headphones/huawei/freebuds4i.dart @@ -12,6 +12,8 @@ import '../framework/lrc_battery.dart'; /// features they implement), and some basic info for easy simulation abstract base class HuaweiFreeBuds4i implements BluetoothHeadphones, HeadphonesModelInfo, LRCBattery, Anc { + const HuaweiFreeBuds4i(); + @override String get vendor => "Huawei"; diff --git a/lib/headphones/huawei/freebuds4i_sim.dart b/lib/headphones/huawei/freebuds4i_sim.dart index 67e7cdc..cb759d7 100644 --- a/lib/headphones/huawei/freebuds4i_sim.dart +++ b/lib/headphones/huawei/freebuds4i_sim.dart @@ -1,3 +1,7 @@ +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'; @@ -5,3 +9,32 @@ import 'freebuds4i.dart'; final class HuaweiFreeBuds4iSim extends HuaweiFreeBuds4i with BluetoothHeadphonesSim, LRCBatteryAlwaysFullSim, AncSim {} + +/// Class to use as placeholder for Disabled() widget +// this is not done with mixins because we may want to fill it with +// last-remembered values in future, and we will pretty much override +// all of this +final class HuaweiFreeBuds4iSimPlaceholder extends HuaweiFreeBuds4i { + const HuaweiFreeBuds4iSimPlaceholder(); + + @override + ValueStream get ancMode => BehaviorSubject(); + + @override + ValueStream get batteryLevel => BehaviorSubject(); + + @override + ValueStream get bluetoothAlias => BehaviorSubject(); + + @override + String get bluetoothName => super.vendor + super.name; + + @override + ValueStream get lrcBattery => BehaviorSubject(); + + @override + String get macAddress => ''; + + @override + Future setAncMode(AncMode mode) async {} +} diff --git a/lib/platform_stuff/android/background/periodic.dart b/lib/platform_stuff/android/background/periodic.dart index 1b4d9b5..53e1cfa 100644 --- a/lib/platform_stuff/android/background/periodic.dart +++ b/lib/platform_stuff/android/background/periodic.dart @@ -29,6 +29,7 @@ Future routineUpdateCallback() async { "because cubit is already running"); return true; } + // TODO: Multi-headphones BIG decisions here 😬😬 - possibly impossible to resolve without nuking all of this // NOT_SURE: Also use real/mock logic here?? idk, but if you want, // feel free to make some proper DI for this to be shared in UI and here final cubit = di.getHeadphonesCubit(); diff --git a/lib/ui/common/headphones_connection_ensuring_overlay.dart b/lib/ui/common/headphones_connection_ensuring_overlay.dart index 2518376..f987dbf 100644 --- a/lib/ui/common/headphones_connection_ensuring_overlay.dart +++ b/lib/ui/common/headphones_connection_ensuring_overlay.dart @@ -6,7 +6,6 @@ import '../../headphones/cubit/headphones_connection_cubit.dart'; import '../../headphones/cubit/headphones_cubit_objects.dart'; import '../../headphones/framework/bluetooth_headphones.dart'; import '../../headphones/headphones_mocks.dart'; -import '../../headphones/huawei/freebuds4i_sim.dart'; import '../pages/disabled.dart'; import '../pages/home/bluetooth_disabled_info_widget.dart'; import '../pages/home/connected_closed_widget.dart'; @@ -73,13 +72,14 @@ class HeadphonesConnectionEnsuringOverlay extends StatelessWidget { }, child: builder( context, - state is HeadphonesConnectedOpen - ? state.headphones - // TODO MIGRATION: Think about this mock - should it be - // headphone-specific or generic or what. Should mocks be - // as easily detected as concrete headphones, to properly mock - // display mock in grayed-out ui when not connected? - : HuaweiFreeBuds4iSim(), + switch (state) { + HeadphonesConnectedOpen(headphones: final hp) => hp, + HeadphonesDisconnected(placeholder: final ph) || + HeadphonesConnecting(placeholder: final ph) || + HeadphonesConnectedClosed(placeholder: final ph) => + ph, + _ => throw 'impossible :O' + }, ), ), _ => Text(l.pageHomeUnknown), diff --git a/lib/ui/pages/home/controls/headphones_controls_widget.dart b/lib/ui/pages/home/controls/headphones_controls_widget.dart index c1fb3fb..cded15a 100644 --- a/lib/ui/pages/home/controls/headphones_controls_widget.dart +++ b/lib/ui/pages/home/controls/headphones_controls_widget.dart @@ -6,7 +6,6 @@ import '../../../../headphones/framework/bluetooth_headphones.dart'; import '../../../../headphones/framework/headphones_info.dart'; import '../../../../headphones/framework/lrc_battery.dart'; import '../../../../headphones/headphones_base.dart'; -import '../../../../headphones/huawei/freebuds4i.dart'; import '../../../theme/layouts.dart'; import 'anc_card.dart'; import 'battery_card.dart'; @@ -30,70 +29,79 @@ class HeadphonesControlsWidget extends StatelessWidget { Widget build(BuildContext context) { final t = Theme.of(context); final tt = t.textTheme; - // TODO MIGRATION: Whole big ass branching here in detecting whether hp - // support different features - if (headphones is HuaweiFreeBuds4i) { - return Padding( - padding: const EdgeInsets.all(12.0), - child: WindowSizeClass.of(context) == WindowSizeClass.compact - ? Column( - children: [ - StreamBuilder( - stream: headphones.bluetoothAlias, - builder: (_, snap) => Text( - snap.data ?? headphones.bluetoothName, - style: tt.headlineMedium, - ), + // TODO here: + // - [ ] Make this clearer - this padding shouldn't be here? + // - [ ] De-duplicate responsive stuff + // - [ ] Think what to put when we have no image, or generally not many + // features 🀷 + return Padding( + padding: const EdgeInsets.all(12.0), + child: WindowSizeClass.of(context) == WindowSizeClass.compact + ? Column( + children: [ + StreamBuilder( + stream: headphones.bluetoothAlias, + builder: (_, snap) => Text( + snap.data ?? headphones.bluetoothName, + style: tt.headlineMedium, ), - HeadphonesImage(headphones as HeadphonesModelInfo), - // TODO MIGRATION: hp settings not yet implemented - // Align( - // alignment: Alignment.centerRight, - // child: _HeadphonesSettingsButton(headphones), - // ), + ), + if (headphones is HeadphonesModelInfo) + HeadphonesImage(headphones as HeadphonesModelInfo) + else + // TODO: This is ugly. Very + const Expanded(child: Icon(Icons.headphones, size: 64)), + // TODO MIGRATION: hp settings not yet implemented + // Align( + // alignment: Alignment.centerRight, + // child: _HeadphonesSettingsButton(headphones), + // ), + if (headphones is LRCBattery) BatteryCard(headphones as LRCBattery), - AncCard(headphones as Anc), - ], - ) - : Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Column( - children: [ - StreamBuilder( - stream: headphones.bluetoothAlias, - builder: (_, snap) => Text( - snap.data ?? headphones.bluetoothName, - style: tt.headlineMedium, - ), + if (headphones is Anc) AncCard(headphones as Anc), + ], + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + children: [ + StreamBuilder( + stream: headphones.bluetoothAlias, + builder: (_, snap) => Text( + snap.data ?? headphones.bluetoothName, + style: tt.headlineMedium, ), - HeadphonesImage(headphones as HeadphonesModelInfo), - ], - ), + ), + if (headphones is HeadphonesModelInfo) + HeadphonesImage(headphones as HeadphonesModelInfo) + else + // TODO: This is ugly. Very + const Expanded(child: Icon(Icons.headphones, size: 64)), + ], ), - Expanded( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - // TODO MIGRATION: hp settings not yet implemented - // Align( - // alignment: Alignment.centerRight, - // child: _HeadphonesSettingsButton(headphones), - // ), + ), + Expanded( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // TODO MIGRATION: hp settings not yet implemented + // Align( + // alignment: Alignment.centerRight, + // child: _HeadphonesSettingsButton(headphones), + // ), + if (headphones is LRCBattery) BatteryCard(headphones as LRCBattery), - AncCard(headphones as Anc), - ], - ), + if (headphones is Anc) AncCard(headphones as Anc), + ], ), ), - ], - ), - ); - } else { - return const Text("no i dupa"); - } + ), + ], + ), + ); } } From 7d2bafcce071285f6daea3ac8e40f410d24c1652 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 23 Feb 2024 00:14:49 +0100 Subject: [PATCH 12/19] use consts --- lib/headphones/cubit/headphones_connection_cubit.dart | 10 +++++----- lib/main.dart | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/headphones/cubit/headphones_connection_cubit.dart b/lib/headphones/cubit/headphones_connection_cubit.dart index 42ba23d..4998781 100644 --- a/lib/headphones/cubit/headphones_connection_cubit.dart +++ b/lib/headphones/cubit/headphones_connection_cubit.dart @@ -65,14 +65,14 @@ class HeadphonesConnectionCubit extends Cubit { // for example, all placeholders assume we have 4i... not good Future _connect(List devices) async { if (!await _bluetooth.isEnabled()) { - emit(HeadphonesBluetoothDisabled()); + 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(HeadphonesNotPaired()); + emit(const HeadphonesNotPaired()); return; } if (!otter.isConnected) { @@ -119,7 +119,7 @@ class HeadphonesConnectionCubit extends Cubit { HeadphonesConnectionCubit({required TheLastBluetooth bluetooth}) : _bluetooth = bluetooth, - super(HeadphonesNotPaired()) { + super(const HeadphonesNotPaired()) { IsolateNameServer.removePortNameMapping(pingReceivePortName); IsolateNameServer.registerPortWithName( _pingReceivePort.sendPort, pingReceivePortName); @@ -133,12 +133,12 @@ class HeadphonesConnectionCubit extends Cubit { Future _init() async { // it's down here to be sure that we do have device connected so if (!await Permission.bluetoothConnect.isGranted) { - emit(HeadphonesNoPermission()); + emit(const HeadphonesNoPermission()); return; } _btStream = _bluetooth.adapterInfoStream.listen((event) { _btEnabledCache = event.isEnabled; - if (!event.isEnabled) emit(HeadphonesBluetoothDisabled()); + if (!event.isEnabled) emit(const HeadphonesBluetoothDisabled()); }); // logic of connect() is so universal we can use it on every change _devStream = _bluetooth.pairedDevicesStream.listen(_connect); diff --git a/lib/main.dart b/lib/main.dart index a6b9890..4fcd3a5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -55,7 +55,7 @@ class _MyAppWrapperState extends State .firstWhere((e) => e is HeadphonesConnectedOpen) .timeout( const Duration(seconds: 1), - onTimeout: () => HeadphonesNotPaired(), // just placeholder + onTimeout: () => const HeadphonesNotPaired(), // just placeholder ) .then((_) => FlutterNativeSplash.remove()); super.initState(); From 282fd0d91aa879730e09b42dc312e958cac26598 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 23 Feb 2024 00:32:19 +0100 Subject: [PATCH 13/19] ah --- lib/headphones/huawei/freebuds4i_impl.dart | 4 ++-- lib/headphones/huawei/freebuds4i_sim.dart | 2 +- lib/headphones/simulators/bluetooth_headphones_sim.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/headphones/huawei/freebuds4i_impl.dart b/lib/headphones/huawei/freebuds4i_impl.dart index 347c022..faadf17 100644 --- a/lib/headphones/huawei/freebuds4i_impl.dart +++ b/lib/headphones/huawei/freebuds4i_impl.dart @@ -171,11 +171,11 @@ final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { @override // TODO: implement bluetoothAlias - ValueStream get bluetoothAlias => throw UnimplementedError(); + ValueStream get bluetoothAlias => BehaviorSubject(); @override // TODO: implement bluetoothName - String get bluetoothName => throw UnimplementedError(); + String get bluetoothName => '${super.vendor} ${super.name}'; @override // TODO: implement macAddress diff --git a/lib/headphones/huawei/freebuds4i_sim.dart b/lib/headphones/huawei/freebuds4i_sim.dart index cb759d7..f4b1d82 100644 --- a/lib/headphones/huawei/freebuds4i_sim.dart +++ b/lib/headphones/huawei/freebuds4i_sim.dart @@ -27,7 +27,7 @@ final class HuaweiFreeBuds4iSimPlaceholder extends HuaweiFreeBuds4i { ValueStream get bluetoothAlias => BehaviorSubject(); @override - String get bluetoothName => super.vendor + super.name; + String get bluetoothName => '${super.vendor} ${super.name}'; @override ValueStream get lrcBattery => BehaviorSubject(); diff --git a/lib/headphones/simulators/bluetooth_headphones_sim.dart b/lib/headphones/simulators/bluetooth_headphones_sim.dart index b921911..5cbb3ef 100644 --- a/lib/headphones/simulators/bluetooth_headphones_sim.dart +++ b/lib/headphones/simulators/bluetooth_headphones_sim.dart @@ -13,7 +13,7 @@ mixin BluetoothHeadphonesSim on HeadphonesModelInfo String get macAddress => "AA:BB:CC:DD:EE:FF"; @override - String get bluetoothName => vendor + name; + String get bluetoothName => '$vendor $name'; @override ValueStream get bluetoothAlias => Stream.value(name).shareValue(); From 5b6690ca01030ef18acbf298e7ef33939795fbe9 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 23 Feb 2024 00:43:05 +0100 Subject: [PATCH 14/19] note --- lib/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/README.md b/lib/README.md index 336b6c2..99b1b93 100644 --- a/lib/README.md +++ b/lib/README.md @@ -14,7 +14,7 @@ TODO: - [ ] Implement hp settings This is non trivial cause we have to decide how to make this universal when almost all headphones have this different - [X] Change ui BIG to dynamically support *all* headphones by their features instead of concrete model -> pretty much done? -- [ ] Support multiple diff headphones +- [ ] Support multiple diff/same headphones - [ ] Basic fix in connection loops to show them at all - [ ] Basic fix in widget to maybe show their name to distinguish which ones are shown now Currently, widget just creates the cubit, wait until it connects, and gets the battery. What if there are @@ -22,6 +22,7 @@ TODO: - [ ] Some database stuf... ehhhh... to distinguish between them, remember their last time etc This is potentially waaayyy ahead todo as it requires *serious* decisions that will affect *everything* wayyy ahead in later development, so better not fuck it up - [ ] Selecting headphones when making new widget +- [ ] Tests for everything ## Future features: - [ ] Detect hp colors and assign proper image From 3880dd32c78cd7e67ad0be17f03e8ce5a3ca19c9 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 23 Feb 2024 00:44:20 +0100 Subject: [PATCH 15/19] =?UTF-8?q?remove=20old=20import=20bc=20code=20is=20?= =?UTF-8?q?great=20now=20=F0=9F=92=85=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ui/common/headphones_connection_ensuring_overlay.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ui/common/headphones_connection_ensuring_overlay.dart b/lib/ui/common/headphones_connection_ensuring_overlay.dart index f987dbf..618ef03 100644 --- a/lib/ui/common/headphones_connection_ensuring_overlay.dart +++ b/lib/ui/common/headphones_connection_ensuring_overlay.dart @@ -5,7 +5,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../headphones/cubit/headphones_connection_cubit.dart'; import '../../headphones/cubit/headphones_cubit_objects.dart'; import '../../headphones/framework/bluetooth_headphones.dart'; -import '../../headphones/headphones_mocks.dart'; import '../pages/disabled.dart'; import '../pages/home/bluetooth_disabled_info_widget.dart'; import '../pages/home/connected_closed_widget.dart'; From fee7083eba529318600c47a5502505d7811f31f2 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 23 Feb 2024 00:45:11 +0100 Subject: [PATCH 16/19] make tests work holy shit they still work --- lib/headphones/framework/lrc_battery.dart | 21 ++++++++++++ .../unit/headphones_impl/impl_otter_test.dart | 33 ++++++++++--------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/headphones/framework/lrc_battery.dart b/lib/headphones/framework/lrc_battery.dart index 50aa9bb..61c8475 100644 --- a/lib/headphones/framework/lrc_battery.dart +++ b/lib/headphones/framework/lrc_battery.dart @@ -21,4 +21,25 @@ class LRCBatteryLevels { this.chargingRight, this.chargingCase, ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LRCBatteryLevels && + runtimeType == other.runtimeType && + levelLeft == other.levelLeft && + levelRight == other.levelRight && + levelCase == other.levelCase && + chargingLeft == other.chargingLeft && + chargingRight == other.chargingRight && + chargingCase == other.chargingCase; + + @override + int get hashCode => + levelLeft.hashCode ^ + levelRight.hashCode ^ + levelCase.hashCode ^ + chargingLeft.hashCode ^ + chargingRight.hashCode ^ + chargingCase.hashCode; } diff --git a/test/unit/headphones_impl/impl_otter_test.dart b/test/unit/headphones_impl/impl_otter_test.dart index 09b03f8..b0c0755 100644 --- a/test/unit/headphones_impl/impl_otter_test.dart +++ b/test/unit/headphones_impl/impl_otter_test.dart @@ -2,25 +2,26 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; -import 'package:freebuddy/headphones/headphones_data_objects.dart'; +import 'package:freebuddy/headphones/framework/anc.dart'; +import 'package:freebuddy/headphones/framework/lrc_battery.dart'; +import 'package:freebuddy/headphones/huawei/freebuds4i_impl.dart'; import 'package:freebuddy/headphones/huawei/mbb.dart'; -import 'package:freebuddy/headphones/huawei/otter/headphones_impl_otter.dart'; import 'package:stream_channel/stream_channel.dart'; void main() { - group("Otter implementation tests", () { + group("FreeBuds 4i implementation tests", () { // test with keyword "info" test if impl reacts to info *from* buds // ones with "set" test if impl sends correct bytes *to* buds late StreamController inputCtrl; late StreamController outputCtrl; late StreamChannel channel; - late HeadphonesImplOtter otter; + late HuaweiFreeBuds4iImpl fb4i; setUp(() { inputCtrl = StreamController.broadcast(); outputCtrl = StreamController(); channel = StreamChannel(inputCtrl.stream, outputCtrl.sink); - otter = HeadphonesImplOtter(channel); + fb4i = HuaweiFreeBuds4iImpl(channel); }); tearDown(() { inputCtrl.close(); @@ -36,7 +37,7 @@ void main() { ); }); test("ANC mode set", () async { - await otter.setAncMode(HeadphonesAncMode.noiseCancel); + await fb4i.setAncMode(AncMode.noiseCancelling); expect( outputCtrl.stream.bytesToList(), emitsThrough([90, 0, 7, 0, 43, 4, 1, 2, 1, 255, 255, 236]), @@ -61,12 +62,12 @@ void main() { inputCtrl.add(c.toPayload()); } expect( - otter.ancMode, + fb4i.ancMode, emitsInOrder([ - HeadphonesAncMode.noiseCancel, - HeadphonesAncMode.off, - HeadphonesAncMode.awareness, - HeadphonesAncMode.awareness, + AncMode.noiseCancelling, + AncMode.off, + AncMode.transparency, + AncMode.transparency, ]), ); }); @@ -77,16 +78,16 @@ void main() { 3: [1, 0, 1] }).toPayload()); expect( - otter.batteryData, - emits(HeadphonesBatteryData(35, 70, 99, true, false, true)), + fb4i.lrcBattery, + emits(const LRCBatteryLevels(35, 70, 99, true, false, true)), ); }); test("Properly closes", () async { expectLater( - otter.ancMode, - emitsInOrder([HeadphonesAncMode.noiseCancel, emitsDone]), + fb4i.ancMode, + emitsInOrder([AncMode.noiseCancelling, emitsDone]), ); - expectLater(otter.batteryData, emitsDone); + expectLater(fb4i.lrcBattery, emitsDone); inputCtrl.add(const MbbCommand(43, 42, { 1: [4, 1] }).toPayload()); From 023f981b757fa4c16b07716f13a4dcc5a26ff8f4 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 23 Feb 2024 00:45:32 +0100 Subject: [PATCH 17/19] move everything to _old to clearly mark that it's shit --- lib/headphones/{ => _old}/headphones_base.dart | 0 lib/headphones/{ => _old}/headphones_data_objects.dart | 0 .../{huawei/otter => _old}/headphones_impl_otter.dart | 8 ++++---- lib/headphones/{ => _old}/headphones_mocks.dart | 0 lib/headphones/huawei/mbb.dart | 2 +- lib/ui/pages/headphones_settings/auto_pause_section.dart | 2 +- lib/ui/pages/headphones_settings/double_tap_section.dart | 4 ++-- lib/ui/pages/headphones_settings/hold_section.dart | 4 ++-- .../pages/home/controls/headphones_controls_widget.dart | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) rename lib/headphones/{ => _old}/headphones_base.dart (100%) rename lib/headphones/{ => _old}/headphones_data_objects.dart (100%) rename lib/headphones/{huawei/otter => _old}/headphones_impl_otter.dart (98%) rename lib/headphones/{ => _old}/headphones_mocks.dart (100%) diff --git a/lib/headphones/headphones_base.dart b/lib/headphones/_old/headphones_base.dart similarity index 100% rename from lib/headphones/headphones_base.dart rename to lib/headphones/_old/headphones_base.dart diff --git a/lib/headphones/headphones_data_objects.dart b/lib/headphones/_old/headphones_data_objects.dart similarity index 100% rename from lib/headphones/headphones_data_objects.dart rename to lib/headphones/_old/headphones_data_objects.dart diff --git a/lib/headphones/huawei/otter/headphones_impl_otter.dart b/lib/headphones/_old/headphones_impl_otter.dart similarity index 98% rename from lib/headphones/huawei/otter/headphones_impl_otter.dart rename to lib/headphones/_old/headphones_impl_otter.dart index c4393e3..30006b2 100644 --- a/lib/headphones/huawei/otter/headphones_impl_otter.dart +++ b/lib/headphones/_old/headphones_impl_otter.dart @@ -4,10 +4,10 @@ import 'dart:typed_data'; import 'package:rxdart/rxdart.dart'; import 'package:stream_channel/stream_channel.dart'; -import '../../../logger.dart'; -import '../../headphones_base.dart'; -import '../../headphones_data_objects.dart'; -import '../mbb.dart'; +import '../../logger.dart'; +import '../huawei/mbb.dart'; +import 'headphones_base.dart'; +import 'headphones_data_objects.dart'; class HeadphonesImplOtter extends HeadphonesBase { final StreamChannel connection; diff --git a/lib/headphones/headphones_mocks.dart b/lib/headphones/_old/headphones_mocks.dart similarity index 100% rename from lib/headphones/headphones_mocks.dart rename to lib/headphones/_old/headphones_mocks.dart diff --git a/lib/headphones/huawei/mbb.dart b/lib/headphones/huawei/mbb.dart index fdec5b1..c5e5134 100644 --- a/lib/headphones/huawei/mbb.dart +++ b/lib/headphones/huawei/mbb.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:crclib/catalog.dart'; -import '../headphones_data_objects.dart'; +import '../_old/headphones_data_objects.dart'; /// Helper class for Mbb protocol used to communicate with headphones class MbbUtils { diff --git a/lib/ui/pages/headphones_settings/auto_pause_section.dart b/lib/ui/pages/headphones_settings/auto_pause_section.dart index 0a3c59e..f034d6e 100644 --- a/lib/ui/pages/headphones_settings/auto_pause_section.dart +++ b/lib/ui/pages/headphones_settings/auto_pause_section.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../../../headphones/headphones_base.dart'; +import '../../../headphones/_old/headphones_base.dart'; import '../../common/list_tile_switch.dart'; class AutoPauseSection extends StatelessWidget { diff --git a/lib/ui/pages/headphones_settings/double_tap_section.dart b/lib/ui/pages/headphones_settings/double_tap_section.dart index 29e25a4..cf41533 100644 --- a/lib/ui/pages/headphones_settings/double_tap_section.dart +++ b/lib/ui/pages/headphones_settings/double_tap_section.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../../../headphones/headphones_base.dart'; -import '../../../headphones/headphones_data_objects.dart'; +import '../../../headphones/_old/headphones_base.dart'; +import '../../../headphones/_old/headphones_data_objects.dart'; import '../../common/list_tile_radio.dart'; import '../../common/list_tile_switch.dart'; import '../disabled.dart'; diff --git a/lib/ui/pages/headphones_settings/hold_section.dart b/lib/ui/pages/headphones_settings/hold_section.dart index d85bfde..d29c55a 100644 --- a/lib/ui/pages/headphones_settings/hold_section.dart +++ b/lib/ui/pages/headphones_settings/hold_section.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../../../headphones/headphones_base.dart'; -import '../../../headphones/headphones_data_objects.dart'; +import '../../../headphones/_old/headphones_base.dart'; +import '../../../headphones/_old/headphones_data_objects.dart'; import '../../common/list_tile_checkbox.dart'; import '../../common/list_tile_switch.dart'; import '../disabled.dart'; diff --git a/lib/ui/pages/home/controls/headphones_controls_widget.dart b/lib/ui/pages/home/controls/headphones_controls_widget.dart index cded15a..0ce4559 100644 --- a/lib/ui/pages/home/controls/headphones_controls_widget.dart +++ b/lib/ui/pages/home/controls/headphones_controls_widget.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../../../../headphones/_old/headphones_base.dart'; import '../../../../headphones/framework/anc.dart'; import '../../../../headphones/framework/bluetooth_headphones.dart'; import '../../../../headphones/framework/headphones_info.dart'; import '../../../../headphones/framework/lrc_battery.dart'; -import '../../../../headphones/headphones_base.dart'; import '../../../theme/layouts.dart'; import 'anc_card.dart'; import 'battery_card.dart'; From 7ee949bbc2ffd714fbdb6c93fe97a62a32006b41 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 23 Feb 2024 17:37:04 +0100 Subject: [PATCH 18/19] lay out initial settings framework don't implement them yet because we want to merge with 3i's --- .../framework/headphones_settings.dart | 18 +++++++++ lib/headphones/huawei/freebuds4i.dart | 9 ++++- lib/headphones/huawei/freebuds4i_impl.dart | 12 ++++++ lib/headphones/huawei/freebuds4i_sim.dart | 16 +++++++- lib/headphones/huawei/settings.dart | 37 +++++++++++++++++++ .../simulators/headphones_settings_sim.dart | 17 +++++++++ 6 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 lib/headphones/framework/headphones_settings.dart create mode 100644 lib/headphones/huawei/settings.dart create mode 100644 lib/headphones/simulators/headphones_settings_sim.dart diff --git a/lib/headphones/framework/headphones_settings.dart b/lib/headphones/framework/headphones_settings.dart new file mode 100644 index 0000000..c223850 --- /dev/null +++ b/lib/headphones/framework/headphones_settings.dart @@ -0,0 +1,18 @@ +import 'package:rxdart/rxdart.dart'; + +/// This class indicates that given headphones have some on-device settings +/// +/// Since pretty much *every* model can have this completely different, I have +/// NO IDEA what to put here. Sure, they often have double tap - but it can be +/// double, triple, or hold, and can change music, or anc, or equalizer... 🀯🀯 +/// +/// Current idea: *all* settings of given model will exist as a single big +/// data class that will be streamed/set from here. Good? Good. +abstract class HeadphonesSettings { + /// Model specific. Not type, not vendor - each model can emit it's own class + /// + /// Yes I know. I'm sorry + ValueStream get settings; + + Future setSettings(T newSettings); +} diff --git a/lib/headphones/huawei/freebuds4i.dart b/lib/headphones/huawei/freebuds4i.dart index 0517958..2dd238a 100644 --- a/lib/headphones/huawei/freebuds4i.dart +++ b/lib/headphones/huawei/freebuds4i.dart @@ -3,7 +3,9 @@ import 'package:rxdart/rxdart.dart'; import '../framework/anc.dart'; import '../framework/bluetooth_headphones.dart'; import '../framework/headphones_info.dart'; +import '../framework/headphones_settings.dart'; import '../framework/lrc_battery.dart'; +import 'settings.dart'; /// Base abstract class of 4i's. It contains static info like vendor names etc, /// but no logic whatsoever. @@ -11,7 +13,12 @@ import '../framework/lrc_battery.dart'; /// It makes both a solid ground for actual implementation (by defining what /// features they implement), and some basic info for easy simulation abstract base class HuaweiFreeBuds4i - implements BluetoothHeadphones, HeadphonesModelInfo, LRCBattery, Anc { + implements + BluetoothHeadphones, + HeadphonesModelInfo, + LRCBattery, + Anc, + HeadphonesSettings { const HuaweiFreeBuds4i(); @override diff --git a/lib/headphones/huawei/freebuds4i_impl.dart b/lib/headphones/huawei/freebuds4i_impl.dart index faadf17..4f1cd81 100644 --- a/lib/headphones/huawei/freebuds4i_impl.dart +++ b/lib/headphones/huawei/freebuds4i_impl.dart @@ -10,6 +10,7 @@ import '../framework/anc.dart'; import '../framework/lrc_battery.dart'; import 'freebuds4i.dart'; import 'mbb.dart'; +import 'settings.dart'; final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { /// Bluetooth serial port that we communicate over @@ -21,6 +22,7 @@ final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { final _bluetoothNameCtrl = BehaviorSubject(); final _lrcBatteryCtrl = BehaviorSubject(); final _ancModeCtrl = BehaviorSubject(); + final _settingsCtrl = BehaviorSubject(); // stream controllers * @@ -52,6 +54,7 @@ final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { _bluetoothNameCtrl.close(); _lrcBatteryCtrl.close(); _ancModeCtrl.close(); + _settingsCtrl.close(); _watchdogStreamSub.cancel(); }); @@ -203,4 +206,13 @@ final class HuaweiFreeBuds4iImpl extends HuaweiFreeBuds4i { } await _sendMbb(comm); } + + @override + ValueStream get settings => _settingsCtrl.stream; + + @override + Future setSettings(newSettings) { + // TODO: implement setSettings + throw UnimplementedError(); + } } diff --git a/lib/headphones/huawei/freebuds4i_sim.dart b/lib/headphones/huawei/freebuds4i_sim.dart index f4b1d82..6b84b58 100644 --- a/lib/headphones/huawei/freebuds4i_sim.dart +++ b/lib/headphones/huawei/freebuds4i_sim.dart @@ -4,16 +4,24 @@ import '../framework/anc.dart'; import '../framework/lrc_battery.dart'; import '../simulators/anc_sim.dart'; import '../simulators/bluetooth_headphones_sim.dart'; +import '../simulators/headphones_settings_sim.dart'; import '../simulators/lrc_battery_sim.dart'; import 'freebuds4i.dart'; +import 'settings.dart'; final class HuaweiFreeBuds4iSim extends HuaweiFreeBuds4i - with BluetoothHeadphonesSim, LRCBatteryAlwaysFullSim, AncSim {} + with + BluetoothHeadphonesSim, + LRCBatteryAlwaysFullSim, + AncSim, + HeadphonesSettingsSim {} /// Class to use as placeholder for Disabled() widget // this is not done with mixins because we may want to fill it with // last-remembered values in future, and we will pretty much override // all of this +// +// ...or not. I just don't know yet 🀷 final class HuaweiFreeBuds4iSimPlaceholder extends HuaweiFreeBuds4i { const HuaweiFreeBuds4iSimPlaceholder(); @@ -37,4 +45,10 @@ final class HuaweiFreeBuds4iSimPlaceholder extends HuaweiFreeBuds4i { @override Future setAncMode(AncMode mode) async {} + + @override + ValueStream get settings => BehaviorSubject(); + + @override + Future setSettings(newSettings) async {} } diff --git a/lib/headphones/huawei/settings.dart b/lib/headphones/huawei/settings.dart new file mode 100644 index 0000000..2ddb54f --- /dev/null +++ b/lib/headphones/huawei/settings.dart @@ -0,0 +1,37 @@ +import '../framework/anc.dart'; + +class HuaweiFreeBuds4iSettings { + // hey hey hay, not only settings are gonna be duplicate spaghetti shithole, + // but all the fields are gonna be nullable too! + final DoubleTap? doubleTapLeft; + final DoubleTap? doubleTapRight; + final Hold? holdBoth; + final Set? holdBothToggledAncModes; + + final bool? autoPause; + + const HuaweiFreeBuds4iSettings({ + this.doubleTapLeft, + this.doubleTapRight, + this.holdBoth, + this.holdBothToggledAncModes, + this.autoPause, + }); +} + +// i don't have idea how to public/privatise those and how to name them +// let's assume that any screen/logic that uses them at all is already +// model-specific so generic names are okay + +enum DoubleTap { + nothing, + voiceAssistant, + playPause, + next, + previous; +} + +enum Hold { + nothing, + cycleAnc; +} diff --git a/lib/headphones/simulators/headphones_settings_sim.dart b/lib/headphones/simulators/headphones_settings_sim.dart new file mode 100644 index 0000000..659440a --- /dev/null +++ b/lib/headphones/simulators/headphones_settings_sim.dart @@ -0,0 +1,17 @@ +import 'package:rxdart/rxdart.dart'; + +import '../framework/headphones_settings.dart'; + +mixin HeadphonesSettingsSim implements HeadphonesSettings { + // No initial data... since we can't know it nor pass it to mixins... + // I thought about making some abstract class for all settings, that would + // require .default()... decided to hold up now... but in future, maybe :) + final _settingsCtrl = BehaviorSubject(); + + @override + ValueStream get settings => _settingsCtrl.stream; + + @override + Future setSettings(T newSettings) async => + _settingsCtrl.add(newSettings); +} From e02da40c44ce38f236998093c05a8969c6cc8316 Mon Sep 17 00:00:00 2001 From: TheLastGimbus Date: Fri, 23 Feb 2024 19:15:23 +0100 Subject: [PATCH 19/19] lay out initial settings framework don't implement them yet because we want to merge with 3i's --- lib/headphones/huawei/freebuds4i_sim.dart | 39 +++++++++-- lib/headphones/huawei/settings.dart | 17 +++++ .../simulators/headphones_settings_sim.dart | 17 ----- .../auto_pause_section.dart | 28 -------- .../headphones_settings_page.dart | 39 +++++++---- .../huawei/auto_pause_section.dart | 31 ++++++++ .../{ => huawei}/double_tap_section.dart | 66 ++++++++--------- .../{ => huawei}/hold_section.dart | 70 +++++++++---------- .../controls/headphones_controls_widget.dart | 26 ++++--- 9 files changed, 181 insertions(+), 152 deletions(-) delete mode 100644 lib/headphones/simulators/headphones_settings_sim.dart delete mode 100644 lib/ui/pages/headphones_settings/auto_pause_section.dart create mode 100644 lib/ui/pages/headphones_settings/huawei/auto_pause_section.dart rename lib/ui/pages/headphones_settings/{ => huawei}/double_tap_section.dart (64%) rename lib/ui/pages/headphones_settings/{ => huawei}/hold_section.dart (52%) diff --git a/lib/headphones/huawei/freebuds4i_sim.dart b/lib/headphones/huawei/freebuds4i_sim.dart index 6b84b58..8e9b7d6 100644 --- a/lib/headphones/huawei/freebuds4i_sim.dart +++ b/lib/headphones/huawei/freebuds4i_sim.dart @@ -4,17 +4,44 @@ import '../framework/anc.dart'; import '../framework/lrc_battery.dart'; import '../simulators/anc_sim.dart'; import '../simulators/bluetooth_headphones_sim.dart'; -import '../simulators/headphones_settings_sim.dart'; import '../simulators/lrc_battery_sim.dart'; import 'freebuds4i.dart'; import 'settings.dart'; final class HuaweiFreeBuds4iSim extends HuaweiFreeBuds4i - with - BluetoothHeadphonesSim, - LRCBatteryAlwaysFullSim, - AncSim, - HeadphonesSettingsSim {} + with BluetoothHeadphonesSim, LRCBatteryAlwaysFullSim, AncSim { + // ehhhhhh... + + final _settingsCtrl = BehaviorSubject.seeded( + const HuaweiFreeBuds4iSettings( + doubleTapLeft: DoubleTap.playPause, + doubleTapRight: DoubleTap.playPause, + holdBoth: Hold.cycleAnc, + holdBothToggledAncModes: { + AncMode.noiseCancelling, + AncMode.off, + AncMode.transparency, + }, + autoPause: true, + ), + ); + + @override + ValueStream get settings => _settingsCtrl.stream; + + @override + Future setSettings(HuaweiFreeBuds4iSettings newSettings) async { + _settingsCtrl.add( + _settingsCtrl.value.copyWith( + doubleTapLeft: newSettings.doubleTapLeft, + doubleTapRight: newSettings.doubleTapRight, + holdBoth: newSettings.holdBoth, + holdBothToggledAncModes: newSettings.holdBothToggledAncModes, + autoPause: newSettings.autoPause, + ), + ); + } +} /// Class to use as placeholder for Disabled() widget // this is not done with mixins because we may want to fill it with diff --git a/lib/headphones/huawei/settings.dart b/lib/headphones/huawei/settings.dart index 2ddb54f..f5ec5e1 100644 --- a/lib/headphones/huawei/settings.dart +++ b/lib/headphones/huawei/settings.dart @@ -17,6 +17,23 @@ class HuaweiFreeBuds4iSettings { this.holdBothToggledAncModes, this.autoPause, }); + + // don't want to use codegen *yet* + HuaweiFreeBuds4iSettings copyWith({ + DoubleTap? doubleTapLeft, + DoubleTap? doubleTapRight, + Hold? holdBoth, + Set? holdBothToggledAncModes, + bool? autoPause, + }) => + HuaweiFreeBuds4iSettings( + doubleTapLeft: doubleTapLeft ?? this.doubleTapLeft, + doubleTapRight: doubleTapRight ?? this.doubleTapRight, + holdBoth: holdBoth ?? this.holdBoth, + holdBothToggledAncModes: + holdBothToggledAncModes ?? this.holdBothToggledAncModes, + autoPause: autoPause ?? this.autoPause, + ); } // i don't have idea how to public/privatise those and how to name them diff --git a/lib/headphones/simulators/headphones_settings_sim.dart b/lib/headphones/simulators/headphones_settings_sim.dart deleted file mode 100644 index 659440a..0000000 --- a/lib/headphones/simulators/headphones_settings_sim.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:rxdart/rxdart.dart'; - -import '../framework/headphones_settings.dart'; - -mixin HeadphonesSettingsSim implements HeadphonesSettings { - // No initial data... since we can't know it nor pass it to mixins... - // I thought about making some abstract class for all settings, that would - // require .default()... decided to hold up now... but in future, maybe :) - final _settingsCtrl = BehaviorSubject(); - - @override - ValueStream get settings => _settingsCtrl.stream; - - @override - Future setSettings(T newSettings) async => - _settingsCtrl.add(newSettings); -} diff --git a/lib/ui/pages/headphones_settings/auto_pause_section.dart b/lib/ui/pages/headphones_settings/auto_pause_section.dart deleted file mode 100644 index f034d6e..0000000 --- a/lib/ui/pages/headphones_settings/auto_pause_section.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import '../../../headphones/_old/headphones_base.dart'; -import '../../common/list_tile_switch.dart'; - -class AutoPauseSection extends StatelessWidget { - final HeadphonesBase headphones; - - const AutoPauseSection({super.key, required this.headphones}); - - @override - Widget build(BuildContext context) { - final l = AppLocalizations.of(context)!; - return StreamBuilder( - stream: headphones.autoPause, - initialData: headphones.autoPause.valueOrNull ?? false, - builder: (_, snapshot) { - return ListTileSwitch( - title: Text(l.autoPause), - subtitle: Text(l.autoPauseDesc), - value: snapshot.data!, - onChanged: (newVal) => headphones.setAutoPause(newVal), - ); - }, - ); - } -} diff --git a/lib/ui/pages/headphones_settings/headphones_settings_page.dart b/lib/ui/pages/headphones_settings/headphones_settings_page.dart index e0de77e..1e7d0c4 100644 --- a/lib/ui/pages/headphones_settings/headphones_settings_page.dart +++ b/lib/ui/pages/headphones_settings/headphones_settings_page.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../../../headphones/framework/headphones_settings.dart'; +import '../../../headphones/huawei/settings.dart'; import '../../common/headphones_connection_ensuring_overlay.dart'; +import 'huawei/auto_pause_section.dart'; +import 'huawei/double_tap_section.dart'; +import 'huawei/hold_section.dart'; class HeadphonesSettingsPage extends StatelessWidget { const HeadphonesSettingsPage({super.key}); @@ -13,22 +18,28 @@ class HeadphonesSettingsPage extends StatelessWidget { appBar: AppBar(title: Text(l.pageHeadphonesSettingsTitle)), body: Center( child: HeadphonesConnectionEnsuringOverlay( - builder: (_, h) { - return ListView( - children: [ - // TODO MIGRATION: hp settings not yet implemented - const Text('HP Settings not yet implemented'), - // AutoPauseSection(headphones: h), - // const Divider(indent: 16, endIndent: 16), - // DoubleTapSection(headphones: h), - // const Divider(indent: 16, endIndent: 16), - // HoldSection(headphones: h), - // const SizedBox(height: 64), - ], - ); - }, + builder: (_, h) => + ListView(children: widgetsForModel(h as HeadphonesSettings)), ), ), ); } } + +// this is shitty. and we don't want this. not here. +// ... +// but i have no better idea for now :))))) +List widgetsForModel(HeadphonesSettings settings) { + if (settings is HeadphonesSettings) { + return [ + AutoPauseSection(settings), + const Divider(indent: 16, endIndent: 16), + DoubleTapSection(settings), + const Divider(indent: 16, endIndent: 16), + HoldSection(settings), + const SizedBox(height: 64), + ]; + } else { + throw "You shouldn't be on this screen if you don't have settings!"; + } +} diff --git a/lib/ui/pages/headphones_settings/huawei/auto_pause_section.dart b/lib/ui/pages/headphones_settings/huawei/auto_pause_section.dart new file mode 100644 index 0000000..e2dfb23 --- /dev/null +++ b/lib/ui/pages/headphones_settings/huawei/auto_pause_section.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../../../headphones/framework/headphones_settings.dart'; +import '../../../../headphones/huawei/settings.dart'; +import '../../../common/list_tile_switch.dart'; + +class AutoPauseSection extends StatelessWidget { + final HeadphonesSettings headphones; + + const AutoPauseSection(this.headphones, {super.key}); + + @override + Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; + return StreamBuilder( + stream: headphones.settings.map((s) => s.autoPause), + initialData: false, + builder: (_, snap) { + return ListTileSwitch( + title: Text(l.autoPause), + subtitle: Text(l.autoPauseDesc), + value: snap.data ?? false, + onChanged: (newVal) => headphones.setSettings( + HuaweiFreeBuds4iSettings(autoPause: newVal), + ), + ); + }, + ); + } +} diff --git a/lib/ui/pages/headphones_settings/double_tap_section.dart b/lib/ui/pages/headphones_settings/huawei/double_tap_section.dart similarity index 64% rename from lib/ui/pages/headphones_settings/double_tap_section.dart rename to lib/ui/pages/headphones_settings/huawei/double_tap_section.dart index cf41533..0d67bb0 100644 --- a/lib/ui/pages/headphones_settings/double_tap_section.dart +++ b/lib/ui/pages/headphones_settings/huawei/double_tap_section.dart @@ -1,31 +1,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../../../headphones/_old/headphones_base.dart'; -import '../../../headphones/_old/headphones_data_objects.dart'; -import '../../common/list_tile_radio.dart'; -import '../../common/list_tile_switch.dart'; -import '../disabled.dart'; +import '../../../../headphones/framework/headphones_settings.dart'; +import '../../../../headphones/huawei/settings.dart'; +import '../../../common/list_tile_radio.dart'; +import '../../../common/list_tile_switch.dart'; +import '../../disabled.dart'; class DoubleTapSection extends StatelessWidget { - final HeadphonesBase headphones; + final HeadphonesSettings headphones; - const DoubleTapSection({super.key, required this.headphones}); + const DoubleTapSection(this.headphones, {super.key}); @override Widget build(BuildContext context) { final t = Theme.of(context); final tt = t.textTheme; final l = AppLocalizations.of(context)!; - return StreamBuilder( - stream: headphones.gestureSettings, - initialData: headphones.gestureSettings.valueOrNull ?? - const HeadphonesGestureSettings(), - builder: (context, snapshot) { - final gs = snapshot.data!; + return StreamBuilder( + stream: headphones.settings + .map((s) => (l: s.doubleTapLeft, r: s.doubleTapRight)), + initialData: (l: null, r: null), + builder: (context, snap) { + final dt = snap.data!; final enabled = - (gs.doubleTapLeft != HeadphonesGestureDoubleTap.nothing || - gs.doubleTapRight != HeadphonesGestureDoubleTap.nothing); + (dt.l != DoubleTap.nothing || dt.r != DoubleTap.nothing); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -34,14 +33,9 @@ class DoubleTapSection extends StatelessWidget { subtitle: Text(l.pageHeadphonesSettingsDoubleTapDesc), value: enabled, onChanged: (newVal) { - final g = newVal - ? HeadphonesGestureDoubleTap.playPause - : HeadphonesGestureDoubleTap.nothing; - headphones.setGestureSettings( - HeadphonesGestureSettings( - doubleTapLeft: g, - doubleTapRight: g, - ), + final g = newVal ? DoubleTap.playPause : DoubleTap.nothing; + headphones.setSettings( + HuaweiFreeBuds4iSettings(doubleTapLeft: g, doubleTapRight: g), ); }, ), @@ -57,10 +51,10 @@ class DoubleTapSection extends StatelessWidget { l.pageHeadphonesSettingsLeftBud, style: tt.titleMedium, ), - value: gs.doubleTapLeft, + value: dt.l, onChanged: enabled - ? (g) => headphones.setGestureSettings( - HeadphonesGestureSettings(doubleTapLeft: g), + ? (g) => headphones.setSettings( + HuaweiFreeBuds4iSettings(doubleTapLeft: g), ) : null, ), @@ -71,10 +65,10 @@ class DoubleTapSection extends StatelessWidget { l.pageHeadphonesSettingsRightBud, style: tt.titleMedium, ), - value: gs.doubleTapRight, + value: dt.r, onChanged: enabled - ? (g) => headphones.setGestureSettings( - HeadphonesGestureSettings(doubleTapRight: g), + ? (g) => headphones.setSettings( + HuaweiFreeBuds4iSettings(doubleTapRight: g), ) : null, ), @@ -92,8 +86,8 @@ class DoubleTapSection extends StatelessWidget { class _DoubleTapSetting extends StatelessWidget { final Widget? title; - final HeadphonesGestureDoubleTap? value; - final void Function(HeadphonesGestureDoubleTap?)? onChanged; + final DoubleTap? value; + final void Function(DoubleTap?)? onChanged; const _DoubleTapSetting({ required this.value, @@ -117,35 +111,35 @@ class _DoubleTapSetting extends StatelessWidget { ], ListTileRadio( title: Text(l.pageHeadphonesSettingsDoubleTapPlayPause), - value: HeadphonesGestureDoubleTap.playPause, + value: DoubleTap.playPause, dense: true, groupValue: value, onChanged: onChanged, ), ListTileRadio( title: Text(l.pageHeadphonesSettingsDoubleTapNextSong), - value: HeadphonesGestureDoubleTap.next, + value: DoubleTap.next, dense: true, groupValue: value, onChanged: onChanged, ), ListTileRadio( title: Text(l.pageHeadphonesSettingsDoubleTapPrevSong), - value: HeadphonesGestureDoubleTap.previous, + value: DoubleTap.previous, dense: true, groupValue: value, onChanged: onChanged, ), ListTileRadio( title: Text(l.pageHeadphonesSettingsDoubleTapAssist), - value: HeadphonesGestureDoubleTap.voiceAssistant, + value: DoubleTap.voiceAssistant, dense: true, groupValue: value, onChanged: onChanged, ), ListTileRadio( title: Text(l.pageHeadphonesSettingsDoubleTapNone), - value: HeadphonesGestureDoubleTap.nothing, + value: DoubleTap.nothing, dense: true, groupValue: value, onChanged: onChanged, diff --git a/lib/ui/pages/headphones_settings/hold_section.dart b/lib/ui/pages/headphones_settings/huawei/hold_section.dart similarity index 52% rename from lib/ui/pages/headphones_settings/hold_section.dart rename to lib/ui/pages/headphones_settings/huawei/hold_section.dart index d29c55a..03e5d80 100644 --- a/lib/ui/pages/headphones_settings/hold_section.dart +++ b/lib/ui/pages/headphones_settings/huawei/hold_section.dart @@ -1,48 +1,46 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../../../headphones/_old/headphones_base.dart'; -import '../../../headphones/_old/headphones_data_objects.dart'; -import '../../common/list_tile_checkbox.dart'; -import '../../common/list_tile_switch.dart'; -import '../disabled.dart'; +import '../../../../headphones/framework/anc.dart'; +import '../../../../headphones/framework/headphones_settings.dart'; +import '../../../../headphones/huawei/settings.dart'; +import '../../../common/list_tile_checkbox.dart'; +import '../../../common/list_tile_switch.dart'; +import '../../disabled.dart'; class HoldSection extends StatelessWidget { - final HeadphonesBase headphones; + final HeadphonesSettings headphones; - const HoldSection({super.key, required this.headphones}); + const HoldSection(this.headphones, {super.key}); @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; - return StreamBuilder( - stream: headphones.gestureSettings, - initialData: headphones.gestureSettings.valueOrNull ?? - const HeadphonesGestureSettings(), - builder: (context, snapshot) { - final gs = snapshot.data!; - final enabled = gs.holdBoth == HeadphonesGestureHold.cycleAnc; + return StreamBuilder( + stream: headphones.settings + .map((s) => (holdBoth: s.holdBoth, anc: s.holdBothToggledAncModes)), + initialData: (holdBoth: null, anc: null), + builder: (context, snap) { + final gs = snap.data!; + final enabled = gs.holdBoth == Hold.cycleAnc; return Column( children: [ ListTileSwitch( title: Text(l.pageHeadphonesSettingsHold), subtitle: Text(l.pageHeadphonesSettingsHoldDesc), value: enabled, - onChanged: (newVal) => headphones.setGestureSettings( - HeadphonesGestureSettings( - holdBoth: newVal - ? HeadphonesGestureHold.cycleAnc - : HeadphonesGestureHold.nothing, + onChanged: (newVal) => headphones.setSettings( + HuaweiFreeBuds4iSettings( + holdBoth: newVal ? Hold.cycleAnc : Hold.nothing, ), ), ), Disabled( disabled: !enabled, child: _HoldSettingsCard( - enabledModes: MapEntry(snapshot.data!.holdBoth, - snapshot.data!.holdBothToggledAncModes), - onChanged: (m) => headphones.setGestureSettings( - HeadphonesGestureSettings( + enabledModes: MapEntry(gs.holdBoth, gs.anc), + onChanged: (m) => headphones.setSettings( + HuaweiFreeBuds4iSettings( holdBoth: m.key, holdBothToggledAncModes: m.value, ), @@ -57,23 +55,21 @@ class HoldSection extends StatelessWidget { } class _HoldSettingsCard extends StatelessWidget { - final MapEntry?> enabledModes; - final void Function( - MapEntry?>)? onChanged; + final MapEntry?> enabledModes; + final void Function(MapEntry?>)? onChanged; const _HoldSettingsCard({required this.enabledModes, this.onChanged}); - bool checkboxChecked(HeadphonesAncMode mode) => + bool checkboxChecked(AncMode mode) => enabledModes.value?.contains(mode) ?? false; - bool checkboxEnabled(bool enabled) => - (enabledModes.key == HeadphonesGestureHold.cycleAnc && - onChanged != null && - enabledModes.value != null && - // either all modes are enabled, or this is the disabled one - (enabledModes.value!.length > 2 || !enabled)); + bool checkboxEnabled(bool enabled) => (enabledModes.key == Hold.cycleAnc && + onChanged != null && + enabledModes.value != null && + // either all modes are enabled, or this is the disabled one + (enabledModes.value!.length > 2 || !enabled)); - Widget modeCheckbox(String title, String desc, HeadphonesAncMode mode) { + Widget modeCheckbox(String title, String desc, AncMode mode) { final checked = checkboxChecked(mode); return ListTileCheckbox( title: Text(title), @@ -105,17 +101,17 @@ class _HoldSettingsCard extends StatelessWidget { modeCheckbox( l.ancNoiseCancel, l.ancNoiseCancelDesc, - HeadphonesAncMode.noiseCancel, + AncMode.noiseCancelling, ), modeCheckbox( l.ancOff, l.ancOffDesc, - HeadphonesAncMode.off, + AncMode.off, ), modeCheckbox( l.ancAwareness, l.ancAwarenessDesc, - HeadphonesAncMode.awareness, + AncMode.transparency, ), ], ), diff --git a/lib/ui/pages/home/controls/headphones_controls_widget.dart b/lib/ui/pages/home/controls/headphones_controls_widget.dart index 0ce4559..4b632a6 100644 --- a/lib/ui/pages/home/controls/headphones_controls_widget.dart +++ b/lib/ui/pages/home/controls/headphones_controls_widget.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../../../../headphones/_old/headphones_base.dart'; import '../../../../headphones/framework/anc.dart'; import '../../../../headphones/framework/bluetooth_headphones.dart'; import '../../../../headphones/framework/headphones_info.dart'; +import '../../../../headphones/framework/headphones_settings.dart'; import '../../../../headphones/framework/lrc_battery.dart'; import '../../../theme/layouts.dart'; import 'anc_card.dart'; @@ -51,11 +51,11 @@ class HeadphonesControlsWidget extends StatelessWidget { else // TODO: This is ugly. Very const Expanded(child: Icon(Icons.headphones, size: 64)), - // TODO MIGRATION: hp settings not yet implemented - // Align( - // alignment: Alignment.centerRight, - // child: _HeadphonesSettingsButton(headphones), - // ), + if (headphones is HeadphonesSettings) + const Align( + alignment: Alignment.centerRight, + child: _HeadphonesSettingsButton(), + ), if (headphones is LRCBattery) BatteryCard(headphones as LRCBattery), if (headphones is Anc) AncCard(headphones as Anc), @@ -87,11 +87,11 @@ class HeadphonesControlsWidget extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - // TODO MIGRATION: hp settings not yet implemented - // Align( - // alignment: Alignment.centerRight, - // child: _HeadphonesSettingsButton(headphones), - // ), + if (headphones is HeadphonesSettings) + const Align( + alignment: Alignment.centerRight, + child: _HeadphonesSettingsButton(), + ), if (headphones is LRCBattery) BatteryCard(headphones as LRCBattery), if (headphones is Anc) AncCard(headphones as Anc), @@ -107,9 +107,7 @@ class HeadphonesControlsWidget extends StatelessWidget { /// Simple button leading to headphones settings page class _HeadphonesSettingsButton extends StatelessWidget { - final HeadphonesBase headphones; - - const _HeadphonesSettingsButton(this.headphones); + const _HeadphonesSettingsButton(); @override Widget build(BuildContext context) {