system_controller.dart 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import 'dart:async';
  2. import 'package:flutter/foundation.dart';
  3. import '../../data/models/system_status.dart';
  4. import '../../data/services/native_bridge.dart';
  5. /// Polls the system for battery, Wi-Fi, media volume and media activity.
  6. ///
  7. /// Polling keeps the app free of long-lived broadcast receivers and works
  8. /// identically on Android 10 and 15. Timers are stopped while the launcher is
  9. /// in the background.
  10. class SystemController extends ChangeNotifier {
  11. SystemController(this._platform);
  12. final LauncherPlatform _platform;
  13. SystemStatus _status = const SystemStatus();
  14. VolumeInfo _volume = const VolumeInfo();
  15. Timer? _volumeTimer;
  16. Timer? _statusTimer;
  17. bool _active = false;
  18. bool _volumeBusy = false;
  19. bool _statusBusy = false;
  20. bool _musicBusy = false;
  21. bool _musicPlaying = false;
  22. bool _disposed = false;
  23. int _lastNonZeroVolume = 0;
  24. SystemStatus get status => _status;
  25. VolumeInfo get volume => _volume;
  26. int get volumePercent => _volume.percent;
  27. bool get isMuted => _volume.index <= 0;
  28. bool get isMusicPlaying => _musicPlaying;
  29. Future<void> start() async {
  30. if (_active) return;
  31. _active = true;
  32. await refreshAll();
  33. _volumeTimer = Timer.periodic(const Duration(milliseconds: 900), (_) {
  34. _pollVolume();
  35. _pollMusic();
  36. });
  37. _statusTimer = Timer.periodic(
  38. const Duration(seconds: 20),
  39. (_) => _pollStatus(),
  40. );
  41. }
  42. void stop() {
  43. _active = false;
  44. _volumeTimer?.cancel();
  45. _volumeTimer = null;
  46. _statusTimer?.cancel();
  47. _statusTimer = null;
  48. }
  49. Future<void> refreshAll() async {
  50. await Future.wait([_pollVolume(), _pollStatus(), _pollMusic()]);
  51. }
  52. Future<void> _pollVolume() async {
  53. if (_volumeBusy || !_active) return;
  54. _volumeBusy = true;
  55. try {
  56. final next = await _platform.getVolume();
  57. if (next.index != _volume.index || next.max != _volume.max) {
  58. _volume = next;
  59. if (next.index > 0) _lastNonZeroVolume = next.index;
  60. _notify();
  61. }
  62. } finally {
  63. _volumeBusy = false;
  64. }
  65. }
  66. Future<void> _pollMusic() async {
  67. if (_musicBusy || !_active) return;
  68. _musicBusy = true;
  69. try {
  70. final playing = await _platform.isMusicPlaying();
  71. if (playing != _musicPlaying) {
  72. _musicPlaying = playing;
  73. _notify();
  74. }
  75. } finally {
  76. _musicBusy = false;
  77. }
  78. }
  79. Future<void> _pollStatus() async {
  80. if (_statusBusy) return;
  81. _statusBusy = true;
  82. try {
  83. final next = await _platform.getStatus();
  84. if (next.batteryLevel != _status.batteryLevel ||
  85. next.charging != _status.charging ||
  86. next.wifi != _status.wifi) {
  87. _status = next;
  88. _notify();
  89. }
  90. } finally {
  91. _statusBusy = false;
  92. }
  93. }
  94. Future<void> setVolume(int index) async {
  95. final max = _volume.max <= 0 ? 15 : _volume.max;
  96. final clamped = index.clamp(0, max);
  97. _volume = VolumeInfo(index: clamped, max: max);
  98. if (clamped > 0) _lastNonZeroVolume = clamped;
  99. _notify();
  100. await _platform.setVolume(clamped);
  101. }
  102. Future<void> adjustVolume(int delta) => setVolume(_volume.index + delta);
  103. /// Sends a pause key immediately. The next poll reconciles the real state.
  104. Future<void> pauseMusic() async {
  105. try {
  106. await _platform.sendMediaKey('pause');
  107. } catch (_) {
  108. // Media keys are best-effort; the next poll still reflects reality.
  109. }
  110. if (_musicPlaying) {
  111. _musicPlaying = false;
  112. _notify();
  113. }
  114. }
  115. /// Tries to resume a paused session. Returns false when nothing started, so
  116. /// the caller can open the configured music app instead.
  117. Future<bool> playMusic() async {
  118. try {
  119. final sent = await _platform.sendMediaKey('play');
  120. if (!sent) return false;
  121. await Future<void>.delayed(const Duration(milliseconds: 450));
  122. final playing = await _platform.isMusicPlaying();
  123. if (playing != _musicPlaying) {
  124. _musicPlaying = playing;
  125. _notify();
  126. }
  127. return playing;
  128. } catch (_) {
  129. return false;
  130. }
  131. }
  132. Future<void> toggleMute() async {
  133. if (_volume.index > 0) {
  134. _lastNonZeroVolume = _volume.index;
  135. await setVolume(0);
  136. } else {
  137. final restore = _lastNonZeroVolume > 0
  138. ? _lastNonZeroVolume
  139. : (_volume.max <= 0
  140. ? 1
  141. : (_volume.max / 2).round().clamp(1, _volume.max));
  142. await setVolume(restore);
  143. }
  144. }
  145. void _notify() {
  146. if (!_disposed) notifyListeners();
  147. }
  148. @override
  149. void dispose() {
  150. _disposed = true;
  151. stop();
  152. super.dispose();
  153. }
  154. }