import 'dart:async'; import 'package:flutter/foundation.dart'; import '../../data/models/system_status.dart'; import '../../data/services/native_bridge.dart'; /// Polls the system for battery, Wi-Fi and media volume. /// /// Polling keeps the app free of long-lived broadcast receivers and works /// identically on Android 10 and 15. Timers are stopped while the launcher is /// in the background. class SystemController extends ChangeNotifier { SystemController(this._platform); final LauncherPlatform _platform; SystemStatus _status = const SystemStatus(); VolumeInfo _volume = const VolumeInfo(); Timer? _volumeTimer; Timer? _statusTimer; bool _active = false; bool _volumeBusy = false; bool _statusBusy = false; bool _disposed = false; int _lastNonZeroVolume = 0; SystemStatus get status => _status; VolumeInfo get volume => _volume; int get volumePercent => _volume.percent; bool get isMuted => _volume.index <= 0; Future start() async { if (_active) return; _active = true; await refreshAll(); _volumeTimer = Timer.periodic( const Duration(milliseconds: 900), (_) => _pollVolume(), ); _statusTimer = Timer.periodic( const Duration(seconds: 20), (_) => _pollStatus(), ); } void stop() { _active = false; _volumeTimer?.cancel(); _volumeTimer = null; _statusTimer?.cancel(); _statusTimer = null; } Future refreshAll() async { await Future.wait([_pollVolume(), _pollStatus()]); } Future _pollVolume() async { if (_volumeBusy || !_active) return; _volumeBusy = true; try { final next = await _platform.getVolume(); if (next.index != _volume.index || next.max != _volume.max) { _volume = next; if (next.index > 0) _lastNonZeroVolume = next.index; _notify(); } } finally { _volumeBusy = false; } } Future _pollStatus() async { if (_statusBusy) return; _statusBusy = true; try { final next = await _platform.getStatus(); if (next.batteryLevel != _status.batteryLevel || next.charging != _status.charging || next.wifi != _status.wifi) { _status = next; _notify(); } } finally { _statusBusy = false; } } Future setVolume(int index) async { final max = _volume.max <= 0 ? 15 : _volume.max; final clamped = index.clamp(0, max); _volume = VolumeInfo(index: clamped, max: max); if (clamped > 0) _lastNonZeroVolume = clamped; _notify(); await _platform.setVolume(clamped); } Future adjustVolume(int delta) => setVolume(_volume.index + delta); Future toggleMute() async { if (_volume.index > 0) { _lastNonZeroVolume = _volume.index; await setVolume(0); } else { final restore = _lastNonZeroVolume > 0 ? _lastNonZeroVolume : (_volume.max <= 0 ? 1 : (_volume.max / 2).round().clamp(1, _volume.max)); await setVolume(restore); } } void _notify() { if (!_disposed) notifyListeners(); } @override void dispose() { _disposed = true; stop(); super.dispose(); } }