Browse Source

成支持在后台,锁屏等

heavyrain 3 months ago
parent
commit
db8f58a0fd

+ 10 - 2
lib/main.dart

@@ -8,16 +8,24 @@ import 'package:flutter_clock/utils/app_utils.dart';
 /// Description: enter point of the app
 /// Time       : 04/06/2025 Sunday
 /// Author     : liuyuqi.gov@msn.cn
-void main() {
+void main() async {
   WidgetsFlutterBinding.ensureInitialized();
   AppUtils.setOverrideForDesktop();
-  runApp(MyApp());
+  
+  // Configure system settings for better background execution
   if (Platform.isAndroid) {
     SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
       statusBarColor: Colors.transparent,
       statusBarIconBrightness: Brightness.dark,
     ));
+    
+    // Prevent app from being killed in background
+    SystemChannels.platform.invokeMethod('SystemChrome.setEnabledSystemUIMode', [
+      SystemUiMode.edgeToEdge.index,
+    ]);
   }
+  
+  runApp(MyApp());
 }
 
 class MyApp extends StatelessWidget {

+ 104 - 0
lib/model/timer_data.dart

@@ -0,0 +1,104 @@
+import 'package:shared_preferences/shared_preferences.dart';
+
+/// A model class to store timer state data
+class TimerData {
+  static const String _startTimeKey = 'timer_start_time';
+  static const String _endTimeKey = 'timer_end_time';
+  static const String _pausedRemainingKey = 'timer_paused_remaining';
+  static const String _isRunningKey = 'timer_is_running';
+  static const String _isPausedKey = 'timer_is_paused';
+
+  // Timer start time in milliseconds since epoch
+  final int? startTime;
+  
+  // Timer end time in milliseconds since epoch
+  final int? endTime;
+  
+  // Remaining seconds when timer was paused
+  final int? pausedRemaining;
+  
+  // Whether timer is currently running
+  final bool isRunning;
+  
+  // Whether timer is currently paused
+  final bool isPaused;
+
+  TimerData({
+    this.startTime,
+    this.endTime,
+    this.pausedRemaining,
+    this.isRunning = false,
+    this.isPaused = false,
+  });
+
+  /// Save timer data to SharedPreferences
+  Future<void> save() async {
+    final prefs = await SharedPreferences.getInstance();
+    
+    if (startTime != null) {
+      await prefs.setInt(_startTimeKey, startTime!);
+    } else {
+      await prefs.remove(_startTimeKey);
+    }
+    
+    if (endTime != null) {
+      await prefs.setInt(_endTimeKey, endTime!);
+    } else {
+      await prefs.remove(_endTimeKey);
+    }
+    
+    if (pausedRemaining != null) {
+      await prefs.setInt(_pausedRemainingKey, pausedRemaining!);
+    } else {
+      await prefs.remove(_pausedRemainingKey);
+    }
+    
+    await prefs.setBool(_isRunningKey, isRunning);
+    await prefs.setBool(_isPausedKey, isPaused);
+  }
+
+  /// Load timer data from SharedPreferences
+  static Future<TimerData> load() async {
+    final prefs = await SharedPreferences.getInstance();
+    
+    return TimerData(
+      startTime: prefs.containsKey(_startTimeKey) ? prefs.getInt(_startTimeKey) : null,
+      endTime: prefs.containsKey(_endTimeKey) ? prefs.getInt(_endTimeKey) : null,
+      pausedRemaining: prefs.containsKey(_pausedRemainingKey) ? prefs.getInt(_pausedRemainingKey) : null,
+      isRunning: prefs.getBool(_isRunningKey) ?? false,
+      isPaused: prefs.getBool(_isPausedKey) ?? false,
+    );
+  }
+
+  /// Calculate remaining seconds based on current time and end time
+  int calculateRemainingSeconds() {
+    if (isPaused && pausedRemaining != null) {
+      return pausedRemaining!;
+    }
+    
+    if (endTime != null) {
+      final now = DateTime.now().millisecondsSinceEpoch;
+      final remaining = (endTime! - now) ~/ 1000;
+      return remaining > 0 ? remaining : 0;
+    }
+    
+    return 0;
+  }
+
+  /// Create a copy of this timer data with modified properties
+  TimerData copyWith({
+    int? startTime,
+    int? endTime,
+    int? pausedRemaining,
+    bool? isRunning,
+    bool? isPaused,
+  }) {
+    return TimerData(
+      startTime: startTime ?? this.startTime,
+      endTime: endTime ?? this.endTime,
+      pausedRemaining: pausedRemaining ?? this.pausedRemaining,
+      isRunning: isRunning ?? this.isRunning,
+      isPaused: isPaused ?? this.isPaused,
+    );
+  }
+} 

+ 66 - 33
lib/pages/timer/timer_page.dart

@@ -1,9 +1,12 @@
 import 'dart:async';
 import 'package:flutter/material.dart';
+import 'package:flutter_clock/model/timer_data.dart';
 import 'package:flutter_clock/model/timer_settings.dart';
 import 'package:flutter_clock/pages/timer/timer_settings_page.dart';
 import 'package:flutter_clock/utils/audio_manager.dart';
 import 'package:flutter_clock/utils/screen_manager.dart';
+import 'package:flutter_clock/utils/background_timer_service.dart';
+import 'package:flutter_clock/utils/notification_manager.dart';
 
 /// Description: 倒计时页面
 /// Time       : 04/06/2025 Sunday
@@ -22,10 +25,16 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
   int _seconds = 0;
 
   // For timer controller
-  Timer? _timer;
   TimerState timerState = TimerState.prepare;
   int _remainingSeconds = 0;
 
+  // Total timer duration in seconds (for progress calculation)
+  int _totalDurationSeconds = 0;
+  
+  // Background services
+  final BackgroundTimerService _backgroundTimerService = BackgroundTimerService();
+  final NotificationManager _notificationManager = NotificationManager();
+
   // Settings
   late TimerSettings _settings;
   bool _settingsLoaded = false;
@@ -43,13 +52,15 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
   void initState() {
     super.initState();
     WidgetsBinding.instance.addObserver(this);
+    _initializeBackgroundTimer();
     _loadSettings();
+    _restoreTimerState();
   }
 
   @override
   void dispose() {
-    _timer?.cancel();
     _audioManager.dispose();
+    _backgroundTimerService.dispose();
     if (ScreenManager.isWakeLockEnabled) {
       ScreenManager.disableWakeLock();
     }
@@ -62,11 +73,42 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
 
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) {
-    if (state == AppLifecycleState.resumed &&
-        timerState == TimerState.running) {
-      _syncTimer();
+    // No additional handling required as background service handles the state
+  }
+  
+  /// Restore timer state from persistent storage
+  Future<void> _restoreTimerState() async {
+    final timerData = await TimerData.load();
+    
+    if (timerData.isRunning) {
+      setState(() {
+        timerState = TimerState.running;
+        _remainingSeconds = timerData.calculateRemainingSeconds();
+        _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds;
+      });
+    } else if (timerData.isPaused && timerData.pausedRemaining != null) {
+      setState(() {
+        timerState = TimerState.pause;
+        _remainingSeconds = timerData.pausedRemaining!;
+        _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds;
+      });
     }
   }
+  
+  /// Initialize the background timer service
+  Future<void> _initializeBackgroundTimer() async {
+    await _backgroundTimerService.initialize(
+      onTimerComplete: _timerCompleted,
+      onTimerTick: (remainingSeconds) {
+        setState(() {
+          _remainingSeconds = remainingSeconds;
+          if (remainingSeconds <= 0 && timerState != TimerState.finish) {
+            timerState = TimerState.finish;
+          }
+        });
+      },
+    );
+  }
 
   Future<void> _loadSettings() async {
     _settings = await TimerSettings.loadSettings();
@@ -75,12 +117,13 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
     });
   }
 
