git.iko.soy/opencode-android
on 06fd41b help sign in
64 lines 2.8 kB history raw
1package soy.iko.opencode.data.repo
3import android.content.Context
4import kotlinx.coroutines.Dispatchers
5import kotlinx.coroutines.sync.Mutex
6import kotlinx.coroutines.sync.withLock
7import kotlinx.coroutines.withContext
8import kotlinx.serialization.Serializable
9import kotlinx.serialization.decodeFromString
10import kotlinx.serialization.encodeToString
11import kotlinx.serialization.json.Json
12import java.io.File
14/** A minimal session summary persisted for the home-screen widget and launcher shortcuts. */
15@Serializable
16data class RecentSession(val id: String, val title: String)
18/**
19 * Persists a short list of recent sessions to a small JSON file under filesDir so app-external
20 * surfaces (the home-screen widget's [android.widget.RemoteViewsService] and the "Resume last"
21 * launcher shortcut) can read them without a live connection or in-memory state. Both the app
22 * and the widget's RemoteViews factory run in the same process, so a plain file is sufficient.
23 */
24object RecentSessionsStore {
26 private const val FILE = "recent_sessions.json"
27 const val MAX = 8
29 private val json = Json { ignoreUnknownKeys = true }
31 /** Serializes concurrent [write]s. The synchronous [read] (called from the widget's
32 * RemoteViewsService) can't take a suspend lock, so [write] instead swaps the file in
33 * atomically via a temp-file rename — the reader always sees either the old or the new
34 * complete file, never a half-written one. */
35 private val writeMutex = Mutex()
37 private fun file(context: Context) = File(context.applicationContext.filesDir, FILE)
39 /** Synchronous read — safe to call from the widget factory (bounded, tiny file). */
40 fun read(context: Context): List<RecentSession> {
41 val f = file(context).takeIf { it.exists() } ?: return emptyList()
42 return runCatching { json.decodeFromString<List<RecentSession>>(f.readText()) }
43 .getOrDefault(emptyList())
44 }
46 /** Persist [sessions] (capped to [MAX]); a no-op empty list writes an empty file. */
47 suspend fun write(context: Context, sessions: List<RecentSession>) = withContext(Dispatchers.IO) {
48 writeMutex.withLock {
49 runCatching {
50 val target = file(context)
51 val encoded = json.encodeToString(sessions.take(MAX))
52 val tmp = File(target.parentFile, "$FILE.tmp")
53 tmp.writeText(encoded)
54 // Atomic replace on POSIX filesystems (app's internal storage); the reader
55 // never observes a torn file. Fall back to a direct write if rename fails.
56 if (!tmp.renameTo(target)) {
57 target.writeText(encoded)
58 tmp.delete()
59 }
60 }
61 Unit
62 }
63 }