system_controller.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import 'dart:async';
  2. import 'package:flutter/foundation.dart';
  3. import '../../data/models/media_playback.dart';
  4. import '../../data/models/system_status.dart';
  5. import '../../data/services/native_bridge.dart';
  6. /// Polls the system for battery, Wi-Fi, media volume and media activity.
  7. ///
  8. /// Polling keeps the app free of long-lived broadcast receivers and works
  9. /// identically on Android 10 and 15. Timers are stopped while the launcher is
  10. /// in the background.
  11. class SystemController extends ChangeNotifier {
  12. SystemController(this._platform);
  13. final LauncherPlatform _platform;
  14. SystemStatus _status = const SystemStatus();
  15. VolumeInfo _volume = const VolumeInfo();
  16. Timer? _volumeTimer;
  17. Timer? _statusTimer;
  18. bool _active = false;
  19. bool _volumeBusy = false;
  20. bool _statusBusy = false;
  21. bool _musicBusy = false;
  22. bool _musicPlaying = false;
  23. bool _volumeSetting = false;
  24. bool _showVolumeDiagnostics = false;
  25. int _volumeWriteId = 0;
  26. int? _lastVolumeTarget;
  27. VolumeInfo? _lastVolumeRead;
  28. MediaPlayback _media = const MediaPlayback();
  29. bool _disposed = false;
  30. int _lastNonZeroVolume = 0;
  31. /// How long a fresh write may take before a poll decides the read-back is
  32. /// not tracking it. Car units often answer a write a few hundred ms later.
  33. static const Duration _settleWindow = Duration(milliseconds: 1600);
  34. int? _pendingIndex;
  35. int? _pendingBaseline;
  36. DateTime? _pendingSince;
  37. int _pendingMisses = 0;
  38. bool _readBackStale = false;
  39. int? _staleIndex;
  40. SystemStatus get status => _status;
  41. VolumeInfo get volume => _volume;
  42. int get volumePercent => _volume.percent;
  43. bool get isMuted => _volume.index <= 0;
  44. bool get isVolumeFixed => !_volume.isControllable;
  45. bool get isMusicPlaying => _musicPlaying;
  46. /// True when the car reports the same level no matter what was written.
  47. /// The card still shows the requested level, marked with a question mark.
  48. bool get volumeUnconfirmed => _readBackStale;
  49. String? get volumeDiagnostics {
  50. if (!_showVolumeDiagnostics) return null;
  51. final write = _lastVolumeTarget == null
  52. ? 'none'
  53. : '${_lastVolumeTarget!} -> ${_lastVolumeRead?.index ?? '?'}';
  54. final streams = _volume.streams.entries
  55. .map((entry) => '${entry.key} ${entry.value}')
  56. .join(' ');
  57. final state = [
  58. 'raw ${_volume.index}/${_volume.max}',
  59. 'min ${_volume.min}',
  60. 'fixed ${_volume.fixed}',
  61. 'source ${_volume.source}',
  62. if (_volume.vendor.isNotEmpty) 'vendor ${_volume.vendor}',
  63. if (_readBackStale) 'unconfirmed',
  64. ].join(' ');
  65. return '$state\n'
  66. 'write $write\n'
  67. '${streams.isEmpty ? 'streams unavailable' : streams}';
  68. }
  69. void toggleVolumeDiagnostics() {
  70. _showVolumeDiagnostics = !_showVolumeDiagnostics;
  71. _notify();
  72. }
  73. MediaPlayback get media => _media;
  74. bool get hasMediaAccess => _media.hasAccess;
  75. Future<void> start() async {
  76. if (_active) return;
  77. _active = true;
  78. await refreshAll();
  79. _volumeTimer = Timer.periodic(const Duration(milliseconds: 900), (_) {
  80. _pollVolume();
  81. _pollMusic();
  82. });
  83. _statusTimer = Timer.periodic(
  84. const Duration(seconds: 20),
  85. (_) => _pollStatus(),
  86. );
  87. }
  88. void stop() {
  89. _active = false;
  90. _volumeTimer?.cancel();
  91. _volumeTimer = null;
  92. _statusTimer?.cancel();
  93. _statusTimer = null;
  94. }
  95. Future<void> refreshAll() async {
  96. await Future.wait([_pollVolume(), _pollStatus(), _pollMusic()]);
  97. }
  98. Future<void> _pollVolume() async {
  99. if (_volumeBusy || _volumeSetting || !_active) return;
  100. _volumeBusy = true;
  101. try {
  102. final next = await _platform.getVolume();
  103. if (!_active) return;
  104. if (_lastVolumeTarget != null) _lastVolumeRead = next;
  105. final pending = _pendingIndex;
  106. if (pending != null) {
  107. if (next.index == pending) {
  108. _confirmVolume(next);
  109. return;
  110. }
  111. final baseline = _pendingBaseline;
  112. final since = _pendingSince;
  113. final elapsed = since == null
  114. ? _settleWindow
  115. : DateTime.now().difference(since);
  116. if (baseline != null && next.index == baseline) {
  117. if (elapsed >= _settleWindow) {
  118. _pendingMisses++;
  119. if (_pendingMisses >= 2) {
  120. _readBackStale = true;
  121. _staleIndex = baseline;
  122. }
  123. }
  124. _mergeMetadata(next);
  125. return;
  126. }
  127. // The car may still be applying an earlier step of the same gesture.
  128. if (elapsed < _settleWindow) {
  129. _mergeMetadata(next);
  130. return;
  131. }
  132. // Something else moved the volume (hardware keys, another app).
  133. _adoptVolume(next);
  134. return;
  135. }
  136. if (_readBackStale) {
  137. if (next.index == _staleIndex) {
  138. _mergeMetadata(next);
  139. return;
  140. }
  141. _readBackStale = false;
  142. _staleIndex = null;
  143. }
  144. _adoptVolume(next);
  145. } finally {
  146. _volumeBusy = false;
  147. }
  148. }
  149. bool _sameVolume(VolumeInfo a, VolumeInfo b) {
  150. return a.index == b.index &&
  151. a.max == b.max &&
  152. a.min == b.min &&
  153. a.fixed == b.fixed &&
  154. a.source == b.source &&
  155. a.vendor == b.vendor &&
  156. mapEquals(a.streams, b.streams);
  157. }
  158. void _clearPending() {
  159. _pendingIndex = null;
  160. _pendingBaseline = null;
  161. _pendingSince = null;
  162. _pendingMisses = 0;
  163. }
  164. void _confirmVolume(VolumeInfo next) {
  165. _clearPending();
  166. _readBackStale = false;
  167. _staleIndex = null;
  168. _applyVolume(next);
  169. }
  170. void _adoptVolume(VolumeInfo next) {
  171. _clearPending();
  172. _readBackStale = false;
  173. _staleIndex = null;
  174. _applyVolume(next);
  175. }
  176. void _applyVolume(VolumeInfo next) {
  177. if (next.index > 0) _lastNonZeroVolume = next.index;
  178. if (!_sameVolume(next, _volume)) {
  179. _volume = next;
  180. _notify();
  181. }
  182. }
  183. /// Keeps the level the user chose while refreshing the metadata around it.
  184. void _mergeMetadata(VolumeInfo next) {
  185. final max = next.max <= 0 ? _volume.max : next.max;
  186. final min = next.min.clamp(0, max);
  187. final index = _volume.index.clamp(min, max);
  188. final merged = VolumeInfo(
  189. index: index,
  190. max: max,
  191. min: min,
  192. fixed: next.fixed,
  193. streams: next.streams,
  194. source: next.source,
  195. vendor: next.vendor,
  196. );
  197. if (!_sameVolume(merged, _volume)) {
  198. _volume = merged;
  199. _notify();
  200. }
  201. }
  202. Future<void> _pollMusic() async {
  203. if (_musicBusy || !_active) return;
  204. _musicBusy = true;
  205. try {
  206. final media = await _platform.getMediaState();
  207. final fallback = await _platform.isMusicPlaying();
  208. final playing = media.hasAccess && media.hasSession
  209. ? media.playing
  210. : fallback;
  211. final changed = media != _media || playing != _musicPlaying;
  212. _media = media;
  213. _musicPlaying = playing;
  214. if (changed) _notify();
  215. } finally {
  216. _musicBusy = false;
  217. }
  218. }
  219. Future<void> _pollStatus() async {
  220. if (_statusBusy) return;
  221. _statusBusy = true;
  222. try {
  223. final next = await _platform.getStatus();
  224. if (next.batteryLevel != _status.batteryLevel ||
  225. next.charging != _status.charging ||
  226. next.wifi != _status.wifi) {
  227. _status = next;
  228. _notify();
  229. }
  230. } finally {
  231. _statusBusy = false;
  232. }
  233. }
  234. Future<void> setVolume(int index) async {
  235. final max = _volume.max <= 0 ? 15 : _volume.max;
  236. final min = _volume.min.clamp(0, max);
  237. final clamped = index.clamp(min, max);
  238. if (isVolumeFixed) {
  239. _lastVolumeTarget = clamped;
  240. _lastVolumeRead = _volume;
  241. return;
  242. }
  243. final before = _volume.index;
  244. if (clamped == before && _pendingIndex == null) return;
  245. final writeId = ++_volumeWriteId;
  246. _lastVolumeTarget = clamped;
  247. if (_pendingIndex == null) {
  248. _pendingBaseline = _lastVolumeRead?.index ?? before;
  249. }
  250. _pendingIndex = clamped;
  251. _pendingSince = DateTime.now();
  252. _pendingMisses = 0;
  253. _volumeSetting = true;
  254. _volume = _volume.copyWith(index: clamped);
  255. if (clamped > 0) _lastNonZeroVolume = clamped;
  256. _notify();
  257. try {
  258. await _platform.setVolume(clamped);
  259. if (writeId != _volumeWriteId) return;
  260. final actual = await _platform.getVolume();
  261. if (writeId != _volumeWriteId) return;
  262. _lastVolumeRead = actual;
  263. if (actual.index == clamped) {
  264. _confirmVolume(actual);
  265. } else {
  266. // The car has not reported the new level yet. Keep what the user set;
  267. // the next polls either confirm it or mark it unconfirmed instead of
  268. // snapping the slider back to the old value.
  269. _mergeMetadata(actual);
  270. }
  271. } finally {
  272. if (writeId == _volumeWriteId) _volumeSetting = false;
  273. }
  274. }
  275. Future<void> adjustVolume(int delta) => setVolume(_volume.index + delta);
  276. Future<bool> openMediaAccessSettings() {
  277. return _platform.openNotificationAccessSettings();
  278. }
  279. /// Sends a pause key immediately. The next poll reconciles the real state.
  280. Future<void> pauseMusic() async {
  281. try {
  282. await _platform.sendMediaKey('pause');
  283. } catch (_) {
  284. // Media keys are best-effort; the next poll still reflects reality.
  285. }
  286. if (_musicPlaying) {
  287. _musicPlaying = false;
  288. _notify();
  289. }
  290. }
  291. /// Tries to resume a paused session. Returns false when nothing started, so
  292. /// the caller can open the configured music app instead.
  293. Future<bool> playMusic() async {
  294. try {
  295. final sent = await _platform.sendMediaKey('play');
  296. if (!sent) return false;
  297. await Future<void>.delayed(const Duration(milliseconds: 450));
  298. final media = await _platform.getMediaState();
  299. final fallback = await _platform.isMusicPlaying();
  300. final playing = media.hasAccess && media.hasSession
  301. ? media.playing
  302. : fallback;
  303. final changed = media != _media || playing != _musicPlaying;
  304. _media = media;
  305. _musicPlaying = playing;
  306. if (changed) _notify();
  307. return playing;
  308. } catch (_) {
  309. return false;
  310. }
  311. }
  312. Future<void> toggleMute() async {
  313. if (isVolumeFixed) return;
  314. if (_volume.index > 0) {
  315. _lastNonZeroVolume = _volume.index;
  316. await setVolume(0);
  317. } else {
  318. final restore = _lastNonZeroVolume > 0
  319. ? _lastNonZeroVolume
  320. : (_volume.max <= 0
  321. ? 1
  322. : (_volume.max / 2).round().clamp(1, _volume.max));
  323. await setVolume(restore);
  324. }
  325. }
  326. void _notify() {
  327. if (!_disposed) notifyListeners();
  328. }
  329. @override
  330. void dispose() {
  331. _disposed = true;
  332. stop();
  333. super.dispose();
  334. }
  335. }