Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ android {
}

buildTypes {
debug {
// applicationIdSuffix = ".dev" // Temporarily disabled for one-time migration
resValue("string", "app_name", "Podometer-dev")
}
release {
resValue("string", "app_name", "Podometer")
isMinifyEnabled = true
isShrinkResources = true
signingConfig = if (System.getenv("RELEASE_KEYSTORE_PATH") != null) {
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/podometer/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class MainActivity : ComponentActivity() {
onSetAutoStartEnabled = viewModel::setAutoStartEnabled,
onSetNotificationStyle = viewModel::setNotificationStyle,
onExportData = viewModel::exportData,
onImportData = viewModel::importData,
onResetExportState = viewModel::resetExportState,
onNavigateToDonate = {
navController.navigate(Screen.Donate.route)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ interface ActivityTransitionDao {
@Insert
suspend fun insertTransition(transition: ActivityTransition)

/** Inserts multiple [ActivityTransition] rows. */
@Insert
suspend fun insertAllTransitions(transitions: List<ActivityTransition>)

/** Updates an existing [ActivityTransition] row (matched by primary key). */
@Update
suspend fun updateTransition(transition: ActivityTransition)
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/podometer/data/db/CyclingSessionDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ interface CyclingSessionDao {
@Insert
suspend fun insertSession(session: CyclingSession): Long

/** Inserts multiple [CyclingSession] rows. */
@Insert
suspend fun insertAllSessions(sessions: List<CyclingSession>)

/** Updates an existing [CyclingSession] row (matched by primary key). */
@Update
suspend fun updateSession(session: CyclingSession)
Expand Down
31 changes: 30 additions & 1 deletion app/src/main/java/com/podometer/data/db/PodometerDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
* daily summaries, and cycling sessions.
* 2 — Added sensor_windows table for raw classifier window storage
* (7-day retention, ~6 MB).
* 3 — Consolidate 5-second sensor windows into 30-second windows.
* Keeps the window with the highest magnitudeVariance per 30 s slot.
*/
@Database(
entities = [
Expand All @@ -23,7 +25,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
CyclingSession::class,
SensorWindow::class,
],
version = 2,
version = 3,
exportSchema = false,
)
abstract class PodometerDatabase : RoomDatabase() {
Expand Down Expand Up @@ -55,5 +57,32 @@ abstract class PodometerDatabase : RoomDatabase() {
)
}
}

/**
* Migration from version 2 to 3: consolidate 5-second sensor windows
* into 30-second windows.
*
* For each 30-second time slot (timestamp / 30000), keeps only the
* window with the highest magnitudeVariance. This preserves the
* cycling signal (which depends on variance) while reducing storage
* by ~6x. The stepFrequencyHz already encodes a 30-second sliding
* window, so no information is lost from the step frequency perspective.
*/
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
DELETE FROM sensor_windows WHERE id NOT IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY timestamp / 30000
ORDER BY magnitudeVariance DESC
) AS rn FROM sensor_windows
) WHERE rn = 1
)
""".trimIndent(),
)
}
}
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/com/podometer/data/db/SensorWindowDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ interface SensorWindowDao {
@Query("SELECT * FROM sensor_windows WHERE timestamp BETWEEN :startMs AND :endMs ORDER BY timestamp")
fun getWindowsBetween(startMs: Long, endMs: Long): Flow<List<SensorWindow>>

/**
* Returns all sensor windows ordered by timestamp ascending.
* One-shot suspend query intended for data export — not a [Flow].
*/
@Query("SELECT * FROM sensor_windows ORDER BY timestamp ASC")
suspend fun getAllWindows(): List<SensorWindow>

/**
* Deletes all sensor windows older than [cutoffMs].
*
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/podometer/data/db/StepDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ interface StepDao {
""")
suspend fun addCyclingMinutes(date: String, minutes: Int)

/** Inserts multiple [DailySummary] rows, replacing on conflict. */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllDailySummaries(summaries: List<DailySummary>)

/** Inserts multiple [HourlyStepAggregate] rows, replacing on conflict. */
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllHourlyAggregates(aggregates: List<HourlyStepAggregate>)

/**
* Returns all [DailySummary] rows ordered by date ascending.
* One-shot suspend query intended for data export — not a [Flow].
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/com/podometer/data/export/ExportModels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ data class ExportData(
val activityTransitions: List<ExportActivityTransition>,
/** All cycling sessions, ordered by start time ascending. */
val cyclingSessions: List<ExportCyclingSession>,
/** All raw sensor classifier windows, ordered by timestamp ascending. */
val sensorWindows: List<ExportSensorWindow> = emptyList(),
)

/**
Expand Down Expand Up @@ -104,3 +106,20 @@ data class ExportCyclingSession(
/** True when the user manually created or overrode this session. */
val isManualOverride: Boolean,
)

/**
* Export model mirroring the Room [com.podometer.data.db.SensorWindow] entity.
*/
@Serializable
data class ExportSensorWindow(
/** Database row ID. */
val id: Long,
/** Epoch-millisecond timestamp for the start of this window. */
val timestamp: Long,
/** Accelerometer magnitude variance (m/s²)² for this window. */
val magnitudeVariance: Double,
/** Step cadence in Hz. */
val stepFrequencyHz: Double,
/** Number of steps detected during this window. */
val stepCount: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ data class WindowFeatures(
* so contention overhead is negligible.
*
* @param capacity Maximum number of samples to retain. Default: [DEFAULT_CAPACITY]
* (75 slots = 15 seconds at ~5 Hz SENSOR_DELAY_NORMAL rate).
* (150 slots = 30 seconds at ~5 Hz SENSOR_DELAY_NORMAL rate).
*/
class AccelerometerSampleBuffer(val capacity: Int = DEFAULT_CAPACITY) {

Expand Down Expand Up @@ -184,10 +184,10 @@ class AccelerometerSampleBuffer(val capacity: Int = DEFAULT_CAPACITY) {
/**
* Default circular buffer capacity.
*
* 75 slots covers 15 seconds at SENSOR_DELAY_NORMAL (~5 Hz delivery rate).
* This is sufficient for activity-classification sliding windows.
* 150 slots covers 30 seconds at SENSOR_DELAY_NORMAL (~5 Hz delivery rate),
* aligned with the 30-second classifier evaluation interval.
*/
const val DEFAULT_CAPACITY = 75
const val DEFAULT_CAPACITY = 150

/**
* Minimum number of samples required before [computeWindowFeatures] will
Expand Down
Loading