-  /// resumed 暂停, 恢复
+  /// Start a new timer or resume an existing paused timer
   void _startTimer(bool resumed) {
     if (resumed) {
       setState(() {
         timerState = TimerState.running;
       });
+      _backgroundTimerService.resumeTimer(_remainingSeconds, _totalDurationSeconds);
     } else {
       final totalSeconds = _hours * 3600 + _minutes * 60 + _seconds;
       if (totalSeconds <= 0) return;
@@ -88,52 +131,41 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
       setState(() {
         timerState = TimerState.running;
         _remainingSeconds = totalSeconds;
+        _totalDurationSeconds = totalSeconds;
       });
+      
+      _backgroundTimerService.startTimer(totalSeconds);
     }
-    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
-      setState(() {
-        if (_remainingSeconds > 0) {
-          _remainingSeconds--;
-        } else {
-          timerState = TimerState.finish;
-          _timerCompleted();
-        }
-      });
-    });
   }
 
+  /// Pause the running timer
   void _pauseTimer() {
-    _timer?.cancel();
+    _backgroundTimerService.pauseTimer();
     setState(() {
       timerState = TimerState.pause;
     });
   }
 
+  /// Reset the timer to initial state
   void _resetTimer() {
-    _timer?.cancel();
+    _backgroundTimerService.cancelTimer();
     _audioManager.stopSound();
     _audioManager.stopVibration();
+    _notificationManager.stopVibration();
     setState(() {
       timerState = TimerState.prepare;
     });
   }
 
