docs(04): create phase plan for GPS & power optimization

2 plans in 2 waves: core GPS engine (precision switching, lifecycle,
wake lock) then UI integration (main app staleness, screen cleanup,
provider wiring).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2026-02-12 08:50:46 +09:00
parent 1dd3511047
commit 40c74677be
3 changed files with 551 additions and 5 deletions
@@ -0,0 +1,287 @@
---
phase: 04-gps-power-optimization
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- lib/models/processed_gps_data.dart
- lib/config/gps_constants.dart
- lib/config/timing_constants.dart
- lib/services/gps_data_manager.dart
- lib/gps_service.dart
autonomous: true
must_haves:
truths:
- "GPS precision switches to HIGH when speed exceeds 12 km/h (3.33 m/s)"
- "GPS precision switches to BALANCED when speed drops below 8 km/h (2.22 m/s)"
- "Precision does NOT switch until first GPS fix is received"
- "GPS stops after 7s grace period when app backgrounded without overlay"
- "GPS continues running when app backgrounded with overlay visible"
- "GPS restarts when app returns to foreground after being stopped"
- "Wake lock enabled when GPS active AND (overlay visible OR app in foreground)"
- "Wake lock disabled when GPS stops or app backgrounds without overlay"
- "ProcessedGpsData carries timestamp for staleness detection"
artifacts:
- path: "lib/models/processed_gps_data.dart"
provides: "ProcessedGpsData with timestamp field and copyWith support"
contains: "DateTime timestamp"
- path: "lib/config/gps_constants.dart"
provides: "Precision switching threshold constants in m/s"
contains: "PRECISION_UP_THRESHOLD_MPS"
- path: "lib/config/timing_constants.dart"
provides: "Background grace period duration constant"
contains: "BACKGROUND_GRACE_PERIOD"
- path: "lib/services/gps_data_manager.dart"
provides: "Lifecycle-aware GPS engine with precision switching and wake lock"
contains: "WidgetsBindingObserver"
- path: "lib/gps_service.dart"
provides: "Dynamic position stream accepting LocationAccuracy parameter"
contains: "LocationAccuracy accuracy"
key_links:
- from: "lib/services/gps_data_manager.dart"
to: "lib/gps_service.dart"
via: "createPositionStream with dynamic accuracy"
pattern: "GpsService\\.createPositionStream"
- from: "lib/services/gps_data_manager.dart"
to: "lib/providers/overlay_provider.dart"
via: "overlay state check for background decisions"
pattern: "_overlayProvider.*isOverlayActive"
- from: "lib/services/gps_data_manager.dart"
to: "wakelock_plus"
via: "conditional wake lock based on tracking + visibility state"
pattern: "WakelockPlus\\.(enable|disable)"
---
<objective>
Build the core GPS engine with speed-adaptive precision switching, lifecycle-aware background management, and conditional wake lock control.
Purpose: This is the engine layer -- all GPS intelligence lives in GpsDataManager. It reacts to speed changes (hysteresis switching), app lifecycle events (background/foreground), and overlay state to make autonomous decisions about GPS precision, tracking, and power usage. The UI layer (Plan 02) will consume this engine's outputs.
Output: A GpsDataManager that is lifecycle-aware, precision-adaptive, and power-conscious. ProcessedGpsData with timestamp for staleness. Dynamic GPS stream via GpsService.
</objective>
<execution_context>
@C:/Users/SKSup/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SKSup/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-gps-power-optimization/04-CONTEXT.md
@.planning/phases/04-gps-power-optimization/04-RESEARCH.md
Source files to modify:
@lib/models/processed_gps_data.dart
@lib/config/gps_constants.dart
@lib/config/timing_constants.dart
@lib/services/gps_data_manager.dart
@lib/gps_service.dart
Reference files (read-only context):
@lib/providers/overlay_provider.dart
@lib/config/overlay_constants.dart
@lib/main.dart
</context>
<tasks>
<task type="auto">
<name>Task 1: Add timestamp to ProcessedGpsData and precision/lifecycle constants</name>
<files>lib/models/processed_gps_data.dart, lib/config/gps_constants.dart, lib/config/timing_constants.dart, lib/gps_service.dart</files>
<action>
**ProcessedGpsData** (`lib/models/processed_gps_data.dart`):
- Add `final DateTime timestamp` field
- Add to constructor as required parameter
- Add to `copyWith()` method
- Update the `const` default in GpsDataManager to use a factory or remove const (DateTime.now() is not const-compatible). The default initialization in GpsDataManager will need to change from `const ProcessedGpsData(...)` to `ProcessedGpsData(... timestamp: DateTime.now())`.
**GpsConfig** (`lib/config/gps_constants.dart`):
- Add precision switching threshold constants (SCREAMING_SNAKE per project convention):
- `PRECISION_UP_THRESHOLD_MPS = 3.33` (12 km/h, switch to high)
- `PRECISION_DOWN_THRESHOLD_MPS = 2.22` (8 km/h, switch to balanced/medium)
- `PRECISION_SWITCH_FIRST_FIX_DELAY = Duration(seconds: 5)` (minimum time before first precision switch)
- Keep existing constants unchanged
**TimingConfig** (`lib/config/timing_constants.dart`):
- Add `BACKGROUND_GRACE_PERIOD = Duration(seconds: 7)` for background GPS stop delay
- Update comment on existing `GPS_GRACE_PERIOD` (30s) to mark it as deprecated/unused (Phase 3 placeholder, replaced by BACKGROUND_GRACE_PERIOD). OR remove it if nothing references it besides OverlayProvider (which will be cleaned up in Plan 02).
- Check if `GPS_GRACE_PERIOD` is referenced anywhere. If only in OverlayProvider, leave it for now (Plan 02 will clean it up). If referenced elsewhere, leave both.
**GpsService** (`lib/gps_service.dart`):
- Change `positionStream` from a static getter with hardcoded `bestForNavigation` accuracy to a static method that accepts a `LocationAccuracy` parameter:
```dart
static Stream<Position> createPositionStream({
LocationAccuracy accuracy = LocationAccuracy.high,
}) {
return Geolocator.getPositionStream(
locationSettings: LocationSettings(
accuracy: accuracy,
distanceFilter: 0,
),
);
}
```
- Keep the old `positionStream` getter as a deprecated wrapper calling `createPositionStream(accuracy: LocationAccuracy.bestForNavigation)` for backward compatibility during transition, OR remove it if GpsDataManager is the only consumer (check imports first).
Do NOT modify GpsDataManager in this task -- that is Task 2.
</action>
<verify>
Run `cd J:/dev/gps_speedo && flutter analyze` -- should pass with no new errors (existing SCREAMING_SNAKE info warnings are acceptable). Check that ProcessedGpsData, GpsConfig, TimingConfig, and GpsService all compile cleanly. The GpsDataManager will have a compile error on the `const ProcessedGpsData(...)` line due to the new required `timestamp` field -- that is expected and will be fixed in Task 2.
</verify>
<done>
ProcessedGpsData has timestamp field with copyWith support. GpsConfig has precision threshold constants in m/s. TimingConfig has BACKGROUND_GRACE_PERIOD (7s). GpsService has createPositionStream() accepting dynamic accuracy. Flutter analyze shows no new errors beyond the expected GpsDataManager compile issue.
</done>
</task>
<task type="auto">
<name>Task 2: Implement lifecycle-aware GPS engine with precision switching and wake lock</name>
<files>lib/services/gps_data_manager.dart</files>
<action>
**GpsDataManager** (`lib/services/gps_data_manager.dart`) -- This is the big task. Transform GpsDataManager into a lifecycle-aware, precision-adaptive GPS engine.
**Add WidgetsBindingObserver mixin:**
- Change class declaration to `class GpsDataManager extends ChangeNotifier with WidgetsBindingObserver`
- Import `package:flutter/widgets.dart` (for WidgetsBindingObserver and WidgetsBinding)
- Import `package:wakelock_plus/wakelock_plus.dart`
- Register observer in `initialize()`: `WidgetsBinding.instance.addObserver(this)`
- Remove observer in `dispose()` BEFORE `super.dispose()`: `WidgetsBinding.instance.removeObserver(this)`
**Add overlay provider reference:**
- Add field: `OverlayProvider? _overlayProvider;`
- Add setter method: `void setOverlayProvider(OverlayProvider provider)` -- called from main.dart or SpeedometerScreen after providers are wired
- Import OverlayProvider
**Add state tracking fields:**
- `LocationAccuracy _currentAccuracy = LocationAccuracy.high;`
- `bool _hasFirstFix = false;`
- `DateTime? _firstFixTime;` (to gate precision switching for 5s after first fix)
- `bool _isAppInForeground = true;`
- `bool _isGpsActive = false;`
- `Timer? _backgroundGracePeriodTimer;`
**Fix default ProcessedGpsData initialization:**
- Change `_currentData` from `const ProcessedGpsData(...)` to non-const with `timestamp: DateTime.now()`
**Update `initialize()`:**
- After getting initial position, set `_hasFirstFix = true` and `_firstFixTime = DateTime.now()`
- Replace `GpsService.positionStream.listen(...)` with `GpsService.createPositionStream(accuracy: _currentAccuracy).listen(...)`
- Set `_isGpsActive = true` after successful subscription
- Call `_updateWakeLock()` after GPS starts
**Update `_onPositionUpdate(Position position)`:**
- Add `timestamp: DateTime.now()` to the `ProcessedGpsData(...)` constructor call
- After processing position data, add precision switching check:
```dart
if (_hasFirstFix && _firstFixTime != null) {
final timeSinceFirstFix = DateTime.now().difference(_firstFixTime!);
if (timeSinceFirstFix >= GpsConfig.PRECISION_SWITCH_FIRST_FIX_DELAY) {
_checkPrecisionSwitch(position.speed);
}
}
```
**Add precision switching methods:**
- `_checkPrecisionSwitch(double speedMps)`:
- If currently HIGH and speed < PRECISION_DOWN_THRESHOLD_MPS: switch to medium
- If currently MEDIUM and speed > PRECISION_UP_THRESHOLD_MPS: switch to high
- Per user decision: "favor accuracy over battery" -- when in doubt, stay high
- Log precision change at debug level only (user decision: invisible to user)
- `_switchPrecision(LocationAccuracy newAccuracy)`:
- Set `_currentAccuracy = newAccuracy`
- Cancel existing `_gpsSubscription`
- Create new subscription via `GpsService.createPositionStream(accuracy: newAccuracy).listen(...)`
- Log: `Logger.debug('Precision switched to $newAccuracy at ${(speedMps * 3.6).toStringAsFixed(1)} km/h', 'GpsDataManager')`
**Implement lifecycle handlers:**
- Override `didChangeAppLifecycleState(AppLifecycleState state)`:
- On `AppLifecycleState.paused`:
- Set `_isAppInForeground = false`
- Check `_overlayProvider?.isOverlayActive ?? false`
- If overlay active: log "maintaining full GPS", do nothing to GPS
- If overlay NOT active: start grace period timer (7s BACKGROUND_GRACE_PERIOD)
- Call `_updateWakeLock()`
- On `AppLifecycleState.resumed`:
- Set `_isAppInForeground = true`
- Cancel grace period timer if running
- If GPS was stopped (`_gpsSubscription == null` and `_isInitialized`): restart GPS via `_restartGps()`
- Call `_updateWakeLock()`
- Grace period timer callback: `_onGracePeriodExpired()`:
- IMPORTANT: Check overlay state again at expiry (not at start) to handle race condition where overlay activated during grace period
- If overlay is now active: log "overlay became active during grace, keeping GPS", cancel and return
- Otherwise: call `_stopGps()`
**Add GPS stop/restart methods:**
- `_stopGps()`:
- Cancel `_gpsSubscription`, set to null
- Set `_isGpsActive = false`
- Do NOT clear `_currentData` -- keep last known data (user decision: show last known speed as stale)
- Cancel `_staleDataTimer`
- Call `_updateWakeLock()`
- Log: `Logger.info('GPS stopped (backgrounded without overlay)', 'GpsDataManager')`
- `_restartGps()`:
- Log: `Logger.info('Restarting GPS (app foregrounded)', 'GpsDataManager')`
- Create new subscription via `GpsService.createPositionStream(accuracy: _currentAccuracy).listen(...)`
- Set `_isGpsActive = true`
- Call `_updateWakeLock()`
- Do NOT update timestamp on old data -- let staleness show naturally per user decision ("show last known speed until fresh data arrives")
**Add wake lock management:**
- `_updateWakeLock()`:
- `final overlayActive = _overlayProvider?.isOverlayActive ?? false;`
- `final shouldKeepAwake = _isGpsActive && (overlayActive || _isAppInForeground);`
- If shouldKeepAwake: `WakelockPlus.enable()`
- Else: `WakelockPlus.disable()`
- Log at debug level with state breakdown
**Update `dispose()`:**
- Add BEFORE existing cleanup: `WidgetsBinding.instance.removeObserver(this);`
- Add: `_backgroundGracePeriodTimer?.cancel();`
- Add: `WakelockPlus.disable();` as safety net
- Keep all existing cleanup (subscription, stale timer, data controller)
**Important constraints:**
- Do NOT remove or change the existing `_staleDataTimer` / `_scheduleStaleDataTimer()` / `_handleStaleDataTimeout()` logic -- it still serves a purpose for hard stale detection
- Do NOT change `_updateData()` method signature or behavior
- Do NOT change `getFormattedSpeed()` method
- The broadcast stream and notifyListeners pattern stays exactly the same
</action>
<verify>
Run `cd J:/dev/gps_speedo && flutter analyze` -- should pass with no new errors (SCREAMING_SNAKE info warnings acceptable). Verify GpsDataManager compiles with all new fields, methods, and the WidgetsBindingObserver mixin. Check that dispose() removes the observer before super.dispose(). Check that all timer fields are canceled in dispose().
</verify>
<done>
GpsDataManager is lifecycle-aware with WidgetsBindingObserver. Precision switches at hysteresis boundaries (8/12 km/h) with first-fix gate. Background grace period (7s) stops GPS when overlay not visible. GPS restarts on foreground resume. Wake lock is contextual (GPS active + visible). All timers canceled in dispose(). ProcessedGpsData carries timestamp. Flutter analyze passes.
</done>
</task>
</tasks>
<verification>
1. `cd J:/dev/gps_speedo && flutter analyze` passes with no new errors
2. GpsDataManager has WidgetsBindingObserver mixin with proper registration/removal
3. ProcessedGpsData has `timestamp` field with `copyWith` support
4. GpsConfig has `PRECISION_UP_THRESHOLD_MPS` (3.33) and `PRECISION_DOWN_THRESHOLD_MPS` (2.22)
5. TimingConfig has `BACKGROUND_GRACE_PERIOD` (7s)
6. GpsService has `createPositionStream()` accepting dynamic accuracy
7. Precision switching is gated by `_hasFirstFix` and 5s delay
8. Grace period checks overlay state at expiry (not start)
9. Wake lock updates on GPS start/stop, lifecycle change
10. All new timers canceled in dispose()
</verification>
<success_criteria>
- Flutter analyze passes with no new errors
- GpsDataManager implements full lifecycle awareness (background/foreground detection)
- Precision switching works with hysteresis (8/12 km/h) and first-fix safety gate
- Background GPS stops after 7s grace period (no overlay) or continues (overlay visible)
- Wake lock is contextual: active when GPS tracking AND (overlay OR foreground)
- All ProcessedGpsData instances carry timestamps
- No regressions in existing GPS data flow (broadcast stream, notifyListeners)
</success_criteria>
<output>
After completion, create `.planning/phases/04-gps-power-optimization/04-01-SUMMARY.md`
</output>
@@ -0,0 +1,258 @@
---
phase: 04-gps-power-optimization
plan: 02
type: execute
wave: 2
depends_on: ["04-01"]
files_modified:
- lib/screens/speedometer_screen.dart
- lib/providers/overlay_provider.dart
- lib/main.dart
autonomous: true
must_haves:
truths:
- "Main app speed display dims at 3s of no GPS data (0.5 opacity)"
- "Main app speed display shows dashes at 10s of no GPS data"
- "Main app speed display snaps back to full opacity immediately on fresh data"
- "Staleness behavior is identical between main app and overlay"
- "Wake lock management is solely in GpsDataManager (removed from SpeedometerScreen)"
- "OverlayProvider grace period placeholder (30s) is removed"
- "GpsDataManager receives overlay provider reference for lifecycle decisions"
- "Heartbeat and overlay status polling only run when overlay active"
- "All timers and subscriptions properly canceled in dispose()"
artifacts:
- path: "lib/screens/speedometer_screen.dart"
provides: "Main screen with staleness detection and cleaned-up lifecycle"
contains: "STALENESS_DIM_THRESHOLD"
- path: "lib/providers/overlay_provider.dart"
provides: "Overlay provider without Phase 3 grace period placeholder"
- path: "lib/main.dart"
provides: "Provider wiring connecting GpsDataManager to OverlayProvider"
key_links:
- from: "lib/screens/speedometer_screen.dart"
to: "lib/config/overlay_constants.dart"
via: "staleness threshold constants"
pattern: "OverlayConfig\\.STALENESS"
- from: "lib/main.dart"
to: "lib/services/gps_data_manager.dart"
via: "setOverlayProvider wiring"
pattern: "setOverlayProvider"
---
<objective>
Wire the lifecycle-aware GPS engine into the UI layer: add staleness detection to the main speed display, clean up wake lock and lifecycle handling that moved to GpsDataManager, remove OverlayProvider's placeholder grace period, and wire the overlay provider reference into GpsDataManager.
Purpose: Plan 01 built the engine. This plan connects it to the user-visible surfaces. After this plan, the main app and overlay behave identically -- same staleness rules, same data source, consistent behavior. Power management is centralized in GpsDataManager rather than scattered across screens and providers.
Output: A fully integrated app where GPS precision, lifecycle, and power are managed centrally, and both UI surfaces (main + overlay) show consistent staleness behavior.
</objective>
<execution_context>
@C:/Users/SKSup/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SKSup/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-gps-power-optimization/04-CONTEXT.md
@.planning/phases/04-gps-power-optimization/04-RESEARCH.md
@.planning/phases/04-gps-power-optimization/04-01-SUMMARY.md
Source files to modify:
@lib/screens/speedometer_screen.dart
@lib/providers/overlay_provider.dart
@lib/main.dart
Reference files (read-only context):
@lib/services/gps_data_manager.dart
@lib/config/overlay_constants.dart
@lib/config/timing_constants.dart
@lib/screens/overlay_screen.dart
</context>
<tasks>
<task type="auto">
<name>Task 1: Add staleness detection to main speed display and clean up SpeedometerScreen</name>
<files>lib/screens/speedometer_screen.dart</files>
<action>
**SpeedometerScreen** (`lib/screens/speedometer_screen.dart`):
**Remove wake lock management from SpeedometerScreen:**
- Remove `import 'package:wakelock_plus/wakelock_plus.dart';`
- Remove `_enableWakelock()` method entirely
- Remove `WakelockPlus.disable()` from `dispose()`
- Remove `_enableWakelock()` call from `initState()`
- Remove `_enableWakelock()` call from `_handleBackgroundTransition()`
- Wake lock is now managed by GpsDataManager (Plan 01)
**Remove WidgetsBindingObserver from SpeedometerScreen:**
- Remove `with WidgetsBindingObserver` from class declaration
- Remove `WidgetsBinding.instance.addObserver(this)` from `initState()`
- Remove `WidgetsBinding.instance.removeObserver(this)` from `dispose()`
- Remove `didChangeAppLifecycleState()` override entirely
- Remove `_isInBackground` field
- Remove `_handleBackgroundTransition()` method
- Remove `_requestBatteryOptimizationExemption()` method (empty stub)
- Lifecycle management is now in GpsDataManager (Plan 01)
**Keep background heartbeat but simplify:**
- Keep `_backgroundHeartbeatTimer` and `_startBackgroundHeartbeat()`
- The heartbeat pushes data to overlay during background -- this still needs to run from the screen because it uses `context.read<OverlayProvider>()`. However, it should be simplified:
- Remove the `_isInBackground` check (that field is removed)
- The heartbeat should always push if overlay is active -- it's cheap and the overlay needs regular data
- Actually, reconsider: the heartbeat is needed because the screen's `context` is the only way to access OverlayProvider.pushCurrentData(). Keep it running periodically, checking only `overlay.isOverlayActive`.
**Add staleness detection to speed display:**
- Import `../config/overlay_constants.dart` for `OverlayConfig.STALENESS_DIM_THRESHOLD`, `STALENESS_DASH_THRESHOLD`, `STALENESS_DIM_OPACITY`
- Add a `Timer? _stalenessCheckTimer` field
- In `initState()`, start a periodic timer (1s interval, same as overlay's `STALENESS_CHECK_INTERVAL`):
```dart
_stalenessCheckTimer = Timer.periodic(OverlayConfig.STALENESS_CHECK_INTERVAL, (_) {
if (mounted) setState(() {}); // Trigger rebuild to re-evaluate staleness
});
```
- Cancel `_stalenessCheckTimer` in `dispose()`
**Modify speed display builders to apply staleness:**
- In both `_buildPortraitLayout` and `_buildLandscapeLayout`, the speed `Selector<GpsDataManager, double>` needs to change:
- Change selector to select the full `ProcessedGpsData` object (or at minimum speed + timestamp): `Selector<GpsDataManager, ProcessedGpsData>`
- Selector: `(_, gps) => gps.currentData`
- In the builder, calculate staleness:
```dart
final age = DateTime.now().difference(data.timestamp);
final isDimmed = age >= OverlayConfig.STALENESS_DIM_THRESHOLD;
final showDashes = age >= OverlayConfig.STALENESS_DASH_THRESHOLD;
final staleOpacity = isDimmed ? OverlayConfig.STALENESS_DIM_OPACITY : 1.0;
```
- If `showDashes`: set speedText to `'--'`
- Apply `staleOpacity` to speed text color using `.withValues(alpha: staleOpacity)` (same pattern as overlay, per Phase 3 decision -- no animation)
- Apply same stale opacity to the heading display in the compass section
- Import ProcessedGpsData model if not already imported
**Apply staleness to heading/compass section too:**
- The compass `Selector<GpsDataManager, (double heading, String displayHeading)>` should also check staleness
- Change to select `ProcessedGpsData` or add timestamp to the tuple
- Apply `staleOpacity` to heading icon and text colors using `.withValues(alpha: staleOpacity)`
**Remove `_getSpeedDisplayText` method:**
- This method is a pass-through that just calls `toStringAsFixed(1)`. The staleness logic replaces it.
- Speed formatting is already done in GpsDataManager. Use `gpsManager.getFormattedSpeed(unit)` which handles the formatting, or compute inline.
**Important: Do NOT change these:**
- Error callback wiring (`onError = _handleOverlayError`)
- GPS manager initialization (`_initializeGpsManager`)
- Overlay toggle/close gesture handlers
- Theme/unit selector tap handlers
- Layout flex ratios and structure
- `_ensureOverlayPermission()` (leave stub, harmless)
</action>
<verify>
Run `cd J:/dev/gps_speedo && flutter analyze` -- should pass with no new errors. Verify no references to WakelockPlus remain in speedometer_screen.dart. Verify no WidgetsBindingObserver in SpeedometerScreen. Verify staleness constants from OverlayConfig are used (not hardcoded values). Verify stalenessCheckTimer is canceled in dispose().
</verify>
<done>
SpeedometerScreen has staleness detection matching overlay behavior (3s dim, 10s dashes, immediate snap-back). Wake lock management removed (now in GpsDataManager). Lifecycle observer removed (now in GpsDataManager). Staleness applies to both speed and heading displays. No new flutter analyze errors.
</done>
</task>
<task type="auto">
<name>Task 2: Wire overlay provider into GpsDataManager and clean up OverlayProvider</name>
<files>lib/providers/overlay_provider.dart, lib/main.dart</files>
<action>
**OverlayProvider** (`lib/providers/overlay_provider.dart`):
- Remove the Phase 3 grace period placeholder:
- Remove `Timer? _gracePeriodTimer` field
- Remove `_gracePeriodTimer?.cancel()` from `dispose()`
- Remove grace period logic from `_handleOverlayClose()`:
```dart
// DELETE these lines:
_gracePeriodTimer?.cancel();
_gracePeriodTimer = Timer(TimingConfig.GPS_GRACE_PERIOD, () {
Logger.info('GPS grace period expired', 'OverlayProvider');
_gracePeriodTimer = null;
});
```
- Remove `_gracePeriodTimer?.cancel()` from `showOverlay()` (the "cancel grace period if user reopens" line)
- Grace period is now managed by GpsDataManager's background lifecycle handler
- Remove `import '../config/timing_constants.dart'` IF `TimingConfig` is no longer referenced anywhere in this file (check `TAP_CLOSE_DELAY` -- that still uses it, so keep the import)
- Everything else in OverlayProvider stays exactly the same
**main.dart** (`lib/main.dart`):
- Wire GpsDataManager to know about OverlayProvider for lifecycle decisions.
- The challenge: GpsDataManager is created before OverlayProvider in the MultiProvider tree. We need to pass the overlay reference after both are created.
- Approach: Use `ChangeNotifierProxyProvider2`'s update callback to also set the overlay reference on GpsDataManager. OR use a simpler approach:
- Change GpsDataManager from `ChangeNotifierProvider` to `ChangeNotifierProxyProvider<OverlayProvider, GpsDataManager>`:
```dart
ChangeNotifierProxyProvider<OverlayProvider, GpsDataManager>(
create: (_) => GpsDataManager(),
update: (_, overlay, gpsManager) => gpsManager!..setOverlayProvider(overlay),
),
```
- WAIT: This creates a circular dependency! OverlayProvider depends on GpsDataManager (via ProxyProvider2), and now GpsDataManager would depend on OverlayProvider. Provider doesn't allow circular proxy dependencies.
- **Better approach**: Keep GpsDataManager as a plain `ChangeNotifierProvider`. Wire the overlay reference from SpeedometerScreen's `initState()` (after post-frame callback, when both providers are available):
```dart
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeGpsManager();
context.read<OverlayProvider>().onError = _handleOverlayError;
// Wire overlay provider into GPS manager for lifecycle decisions
context.read<GpsDataManager>().setOverlayProvider(context.read<OverlayProvider>());
});
```
This is already in `initState()` inside the post-frame callback, so just add the one line.
- **Actually, even better**: Wire it in main.dart without circular deps by using a regular Provider and a post-creation callback. But the simplest reliable approach is SpeedometerScreen's initState, which already has the post-frame callback pattern. Go with that.
- So main.dart changes are MINIMAL or NONE. The wiring happens in SpeedometerScreen's initState (already modified in Task 1).
- **Update SpeedometerScreen's initState (addendum to Task 1)**: Add this line in the existing `addPostFrameCallback`:
```dart
context.read<GpsDataManager>().setOverlayProvider(context.read<OverlayProvider>());
```
**TimingConfig cleanup** (`lib/config/timing_constants.dart`):
- If Plan 01's Task 1 left the old `GPS_GRACE_PERIOD` (30s) constant, check if anything still references it after the OverlayProvider cleanup above.
- If nothing references it: remove `GPS_GRACE_PERIOD` from TimingConfig
- If something still references it: leave it with a deprecation comment
</action>
<verify>
Run `cd J:/dev/gps_speedo && flutter analyze` -- should pass with no new errors. Verify OverlayProvider has no grace period timer. Verify GpsDataManager.setOverlayProvider() is called in SpeedometerScreen's initState. Verify no unused imports. Check that `GPS_GRACE_PERIOD` constant is either removed or still has a consumer.
</verify>
<done>
OverlayProvider's Phase 3 grace period placeholder removed. GpsDataManager receives overlay provider reference via SpeedometerScreen's initState wiring. TimingConfig cleaned of unused constants. Flutter analyze passes. The full GPS lifecycle chain works: GpsDataManager detects background -> checks overlay state -> decides GPS fate.
</done>
</task>
</tasks>
<verification>
1. `cd J:/dev/gps_speedo && flutter analyze` passes with no new errors
2. Main speed display dims at 3s, shows dashes at 10s (same thresholds as overlay)
3. SpeedometerScreen has NO WakelockPlus references
4. SpeedometerScreen has NO WidgetsBindingObserver
5. OverlayProvider has NO grace period timer (30s placeholder removed)
6. GpsDataManager.setOverlayProvider() called in SpeedometerScreen initState
7. Staleness check timer runs periodically and is canceled in dispose()
8. Heading/compass also dims with staleness
9. All timers canceled in dispose() across all modified files
10. No circular provider dependencies
</verification>
<success_criteria>
- Flutter analyze passes with no new errors
- Main app and overlay have identical staleness behavior (3s dim, 10s dash, immediate snap-back)
- Wake lock management centralized in GpsDataManager (not in SpeedometerScreen)
- Lifecycle management centralized in GpsDataManager (not in SpeedometerScreen)
- OverlayProvider is clean -- no placeholder infrastructure from Phase 3
- GpsDataManager has overlay provider reference for background decisions
- No regressions in overlay toggle, theme/unit cycling, error handling
</success_criteria>
<output>
After completion, create `.planning/phases/04-gps-power-optimization/04-02-SUMMARY.md`
</output>