Files
gps-speedo/.planning/phases/04-gps-power-optimization/04-01-PLAN.md
T
Mikkeli Matlock 40c74677be 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>
2026-02-12 08:50:46 +09:00

15 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 01 execute 1
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
true
truths artifacts key_links
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
path provides contains
lib/models/processed_gps_data.dart ProcessedGpsData with timestamp field and copyWith support DateTime timestamp
path provides contains
lib/config/gps_constants.dart Precision switching threshold constants in m/s PRECISION_UP_THRESHOLD_MPS
path provides contains
lib/config/timing_constants.dart Background grace period duration constant BACKGROUND_GRACE_PERIOD
path provides contains
lib/services/gps_data_manager.dart Lifecycle-aware GPS engine with precision switching and wake lock WidgetsBindingObserver
path provides contains
lib/gps_service.dart Dynamic position stream accepting LocationAccuracy parameter LocationAccuracy accuracy
from to via pattern
lib/services/gps_data_manager.dart lib/gps_service.dart createPositionStream with dynamic accuracy GpsService.createPositionStream
from to via pattern
lib/services/gps_data_manager.dart lib/providers/overlay_provider.dart overlay state check for background decisions _overlayProvider.*isOverlayActive
from to via pattern
lib/services/gps_data_manager.dart wakelock_plus conditional wake lock based on tracking + visibility state WakelockPlus.(enable|disable)
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.

<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

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

Task 1: Add timestamp to ProcessedGpsData and precision/lifecycle constants lib/models/processed_gps_data.dart, lib/config/gps_constants.dart, lib/config/timing_constants.dart, lib/gps_service.dart **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:
    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. 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. 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.

Task 2: Implement lifecycle-aware GPS engine with precision switching and wake lock lib/services/gps_data_manager.dart **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:
    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 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(). 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.
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()

<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>
After completion, create `.planning/phases/04-gps-power-optimization/04-01-SUMMARY.md`