system_controller.dart 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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. MediaPlayback _media = const MediaPlayback();
  24. bool _disposed = false;
  25. int _lastNonZeroVolume = 0;
  26. SystemStatus get status => _status;
  27. VolumeInfo get volume => _volume;
  28. int get volumePercent => _volume.percent;
  29. bool get isMuted => _volume.index <= 0;
  30. bool get isVolumeFixed => _volume.fixed;
  31. bool get isMusicPlaying => _musicPlaying;
  32. MediaPlayback get media => _media;
  33. bool get hasMediaAccess => _media.hasAccess;
  34. Future<void> start() async {
  35. if (_active) return;
  36. _active = true;
  37. await refreshAll();
  38. _volumeTimer = Timer.periodic(const Duration(milliseconds: 900), (_) {
  39. _pollVolume();
  40. _pollMusic();
  41. });
  42. _statusTimer = Timer.periodic(
  43. const Duration(seconds: 20),
  44. (_) => _pollStatus(),
  45. );
  46. }
  47. void stop() {
  48. _active = false;
  49. _volumeTimer?.cancel();
  50. _volumeTimer = null;
  51. _statusTimer?.cancel();
  52. _statusTimer = null;
  53. }
  54. Future<void> refreshAll() async {
  55. await Future.wait([_pollVolume(), _pollStatus(), _pollMusic()]);
  56. }
  57. Future<void> _pollVolume() async {
  58. if (_volumeBusy || !_active) return;
  59. _volumeBusy = true;
  60. try {
  61. final next = await _platform.getVolume();
  62. if (next.index != _volume.index ||
  63. next.max != _volume.max ||
  64. next.fixed != _volume.fixed) {
  65. _volume = next;
  66. if (next.index > 0) _lastNonZeroVolume = next.index;
  67. _notify();
  68. }
  69. } finally {
  70. _volumeBusy = false;
  71. }
  72. }
  73. Future<void> _pollMusic() async {
  74. if (_musicBusy || !_active) return;
  75. _musicBusy = true;
  76. try {
  77. final media = await _platform.getMediaState();
  78. final fallback = await _platform.isMusicPlaying();
  79. final playing = media.hasAccess && media.hasSession
  80. ? media.playing
  81. : fallback;
  82. final changed = media != _media || playing != _musicPlaying;
  83. _media = media;
  84. _musicPlaying = playing;
  85. if (changed) _notify();
  86. } finally {
  87. _musicBusy = false;
  88. }
  89. }
  90. Future<void> _pollStatus() async {
  91. if (_statusBusy) return;
  92. _statusBusy = true;
  93. try {
  94. final next = await _platform.getStatus();
  95. if (next.batteryLevel != _status.batteryLevel ||
  96. next.charging != _status.charging ||
  97. next.wifi != _status.wifi) {
  98. _status = next;
  99. _notify();
  100. }
  101. } finally {
  102. _statusBusy = false;
  103. }
  104. }
  105. Future<void> setVolume(int index) async {
  106. if (_volume.fixed) return;
  107. final max = _volume.max <= 0 ? 15 : _volume.max;
  108. final clamped = index.clamp(0, max);
  109. _volume = VolumeInfo(index: clamped, max: max);
  110. if (clamped > 0) _lastNonZeroVolume = clamped;
  111. _notify();
  112. await _platform.setVolume(clamped);
  113. }
  114. Future<void> adjustVolume(int delta) => setVolume(_volume.index + delta);
  115. Future<bool> openMediaAccessSettings() {
  116. return _platform.openNotificationAccessSettings();
  117. }
  118. /// Sends a pause key immediately. The next poll reconciles the real state.
  119. Future<void> pauseMusic() async {
  120. try {
  121. await _platform.sendMediaKey('pause');
  122. } catch (_) {
  123. // Media keys are best-effort; the next poll still reflects reality.
  124. }
  125. if (_musicPlaying) {
  126. _musicPlaying = false;
  127. _notify();
  128. }
  129. }
  130. /// Tries to resume a paused session. Returns false when nothing started, so
  131. /// the caller can open the configured music app instead.
  132. Future<bool> playMusic() async {
  133. try {
  134. final sent = await _platform.sendMediaKey('play');
  135. if (!sent) return false;
  136. await Future<void>.delayed(const Duration(milliseconds: 450));
  137. final media = await _platform.getMediaState();
  138. final fallback = await _platform.isMusicPlaying();
  139. final playing = media.hasAccess && media.hasSession
  140. ? media.playing
  141. : fallback;
  142. final changed = media != _media || playing != _musicPlaying;
  143. _media = media;
  144. _musicPlaying = playing;
  145. if (changed) _notify();
  146. return playing;
  147. } catch (_) {
  148. return false;
  149. }
  150. }
  151. Future<void> toggleMute() async {
  152. if (_volume.fixed) return;
  153. if (_volume.index > 0) {
  154. _lastNonZeroVolume = _volume.index;
  155. await setVolume(0);
  156. } else {
  157. final restore = _lastNonZeroVolume > 0
  158. ? _lastNonZeroVolume
  159. : (_volume.max <= 0
  160. ? 1
  161. : (_volume.max / 2).round().clamp(1, _volume.max));
  162. await setVolume(restore);
  163. }
  164. }
  165. void _notify() {
  166. if (!_disposed) notifyListeners();
  167. }
  168. @override
  169. void dispose() {
  170. _disposed = true;
  171. stop();
  172. super.dispose();
  173. }
  174. }