diff --git a/lib/config/overlay_constants.dart b/lib/config/overlay_constants.dart index 3fc0536..f798986 100644 --- a/lib/config/overlay_constants.dart +++ b/lib/config/overlay_constants.dart @@ -34,4 +34,16 @@ abstract class OverlayConfig { /// Default overlay height for calculations static const double DEFAULT_OVERLAY_HEIGHT = 140.0; + + /// Time after which overlay dims speed display due to stale data + static const Duration STALENESS_DIM_THRESHOLD = Duration(seconds: 3); + + /// Time after which overlay shows dashes instead of speed + static const Duration STALENESS_DASH_THRESHOLD = Duration(seconds: 10); + + /// Opacity value for dimmed stale state + static const double STALENESS_DIM_OPACITY = 0.5; + + /// Interval for checking data staleness + static const Duration STALENESS_CHECK_INTERVAL = Duration(seconds: 1); } diff --git a/lib/config/timing_constants.dart b/lib/config/timing_constants.dart index c80718e..43e71d9 100644 --- a/lib/config/timing_constants.dart +++ b/lib/config/timing_constants.dart @@ -13,4 +13,7 @@ abstract class TimingConfig { /// Delay for long press gesture signal static const Duration LONG_PRESS_SIGNAL_DELAY = Duration(milliseconds: 50); + + /// GPS grace period - continues for 30s after overlay close when backgrounded + static const Duration GPS_GRACE_PERIOD = Duration(seconds: 30); } diff --git a/lib/services/overlay_service.dart b/lib/services/overlay_service.dart new file mode 100644 index 0000000..7578339 --- /dev/null +++ b/lib/services/overlay_service.dart @@ -0,0 +1,96 @@ +import 'package:flutter_overlay_window/flutter_overlay_window.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:retry/retry.dart'; +import 'logger.dart'; + +/// Service wrapper for FlutterOverlayWindow platform calls with retry logic +/// Encapsulates all overlay operations with error handling and verification +class OverlayService { + /// Retry configuration for platform operations + static final RetryOptions _retryOptions = RetryOptions( + maxAttempts: 3, + delayFactor: const Duration(milliseconds: 200), + ); + + /// Show overlay window with retry logic and verification + /// Returns true if overlay successfully opened and verified active + Future showOverlay({ + required int width, + required int height, + }) async { + try { + await _retryOptions.retry( + () async { + await FlutterOverlayWindow.showOverlay( + enableDrag: true, + overlayTitle: "Speedometer", + overlayContent: 'Speedo overlay active', + flag: OverlayFlag.defaultFlag, + visibility: NotificationVisibility.visibilityPublic, + positionGravity: PositionGravity.none, + width: width, + height: height, + ); + }, + ); + + // Verify overlay actually opened + final isActive = await FlutterOverlayWindow.isActive(); + if (!isActive) { + Logger.error('Overlay showOverlay succeeded but isActive returned false', 'OverlayService'); + return false; + } + + return true; + } catch (e) { + Logger.error('Failed to show overlay after retries: $e', 'OverlayService'); + return false; + } + } + + /// Close overlay window with retry logic + /// Returns true on success, false on failure after retries + Future closeOverlay() async { + try { + await _retryOptions.retry( + () async { + await FlutterOverlayWindow.closeOverlay(); + }, + ); + return true; + } catch (e) { + Logger.error('Failed to close overlay after retries: $e', 'OverlayService'); + return false; + } + } + + /// Share data with overlay window (fire-and-forget) + /// Does NOT retry - data loss is expected and handled by staleness detection + Future shareData(Map data) async { + try { + await FlutterOverlayWindow.shareData(data); + } catch (e) { + Logger.warn('Failed to share data with overlay: $e', 'OverlayService'); + } + } + + /// Check if overlay window is currently active + /// Returns false on error + Future isActive() async { + try { + return await FlutterOverlayWindow.isActive(); + } catch (e) { + Logger.error('Failed to check overlay active status: $e', 'OverlayService'); + return false; + } + } + + /// Stream of messages from overlay window + /// Direct delegation to platform channel + Stream get overlayListener => FlutterOverlayWindow.overlayListener; + + /// Check if system alert window permission is granted + Future checkPermission() async { + return await Permission.systemAlertWindow.isGranted; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9f5c551..00a16b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,9 @@ dependencies: # Persistent settings storage shared_preferences: ^2.3.4 + # Retry logic for platform operations + retry: ^3.1.2 + dev_dependencies: flutter_test: