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>
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-gps-power-optimization | 02 | execute | 2 |
|
|
true |
|
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.
<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>
@.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.mdSource 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
Task 1: Add staleness detection to main speed display and clean up SpeedometerScreen lib/screens/speedometer_screen.dart **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()fromdispose() - Remove
_enableWakelock()call frominitState() - Remove
_enableWakelock()call from_handleBackgroundTransition() - Wake lock is now managed by GpsDataManager (Plan 01)
Remove WidgetsBindingObserver from SpeedometerScreen:
- Remove
with WidgetsBindingObserverfrom class declaration - Remove
WidgetsBinding.instance.addObserver(this)frominitState() - Remove
WidgetsBinding.instance.removeObserver(this)fromdispose() - Remove
didChangeAppLifecycleState()override entirely - Remove
_isInBackgroundfield - Remove
_handleBackgroundTransition()method - Remove
_requestBatteryOptimizationExemption()method (empty stub) - Lifecycle management is now in GpsDataManager (Plan 01)
Keep background heartbeat but simplify:
- Keep
_backgroundHeartbeatTimerand_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
_isInBackgroundcheck (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
contextis the only way to access OverlayProvider.pushCurrentData(). Keep it running periodically, checking onlyoverlay.isOverlayActive.
- Remove the
Add staleness detection to speed display:
- Import
../config/overlay_constants.dartforOverlayConfig.STALENESS_DIM_THRESHOLD,STALENESS_DASH_THRESHOLD,STALENESS_DIM_OPACITY - Add a
Timer? _stalenessCheckTimerfield - In
initState(), start a periodic timer (1s interval, same as overlay'sSTALENESS_CHECK_INTERVAL):_stalenessCheckTimer = Timer.periodic(OverlayConfig.STALENESS_CHECK_INTERVAL, (_) { if (mounted) setState(() {}); // Trigger rebuild to re-evaluate staleness }); - Cancel
_stalenessCheckTimerindispose()
Modify speed display builders to apply staleness:
- In both
_buildPortraitLayoutand_buildLandscapeLayout, the speedSelector<GpsDataManager, double>needs to change:- Change selector to select the full
ProcessedGpsDataobject (or at minimum speed + timestamp):Selector<GpsDataManager, ProcessedGpsData> - Selector:
(_, gps) => gps.currentData - In the builder, calculate staleness:
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
staleOpacityto 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
- Change selector to select the full
Apply staleness to heading/compass section too:
- The compass
Selector<GpsDataManager, (double heading, String displayHeading)>should also check staleness - Change to select
ProcessedGpsDataor add timestamp to the tuple - Apply
staleOpacityto 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) Runcd 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(). 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.
- Remove the Phase 3 grace period placeholder:
- Remove
Timer? _gracePeriodTimerfield - Remove
_gracePeriodTimer?.cancel()fromdispose() - Remove grace period logic from
_handleOverlayClose():// DELETE these lines: _gracePeriodTimer?.cancel(); _gracePeriodTimer = Timer(TimingConfig.GPS_GRACE_PERIOD, () { Logger.info('GPS grace period expired', 'OverlayProvider'); _gracePeriodTimer = null; }); - Remove
_gracePeriodTimer?.cancel()fromshowOverlay()(the "cancel grace period if user reopens" line) - Grace period is now managed by GpsDataManager's background lifecycle handler
- Remove
- Remove
import '../config/timing_constants.dart'IFTimingConfigis no longer referenced anywhere in this file (checkTAP_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
ChangeNotifierProvidertoChangeNotifierProxyProvider<OverlayProvider, GpsDataManager>: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.
- Change GpsDataManager from
-
Better approach: Keep GpsDataManager as a plain
ChangeNotifierProvider. Wire the overlay reference from SpeedometerScreen'sinitState()(after post-frame callback, when both providers are available):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: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_PERIODfrom TimingConfig - If something still references it: leave it with a deprecation comment
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 thatGPS_GRACE_PERIODconstant is either removed or still has a consumer. 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.
<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>