From e4d4d90bce6b677beae96f1546ccf8f68f848e2c Mon Sep 17 00:00:00 2001 From: Jan Jetze Date: Fri, 6 Mar 2026 18:29:34 +0100 Subject: [PATCH] feat: show step count per activity session Sum sensor window steps within each session's time range and display the count next to the duration in the activity log. Also fixes stale KDoc references to 5-second windows (now 30-second). Co-Authored-By: Claude Opus 4.6 --- .../com/podometer/data/db/SensorWindow.kt | 2 +- .../podometer/domain/model/ActivitySession.kt | 2 ++ .../RecomputeActivitySessionsUseCase.kt | 23 +++++++++++++++++-- .../com/podometer/ui/dashboard/ActivityLog.kt | 14 ++++++++++- app/src/main/res/values/strings.xml | 1 + .../RecomputeActivitySessionsUseCaseTest.kt | 21 +++++++++++++++++ 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/podometer/data/db/SensorWindow.kt b/app/src/main/java/com/podometer/data/db/SensorWindow.kt index 1341a57..21ba2b8 100644 --- a/app/src/main/java/com/podometer/data/db/SensorWindow.kt +++ b/app/src/main/java/com/podometer/data/db/SensorWindow.kt @@ -5,7 +5,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey /** - * Room entity storing a single 5-second classifier window of raw sensor data. + * Room entity storing a single 30-second classifier window of raw sensor data. * * These windows are recorded continuously by [com.podometer.service.StepTrackingService] * and retained for 7 days. They enable retroactive recomputation of activity sessions diff --git a/app/src/main/java/com/podometer/domain/model/ActivitySession.kt b/app/src/main/java/com/podometer/domain/model/ActivitySession.kt index 83f68d8..2d2f5c7 100644 --- a/app/src/main/java/com/podometer/domain/model/ActivitySession.kt +++ b/app/src/main/java/com/podometer/domain/model/ActivitySession.kt @@ -16,6 +16,7 @@ package com.podometer.domain.model * this session. Used for override/reclassification. * @property isManualOverride True when the opening transition was manually * overridden by the user. + * @property stepCount Total steps detected during this session. */ data class ActivitySession( val activity: ActivityState, @@ -23,6 +24,7 @@ data class ActivitySession( val endTime: Long?, val startTransitionId: Int, val isManualOverride: Boolean, + val stepCount: Int = 0, ) /** diff --git a/app/src/main/java/com/podometer/domain/usecase/RecomputeActivitySessionsUseCase.kt b/app/src/main/java/com/podometer/domain/usecase/RecomputeActivitySessionsUseCase.kt index 7a78ce3..1f7e551 100644 --- a/app/src/main/java/com/podometer/domain/usecase/RecomputeActivitySessionsUseCase.kt +++ b/app/src/main/java/com/podometer/domain/usecase/RecomputeActivitySessionsUseCase.kt @@ -21,7 +21,7 @@ import javax.inject.Singleton * activity sessions for a given date. * * This enables retroactive recomputation when classifier parameters change — - * the raw 5-second windows are retained for 7 days and can be replayed at any + * the raw 30-second windows are retained for 7 days and can be replayed at any * time to generate updated session data. */ interface RecomputeActivitySessionsUseCase { @@ -94,7 +94,26 @@ class RecomputeActivitySessionsUseCaseImpl @Inject constructor( } } - return buildActivitySessions(transitions, nowMillis) + val sessions = buildActivitySessions(transitions, nowMillis) + return attachStepCounts(sessions, windows) + } + + /** + * Sums [SensorWindow.stepCount] for each session's time range and returns + * sessions with [ActivitySession.stepCount] populated. + */ + private fun attachStepCounts( + sessions: List, + windows: List, + ): List { + if (sessions.isEmpty() || windows.isEmpty()) return sessions + return sessions.map { session -> + val endTime = session.endTime ?: Long.MAX_VALUE + val steps = windows + .filter { it.timestamp in session.startTime until endTime } + .sumOf { it.stepCount } + session.copy(stepCount = steps) + } } } } diff --git a/app/src/main/java/com/podometer/ui/dashboard/ActivityLog.kt b/app/src/main/java/com/podometer/ui/dashboard/ActivityLog.kt index 341f1d3..ff6cf8b 100644 --- a/app/src/main/java/com/podometer/ui/dashboard/ActivityLog.kt +++ b/app/src/main/java/com/podometer/ui/dashboard/ActivityLog.kt @@ -284,9 +284,19 @@ private fun ActivityLogItem( text = durationText, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.weight(1f), ) + if (session.stepCount > 0) { + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.activity_session_steps, session.stepCount), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + if (session.isManualOverride) { Spacer(modifier = Modifier.width(8.dp)) AssistChip( @@ -408,6 +418,7 @@ private fun PreviewActivityLogMultiple() { endTime = 10L * hour + 30L * 60_000L, startTransitionId = 1, isManualOverride = false, + stepCount = 1247, ), ActivitySession( activity = ActivityState.CYCLING, @@ -422,6 +433,7 @@ private fun PreviewActivityLogMultiple() { endTime = null, startTransitionId = 3, isManualOverride = false, + stepCount = 312, ), ) PodometerTheme { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d67d92d..af1548b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,6 +70,7 @@ ongoing + %1$d steps %1$s session, %2$s, %3$s Override options for session at %1$s diff --git a/app/src/test/java/com/podometer/domain/usecase/RecomputeActivitySessionsUseCaseTest.kt b/app/src/test/java/com/podometer/domain/usecase/RecomputeActivitySessionsUseCaseTest.kt index e0292d5..5a900e0 100644 --- a/app/src/test/java/com/podometer/domain/usecase/RecomputeActivitySessionsUseCaseTest.kt +++ b/app/src/test/java/com/podometer/domain/usecase/RecomputeActivitySessionsUseCaseTest.kt @@ -50,6 +50,27 @@ class RecomputeActivitySessionsUseCaseTest { assertEquals(ActivityState.WALKING, sessions[0].activity) } + @Test + fun `replayWindows attaches step counts to walking sessions`() { + val windows = (0 until 40).map { i -> + SensorWindow( + id = i.toLong(), + timestamp = 1_000_000L + i * 5_000L, + magnitudeVariance = 1.0, + stepFrequencyHz = 1.8, + stepCount = 9, + ) + } + + val sessions = RecomputeActivitySessionsUseCaseImpl.replayWindows( + windows = windows, + nowMillis = nowMillis, + ) + + assertTrue("Expected at least one session", sessions.isNotEmpty()) + assertTrue("Walking session should have steps > 0", sessions[0].stepCount > 0) + } + @Test fun `replayWindows detects cycling session from sustained cycling windows`() { // First, we need some STILL state, then transition to cycling.