feat(03-01): create OverlayService and add retry dependency

- Add retry package (^3.1.2) for robust platform operations
- Create OverlayService wrapper for all FlutterOverlayWindow calls
- Implement showOverlay/closeOverlay with 3-attempt retry logic
- Add shareData fire-and-forget with warn-level logging
- Add isActive, overlayListener, checkPermission helpers
- Define staleness constants: 3s dim, 10s dash, 0.5 opacity, 1s check
- Define GPS_GRACE_PERIOD constant (30 seconds)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2026-02-11 19:12:23 +09:00
parent c58183b4fb
commit 505219c858
4 changed files with 114 additions and 0 deletions
+96
View File
@@ -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<bool> 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<bool> 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<void> shareData(Map<String, dynamic> 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<bool> 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<dynamic> get overlayListener => FlutterOverlayWindow.overlayListener;
/// Check if system alert window permission is granted
Future<bool> checkPermission() async {
return await Permission.systemAlertWindow.isGranted;
}
}