feat(03-02): integrate OverlayService into OverlayProvider with error callbacks and GPS grace period

- Add OverlayError enum (permission, creationFailed, communicationDegraded)
- Add OverlayService dependency via constructor injection
- Add onError callback field for error surfacing (set by SpeedometerScreen)
- Refactor showOverlay to check permission before showing overlay
- Refactor showOverlay to verify overlay creation via isActive check
- Only set _isOverlayActive after verification succeeds
- Replace all FlutterOverlayWindow calls with OverlayService methods
- Add GPS grace period timer (30s) in _handleOverlayClose
- Cancel grace period if overlay reopens within 30s
- Register OverlayService in MultiProvider before OverlayProvider
- Inject OverlayService into OverlayProvider constructor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2026-02-11 19:21:58 +09:00
parent acfbeb85a2
commit 66bdd35995
2 changed files with 47 additions and 14 deletions
+3 -1
View File
@@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'screens/speedometer_screen.dart'; import 'screens/speedometer_screen.dart';
import 'screens/overlay_screen.dart'; import 'screens/overlay_screen.dart';
import 'services/gps_data_manager.dart'; import 'services/gps_data_manager.dart';
import 'services/overlay_service.dart';
import 'providers/settings_provider.dart'; import 'providers/settings_provider.dart';
import 'providers/overlay_provider.dart'; import 'providers/overlay_provider.dart';
import 'services/logger.dart'; import 'services/logger.dart';
@@ -16,6 +17,7 @@ void main() async {
MultiProvider( MultiProvider(
providers: [ providers: [
Provider<SharedPreferences>.value(value: prefs), Provider<SharedPreferences>.value(value: prefs),
Provider<OverlayService>(create: (_) => OverlayService()),
ChangeNotifierProvider( ChangeNotifierProvider(
create: (context) => SettingsProvider(context.read<SharedPreferences>()), create: (context) => SettingsProvider(context.read<SharedPreferences>()),
), ),
@@ -23,7 +25,7 @@ void main() async {
create: (_) => GpsDataManager(), create: (_) => GpsDataManager(),
), ),
ChangeNotifierProxyProvider2<GpsDataManager, SettingsProvider, OverlayProvider>( ChangeNotifierProxyProvider2<GpsDataManager, SettingsProvider, OverlayProvider>(
create: (_) => OverlayProvider(), create: (context) => OverlayProvider(context.read<OverlayService>()),
update: (_, gpsManager, settings, overlay) => overlay! update: (_, gpsManager, settings, overlay) => overlay!
..updateDependencies( ..updateDependencies(
gpsManager: gpsManager, gpsManager: gpsManager,
+44 -13
View File
@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:bg_launcher/bg_launcher.dart'; import 'package:bg_launcher/bg_launcher.dart';
import '../config/overlay_constants.dart'; import '../config/overlay_constants.dart';
import '../config/timing_constants.dart'; import '../config/timing_constants.dart';
@@ -9,8 +8,13 @@ import '../models/overlay_message.dart';
import '../models/processed_gps_data.dart'; import '../models/processed_gps_data.dart';
import '../services/gps_data_manager.dart'; import '../services/gps_data_manager.dart';
import '../services/logger.dart'; import '../services/logger.dart';
import '../services/overlay_service.dart';
enum OverlayError { permission, creationFailed, communicationDegraded }
class OverlayProvider extends ChangeNotifier { class OverlayProvider extends ChangeNotifier {
final OverlayService _overlayService;
bool _isOverlayActive = false; bool _isOverlayActive = false;
bool _isDisposed = false; bool _isDisposed = false;
bool _tapCloseRequested = false; bool _tapCloseRequested = false;
@@ -19,12 +23,18 @@ class OverlayProvider extends ChangeNotifier {
StreamSubscription<dynamic>? _overlayMessageSubscription; StreamSubscription<dynamic>? _overlayMessageSubscription;
Timer? _overlayStatusCheckTimer; Timer? _overlayStatusCheckTimer;
Timer? _tapCloseTimer; Timer? _tapCloseTimer;
Timer? _gracePeriodTimer;
// Dependencies — set via update() from ProxyProvider // Dependencies — set via update() from ProxyProvider
GpsDataManager? _gpsManager; GpsDataManager? _gpsManager;
SpeedUnit _currentUnit = SpeedUnit.kmh; SpeedUnit _currentUnit = SpeedUnit.kmh;
int _currentThemeIndex = 0; int _currentThemeIndex = 0;
// Error callback — set by SpeedometerScreen after provider is available
void Function(OverlayError)? onError;
OverlayProvider(this._overlayService);
bool get isOverlayActive => _isOverlayActive; bool get isOverlayActive => _isOverlayActive;
/// Called by ChangeNotifierProxyProvider2 when GpsDataManager or SettingsProvider changes. /// Called by ChangeNotifierProxyProvider2 when GpsDataManager or SettingsProvider changes.
@@ -60,7 +70,19 @@ class OverlayProvider extends ChangeNotifier {
Future<void> showOverlay() async { Future<void> showOverlay() async {
if (_isDisposed) return; if (_isDisposed) return;
// Cancel grace period if user reopens overlay within grace period
_gracePeriodTimer?.cancel();
_gracePeriodTimer = null;
try { try {
// Check permission before attempting to show overlay
final hasPermission = await _overlayService.checkPermission();
if (!hasPermission) {
Logger.warn('Overlay permission denied', 'OverlayProvider');
onError?.call(OverlayError.permission);
return;
}
final overlaySize = _getOverlaySize(); final overlaySize = _getOverlaySize();
final gpsData = _gpsManager?.currentData; final gpsData = _gpsManager?.currentData;
@@ -69,7 +91,7 @@ class OverlayProvider extends ChangeNotifier {
final headingText = gpsData?.displayHeading ?? 'N/A'; final headingText = gpsData?.displayHeading ?? 'N/A';
final unitText = _currentUnit.label; final unitText = _currentUnit.label;
await FlutterOverlayWindow.shareData(OverlayMessage.updateDisplay( await _overlayService.shareData(OverlayMessage.updateDisplay(
speedText: speedText, speedText: speedText,
unitText: unitText, unitText: unitText,
headingText: headingText, headingText: headingText,
@@ -80,17 +102,17 @@ class OverlayProvider extends ChangeNotifier {
overlayHeight: overlaySize['height']!.toDouble(), overlayHeight: overlaySize['height']!.toDouble(),
).toMap()); ).toMap());
await FlutterOverlayWindow.showOverlay( final success = await _overlayService.showOverlay(
enableDrag: true,
overlayTitle: "Speedometer",
overlayContent: 'Speedo overlay active',
flag: OverlayFlag.defaultFlag,
visibility: NotificationVisibility.visibilityPublic,
positionGravity: PositionGravity.none,
width: overlaySize['width']!, width: overlaySize['width']!,
height: overlaySize['height']!, height: overlaySize['height']!,
); );
if (!success) {
Logger.error('Overlay creation failed or verification failed', 'OverlayProvider');
onError?.call(OverlayError.creationFailed);
return;
}
Logger.info('OVERLAY LAUNCHED - Size: ${overlaySize['width']}x${overlaySize['height']}', 'OverlayProvider'); Logger.info('OVERLAY LAUNCHED - Size: ${overlaySize['width']}x${overlaySize['height']}', 'OverlayProvider');
_isOverlayActive = true; _isOverlayActive = true;
@@ -99,6 +121,7 @@ class OverlayProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
Logger.error('Failed to show overlay: $e', 'OverlayProvider'); Logger.error('Failed to show overlay: $e', 'OverlayProvider');
onError?.call(OverlayError.creationFailed);
} }
} }
@@ -107,7 +130,7 @@ class OverlayProvider extends ChangeNotifier {
if (!_isOverlayActive || _isDisposed) return; if (!_isOverlayActive || _isDisposed) return;
try { try {
await FlutterOverlayWindow.closeOverlay(); await _overlayService.closeOverlay();
Logger.info('OVERLAY CLOSED - Requested from main app', 'OverlayProvider'); Logger.info('OVERLAY CLOSED - Requested from main app', 'OverlayProvider');
_handleOverlayClose(); _handleOverlayClose();
} catch (e) { } catch (e) {
@@ -141,7 +164,7 @@ class OverlayProvider extends ChangeNotifier {
Logger.debug('Pushing to overlay: speed="$speedText", heading="$headingText"', 'OverlayProvider'); Logger.debug('Pushing to overlay: speed="$speedText", heading="$headingText"', 'OverlayProvider');
FlutterOverlayWindow.shareData(OverlayMessage.updateDisplay( _overlayService.shareData(OverlayMessage.updateDisplay(
speedText: speedText, speedText: speedText,
unitText: unitText, unitText: unitText,
headingText: headingText, headingText: headingText,
@@ -154,7 +177,7 @@ class OverlayProvider extends ChangeNotifier {
void _startListeningToOverlayMessages() { void _startListeningToOverlayMessages() {
if (_overlayMessageSubscription != null) return; if (_overlayMessageSubscription != null) return;
_overlayMessageSubscription = FlutterOverlayWindow.overlayListener.listen( _overlayMessageSubscription = _overlayService.overlayListener.listen(
(data) { (data) {
try { try {
if (data is Map) { if (data is Map) {
@@ -195,7 +218,7 @@ class OverlayProvider extends ChangeNotifier {
_overlayStatusCheckTimer = Timer.periodic(TimingConfig.OVERLAY_STATUS_CHECK_INTERVAL, (timer) async { _overlayStatusCheckTimer = Timer.periodic(TimingConfig.OVERLAY_STATUS_CHECK_INTERVAL, (timer) async {
if (_isOverlayActive && !_isDisposed) { if (_isOverlayActive && !_isDisposed) {
try { try {
final isActive = await FlutterOverlayWindow.isActive(); final isActive = await _overlayService.isActive();
if (!isActive) { if (!isActive) {
timer.cancel(); timer.cancel();
_handleOverlayClose(); _handleOverlayClose();
@@ -223,6 +246,13 @@ class OverlayProvider extends ChangeNotifier {
_stopListeningToOverlayMessages(); _stopListeningToOverlayMessages();
notifyListeners(); notifyListeners();
// Start GPS grace period (30s) — infrastructure for Phase 4
_gracePeriodTimer?.cancel();
_gracePeriodTimer = Timer(TimingConfig.GPS_GRACE_PERIOD, () {
Logger.info('GPS grace period expired', 'OverlayProvider');
_gracePeriodTimer = null;
});
if (shouldBringToFront) { if (shouldBringToFront) {
_bringAppToFront(); _bringAppToFront();
} }
@@ -250,6 +280,7 @@ class OverlayProvider extends ChangeNotifier {
_overlayMessageSubscription?.cancel(); _overlayMessageSubscription?.cancel();
_overlayStatusCheckTimer?.cancel(); _overlayStatusCheckTimer?.cancel();
_tapCloseTimer?.cancel(); _tapCloseTimer?.cancel();
_gracePeriodTimer?.cancel();
Logger.info('OverlayProvider disposed', 'OverlayProvider'); Logger.info('OverlayProvider disposed', 'OverlayProvider');
super.dispose(); super.dispose();
} }