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