Activity uploads to Intervals.icu were showing incorrect durations - typically 2 minutes shorter than the actual workout time displayed in the app.
The workout system maintained two independent time tracking mechanisms:
-
Wall-Clock Timer (
elapsedSeconds+_previouslyElapsedTime)- Used
DateTime.now()to track actual elapsed time - Accumulated time across pause/resume cycles
- Included system overhead and timing jitter
- Used
-
Progress Timer (
_workoutProgressTime)- Incremented by exactly 0.1 seconds per timer tick
- Represented the workout's intended progress
- Not affected by system overhead
-
Track Points (stored in FIT/GPX files)
- Used
DateTime.now()for timestamps - Duration calculated from first to last track point timestamps
- Reflected wall-clock time with overhead
- Used
- App UI showed
elapsedSeconds(wall-clock time) - Exported files duration calculated from track point timestamps (wall-clock time)
- Timer overhead accumulated over time (e.g., ~2 mins for 30-min workout)
- Result: Mismatch between displayed time and exported file duration
Changed to use _workoutProgressTime as the single authoritative time source:
-
Eliminated
elapsedSecondsvariable- Now a computed getter:
int get elapsedSeconds => _workoutProgressTime.round()
- Now a computed getter:
-
Eliminated
_previouslyElapsedTimetracking- No longer needed since we don't track wall-clock segments
-
Track Point Timestamps
- Changed from:
DateTime.now() - Changed to:
_workoutStartTime + Duration(milliseconds: _workoutProgressTime * 1000) - Track points now based on workout progress, not wall clock
- Changed from:
-
Benefits
- UI elapsed time = workout progress time
- Track point duration = workout progress time
- Exported file duration = workout progress time
- Perfect consistency - no more drift or discrepancies
- Removed
elapsedSecondsas a variable, added as getter - Removed
_previouslyElapsedTimevariable - Changed
_lastTrackPointTimefromDateTime?todouble(workout seconds) - Updated track point creation to use calculated timestamps
- Simplified pause/resume logic
- Updated
togglePlayPause()to not track wall-clock segments
- Removed
elapsedSecondsparameter fromsaveWorkoutState() - Removed
_elapsedSecondsKeyconstant - Cleaned up state loading/saving logic
✅ Workout duration consistency across app and uploads ✅ Simplified time tracking logic (removed duplicate system) ✅ No more timer drift accumulation ✅ Easier to debug and maintain
✅ Pause/resume still works correctly ✅ Background workout continuation still works ✅ Workout state persistence still works ✅ All existing tests pass
The existing test/erg_workout_test.dart validates the fix:
- Creates a 2-hour workout simulation
- Generates GPX and FIT files
- Verifies export functionality
Additional manual testing recommended:
-
Complete a 30-minute workout without pausing
- Verify app shows 30:00 elapsed time
- Verify uploaded file shows 30:00 duration
-
Complete a 30-minute workout with 2 pauses
- Verify app shows 30:00 elapsed time
- Verify uploaded file shows 30:00 duration
-
Navigate away from workout screen and back during active workout
- Verify time continues correctly
- Verify uploaded file has correct duration
No user data migration needed. Existing workout states will load correctly:
progressPositionand_workoutProgressTimeare preservedelapsedSecondsin saved state is ignored (no longer used)- First run after update will work seamlessly