|
@@ -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;
|
|
|
+}
|