diff --git a/lib/data/services/overlay_service.dart b/lib/data/services/overlay_service.dart new file mode 100644 index 0000000..84454da --- /dev/null +++ b/lib/data/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 '../../services/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; + } +}