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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user