From e0df3913fe451056e6c5490f02faf3e8b2f2d33d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 15:41:28 +0000 Subject: Add Room database as track summary cache Adds TrackEntity/TrackDao/NavDatabase (Room v2.6.1). TrackRepository writes to Room on stopTrack() and importTrack(), and backfills existing tracks on first cold-start when the Room cache is empty. GPX/SAF remains source-of-truth for reinstall; Room is a fast metadata cache for future list-view optimisation. https://claude.ai/code/session_01KXYBuAHZkvv63DeUG6XaZD --- .../main/kotlin/org/terst/nav/NavApplication.kt | 6 +++- .../main/kotlin/org/terst/nav/db/NavDatabase.kt | 26 ++++++++++++++++ .../main/kotlin/org/terst/nav/track/TrackDao.kt | 25 ++++++++++++++++ .../main/kotlin/org/terst/nav/track/TrackEntity.kt | 35 ++++++++++++++++++++++ .../kotlin/org/terst/nav/track/TrackRepository.kt | 14 ++++++++- 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/db/NavDatabase.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/track/TrackDao.kt create mode 100644 android-app/app/src/main/kotlin/org/terst/nav/track/TrackEntity.kt (limited to 'android-app/app/src/main/kotlin') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt index 0fe5708..3873002 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt @@ -2,6 +2,7 @@ package org.terst.nav import android.app.Application import com.google.firebase.crashlytics.FirebaseCrashlytics +import org.terst.nav.db.NavDatabase import org.terst.nav.settings.FeatureFlags import java.io.File import java.io.PrintWriter @@ -21,6 +22,8 @@ class NavApplication : Application() { lateinit var trackRepository: org.terst.nav.track.TrackRepository lateinit var boatProfileRepository: org.terst.nav.tripreport.BoatProfileRepository lateinit var vesselRepository: org.terst.nav.vessel.VesselRepository + lateinit var database: NavDatabase + private set var isTesting: Boolean = false get() { if (field) return true @@ -36,8 +39,9 @@ class NavApplication : Application() { override fun onCreate() { super.onCreate() featureFlags = FeatureFlags(this) + database = NavDatabase.get(this) logbookRepository = org.terst.nav.logbook.LogbookRepository(org.terst.nav.logbook.LogbookStorage(this)) - trackRepository = org.terst.nav.track.TrackRepository(this) + trackRepository = org.terst.nav.track.TrackRepository(this, database.trackDao()) boatProfileRepository = org.terst.nav.tripreport.BoatProfileRepository(this) vesselRepository = org.terst.nav.vessel.VesselRepository(this) FirebaseCrashlytics.getInstance().sendUnsentReports() diff --git a/android-app/app/src/main/kotlin/org/terst/nav/db/NavDatabase.kt b/android-app/app/src/main/kotlin/org/terst/nav/db/NavDatabase.kt new file mode 100644 index 0000000..ccef58b --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/db/NavDatabase.kt @@ -0,0 +1,26 @@ +package org.terst.nav.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import org.terst.nav.track.TrackDao +import org.terst.nav.track.TrackEntity + +@Database(entities = [TrackEntity::class], version = 1, exportSchema = false) +abstract class NavDatabase : RoomDatabase() { + + abstract fun trackDao(): TrackDao + + companion object { + @Volatile private var INSTANCE: NavDatabase? = null + + fun get(context: Context): NavDatabase = INSTANCE ?: synchronized(this) { + INSTANCE ?: Room.databaseBuilder( + context.applicationContext, + NavDatabase::class.java, + "nav.db" + ).build().also { INSTANCE = it } + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDao.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDao.kt new file mode 100644 index 0000000..9c628df --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDao.kt @@ -0,0 +1,25 @@ +package org.terst.nav.track + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface TrackDao { + + @Query("SELECT * FROM tracks ORDER BY startMs DESC") + suspend fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: TrackEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(entities: List) + + @Query("DELETE FROM tracks") + suspend fun clear() + + @Query("SELECT COUNT(*) FROM tracks") + suspend fun count(): Int +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackEntity.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackEntity.kt new file mode 100644 index 0000000..502bfba --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackEntity.kt @@ -0,0 +1,35 @@ +package org.terst.nav.track + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "tracks") +data class TrackEntity( + @PrimaryKey val startMs: Long, + val distanceNm: Double, + val durationMs: Long, + val maxSogKt: Double, + val avgSogKt: Double, + val avgWindKt: Double?, + val avgWaveHeightM: Double?, + val tackCount: Int, + val startLat: Double, + val startLon: Double, + val endLat: Double, + val endLon: Double +) + +fun SavedTrack.toEntity(): TrackEntity = TrackEntity( + startMs = startMs, + distanceNm = summary.distanceNm, + durationMs = summary.durationMs, + maxSogKt = summary.maxSogKt, + avgSogKt = summary.avgSogKt, + avgWindKt = summary.avgWindKt, + avgWaveHeightM = summary.avgWaveHeightM, + tackCount = tacks.size, + startLat = points.first().lat, + startLon = points.first().lon, + endLat = points.last().lat, + endLon = points.last().lon +) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index c40c9c2..019f169 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.terst.nav.ui.NavLogger -class TrackRepository(context: Context) { +class TrackRepository(context: Context, private val trackDao: TrackDao) { private val storage = TrackStorage(context) @@ -58,6 +58,8 @@ class TrackRepository(context: Context) { Log.e("TrackRepository", "GPX save failed — ${points.size} points will be lost on restart") NavLogger.e("track", "saveTrack returned false — ${points.size} pts lost on restart") } + val savedTrack = SavedTrack(points, trackStartMs, summary, TackDetector.detectTacks(points)) + runCatching { trackDao.insert(savedTrack.toEntity()) } summary } @@ -72,6 +74,8 @@ class TrackRepository(context: Context) { if (saved) { _pastTracks.add(0, points) NavLogger.i("track", "importTrack: saved ${points.size} pts startMs=$startMs") + val savedTrack = points.toSavedTrack() + runCatching { trackDao.insert(savedTrack.toEntity()) } } else { NavLogger.e("track", "importTrack: save failed for ${points.size} pts") } @@ -125,6 +129,14 @@ class TrackRepository(context: Context) { _pastTracks.addAll(stored) NavLogger.i("track", "getPastTracks: total in memory=${_pastTracks.size}") + if (stored.isNotEmpty() && trackDao.count() == 0) { + // One-time backfill of Room cache from existing GPX files + val entities = stored.mapNotNull { pts -> + runCatching { pts.toSavedTrack().toEntity() }.getOrNull() + } + if (entities.isNotEmpty()) trackDao.insertAll(entities) + NavLogger.i("track", "getPastTracks: backfilled ${entities.size} tracks into Room") + } } _pastTracks.toList() } -- cgit v1.2.3