-  void _syncTimer() {
-    // Recalculate elapsed time if app was in background
-    // final elapsedSeconds = _hours * 3600 + _minutes * 60 + _seconds -
-    //     _remainingSeconds;
-    // setState(() {
-    //   _remainingSeconds = elapsedSeconds;
-    // });
-  }
-
+  /// Handle timer completion
   Future<void> _timerCompleted() async {
-    _timer?.cancel();
-    timerState = TimerState.finish;
+    setState(() {
+      timerState = TimerState.finish;
+    });
 
     // Play sound and vibrate
     if (_settings.vibrate) {
-      _audioManager.triggerVibration();
+      _notificationManager.notifyTimerCompletion();
     }
     _audioManager.playSound(_settings.sound, _settings.volume, _settings.loop);
 
@@ -141,7 +173,7 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
     if (_settings.loop) {
       Future.delayed(Duration(seconds: 5), () {
         _audioManager.stopSound();
-        _audioManager.stopVibration();
+        _notificationManager.stopVibration();
       });
       _startTimer(false);
     }
@@ -292,8 +324,9 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
                           child: CircularProgressIndicator(
                             value: timerState == TimerState.finish
                                 ? 1
-                                : _remainingSeconds /
-                                    (_hours * 3600 + _minutes * 60 + _seconds),
+                                : _totalDurationSeconds > 0
+                                    ? _remainingSeconds / _totalDurationSeconds
+                                    : 0,
                             strokeWidth: 5,
                             backgroundColor: Colors.grey.withOpacity(0.1),
                             color: Colors.blue,

+ 256 - 0
lib/utils/background_timer_service.dart

@@ -0,0 +1,256 @@
+import 'dart:async';
+import 'dart:isolate';
+import 'dart:ui';
+
+import 'package:flutter_clock/model/timer_data.dart';
+import 'package:flutter_clock/utils/audio_manager.dart';
+
+/// A service to handle background timer operations
+class BackgroundTimerService {
+  static const String _isolateName = 'timer_isolate';
+  
+  // Isolate for background processing
+  Isolate? _isolate;
+  ReceivePort? _receivePort;
+  SendPort? _sendPort;
+  
+  // Callback when timer completes
+  Function? _onTimerComplete;
+  // Callback for timer tick
+  Function(int)? _onTimerTick;
+  
+  bool _isRunning = false;
+  int _totalDurationSeconds = 0;
+  
+  /// Initialize the background service
+  Future<void> initialize({
+    Function? onTimerComplete,
+    Function(int)? onTimerTick,
+  }) async {
+    _onTimerComplete = onTimerComplete;
+    _onTimerTick = onTimerTick;
+    
+    // Register port for isolate communication
+    _receivePort = ReceivePort();
+    IsolateNameServer.registerPortWithName(
+      _receivePort!.sendPort,
+      _isolateName,
+    );
+    
+    _receivePort!.listen(_handleIsolateMessage);
+    
+    // Check if there's a running timer from the previous session
+    await _checkForExistingTimer();
+  }
+  
+  /// Check if there's an existing timer running from a previous session
+  Future<void> _checkForExistingTimer() async {
+    final timerData = await TimerData.load();
+    
+    if (timerData.isRunning) {
+      final remainingSeconds = timerData.calculateRemainingSeconds();
+      
+      if (remainingSeconds > 0) {
+        // Timer is still running
+        _startBackgroundTimer(remainingSeconds, _calculateTotalDuration(timerData));
+        
+        // Notify UI of the current state
+        if (_onTimerTick != null) {
+          _onTimerTick!(remainingSeconds);
+        }
+      } else {
+        // Timer has completed while app was closed
+        _completeTimer();
+      }
+    } else if (timerData.isPaused && timerData.pausedRemaining != null) {
+      // Timer is paused, restore state but don't start it
+      _totalDurationSeconds = _calculateTotalDuration(timerData);
+      
+      // Notify UI of the current state
+      if (_onTimerTick != null && timerData.pausedRemaining != null) {
+        _onTimerTick!(timerData.pausedRemaining!);
+      }
+    }
+  }
+  
+  /// Calculate total duration from timer data
+  int _calculateTotalDuration(TimerData timerData) {
+    if (timerData.startTime != null && timerData.endTime != null) {
+      return ((timerData.endTime! - timerData.startTime!) / 1000).round();
+    }
+    return 0;
+  }
+  
+  /// Start a new timer for the given duration
+  Future<void> startTimer(int durationSeconds) async {
+    if (_isRunning) {
+      await cancelTimer();
+    }
+    
+    _totalDurationSeconds = durationSeconds;
+    
+    final now = DateTime.now().millisecondsSinceEpoch;
+    final endTime = now + durationSeconds * 1000;
+    
+    // Save timer state
+    final timerData = TimerData(
+      startTime: now,
+      endTime: endTime,
+      isRunning: true,
+      isPaused: false,
+    );
+    await timerData.save();
+    
+    _startBackgroundTimer(durationSeconds, durationSeconds);
+  }
+  
+  /// Resume a paused timer
+  Future<void> resumeTimer(int remainingSeconds, int totalDurationSeconds) async {
+    _totalDurationSeconds = totalDurationSeconds;
+    
+    final now = DateTime.now().millisecondsSinceEpoch;
+    final endTime = now + remainingSeconds * 1000;
+    
+    // Save timer state
+    final timerData = TimerData(
+      startTime: now - (totalDurationSeconds - remainingSeconds) * 1000,
+      endTime: endTime,
+      isRunning: true,
+      isPaused: false,
+    );
+    await timerData.save();
+    
+    _startBackgroundTimer(remainingSeconds, totalDurationSeconds);
+  }
+  
+  /// Start background timer processing
+  Future<void> _startBackgroundTimer(int remainingSeconds, int totalDurationSeconds) async {
+    if (_isolate != null) {
+      _isolate!.kill(priority: Isolate.immediate);
+      _isolate = null;
+    }
+    
+    _isRunning = true;
+    
+    // Create a new isolate for background processing
+    _isolate = await Isolate.spawn(
+      _isolateEntryPoint,
+      {
+        'remainingSeconds': remainingSeconds,
+        'totalDurationSeconds': totalDurationSeconds,
+        'sendPort': _receivePort!.sendPort,
+      },
+    );
+  }
+  
+  /// Cancel the current timer
+  Future<void> cancelTimer() async {
+    if (_isolate != null) {
+      _isolate!.kill(priority: Isolate.immediate);
+      _isolate = null;
+    }
+    
+    _isRunning = false;
+    
+    // Clear timer state
+    final timerData = TimerData(
+      isRunning: false,
+      isPaused: false,
+    );
+    await timerData.save();
+  }
+  
+  /// Pause the current timer
+  Future<void> pauseTimer() async {
+    if (!_isRunning) return;
+    
+    // Get current timer state
+    final timerData = await TimerData.load();
+    final remainingSeconds = timerData.calculateRemainingSeconds();
+    
+    if (_isolate != null) {
+      _isolate!.kill(priority: Isolate.immediate);
+      _isolate = null;
+    }
+    
+    // Save paused state
+    final updatedTimerData = TimerData(
+      startTime: timerData.startTime,
+      endTime: timerData.endTime,
+      pausedRemaining: remainingSeconds,
+      isRunning: false,
+      isPaused: true,
+    );
+    await updatedTimerData.save();
+    
+    _isRunning = false;
+  }
+  
+  /// Handle messages from the timer isolate
+  void _handleIsolateMessage(dynamic message) {
+    if (message is Map) {
+      if (message.containsKey('tick')) {
+        final remainingSeconds = message['tick'] as int;
+        if (_onTimerTick != null) {
+          _onTimerTick!(remainingSeconds);
+        }
+      } else if (message.containsKey('completed')) {
+        _completeTimer();
+      }
+    }
+  }
+  
+  /// Complete the timer and trigger necessary callbacks
+  Future<void> _completeTimer() async {
+    _isRunning = false;
+    
+    // Clear timer state
+    final timerData = TimerData(
+      isRunning: false,
+      isPaused: false,
+    );
+    await timerData.save();
+    
+    if (_onTimerComplete != null) {
+      _onTimerComplete!();
+    }
+  }
+  
+  /// Entry point for the timer isolate
+  static void _isolateEntryPoint(Map<String, dynamic> params) {
+    final remainingSeconds = params['remainingSeconds'] as int;
+    final sendPort = params['sendPort'] as SendPort;
+    
+    int currentSeconds = remainingSeconds;
+    Timer.periodic(Duration(seconds: 1), (timer) {
+      currentSeconds--;
+      sendPort.send({'tick': currentSeconds});
+      
+      if (currentSeconds <= 0) {
+        timer.cancel();
+        sendPort.send({'completed': true});
+      }
+    });
+  }
+  
+  /// Cleanup resources
+  void dispose() {
+    if (_isolate != null) {
+      _isolate!.kill(priority: Isolate.immediate);
+      _isolate = null;
+    }
+    
+    if (_receivePort != null) {
+      _receivePort!.close();
+      _receivePort = null;
+    }
+    
+    IsolateNameServer.removePortNameMapping(_isolateName);
+  }
+  
+  /// Get the current state of the timer
+  bool get isRunning => _isRunning;
+  
+  /// Get the total duration in seconds
+  int get totalDurationSeconds => _totalDurationSeconds;
+} 

+ 36 - 0
lib/utils/notification_manager.dart

@@ -0,0 +1,36 @@
+import 'package:vibration/vibration.dart';
+
+/// A service to handle notifications and vibration
+class NotificationManager {
+  static final NotificationManager _instance = NotificationManager._internal();
+  
+  factory NotificationManager() {
+    return _instance;
+  }
+  
+  NotificationManager._internal();
+  
+  /// Vibrate the device to notify user of timer completion
+  Future<void> notifyTimerCompletion() async {
+    await triggerVibration();
+  }
+  
+  /// Trigger vibration pattern
+  Future<void> triggerVibration() async {
+    if (await Vibration.hasVibrator() ?? false) {
+      // Pattern for stronger notification - will repeat 3 times with pauses
+      // Vibrate for 500ms, pause for 200ms, repeat
+      Vibration.vibrate(
+        pattern: [0, 500, 200, 500, 200, 500],
+        intensities: [0, 255, 0, 255, 0, 255],
+      );
+    }
+  }
+  
+  /// Stop vibration
+  Future<void> stopVibration() async {
+    if (await Vibration.hasVibrator() ?? false) {
+      Vibration.cancel();
+    }
+  }
+}