From 2539e5103521014d7dae61205f491a7c6e3d3744 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 22:42:22 +0000 Subject: [PATCH] refactor: take the last nine DataStores off the Context delegate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other store in the app now opens through AppPreferenceStores; these nine were still on androidx's `Context.preferencesDataStore`, which is the pattern the rest of this branch removed. They are the stores behind the napplet sandbox, the in-app browser, favorites, chess dismissals and the two calendar-reminder files. The names are unchanged, and that is the whole safety argument. The holder reproduces `filesDir/datastore/.preferences_pb`, which is exactly what the delegate resolved to, so every one of these opens the file it was already using: nothing migrates, and a rollback finds its data where it left it. A typo in one of these strings would silently orphan that store's data rather than fail, so they were copied across verbatim. The calendar migrations moved with their files. DataStore runs a file's migrations once, when that file is first opened, so they have to be attached to the file by the holder rather than by whichever caller happens to open it first — the same reason shared_settings' migration lives there. Two shapes changed rather than being mechanically translated: - The registries' `Context` receiver is gone. These stores are app-scoped now, so a receiver the body ignores would claim a dependency that is not real. The `appContext ?: return` guards stay: they mean "init() has run", which is still true and still worth keeping. - DataStoreNappletStorage and DataStoreNappletPermissionStore lose their Context secondary constructors instead of keeping an unused parameter. Both call sites pass the store directly. On the process boundary, since Amethyst.instance is deliberately unset in the `:napplet` sandbox and reaching it there throws: all four components declared `android:process=":napplet"` are napplethost.* classes from the :nappletHost module, NappletBrokerService declares no process and so runs in main, and onCreate's sandbox early-return precedes the one WebAppNetworkRegistry.init() call. All nine stores are main-process only. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L --- .../com/vitorpamplona/amethyst/AppModules.kt | 14 +++- .../favorites/BrowserHistoryRegistry.kt | 20 +++-- .../favorites/FavoriteAppsRegistry.kt | 24 ++++-- .../DataStoreNappletPermissionStore.kt | 7 -- .../napplet/DataStoreNappletStorage.kt | 7 -- .../amethyst/napplet/NappletBrokerService.kt | 2 +- .../napplet/NappletNetworkRegistry.kt | 20 +++-- .../amethyst/napplet/WebAppNetworkRegistry.kt | 21 ++++-- .../calendar/CalendarReminderStores.kt | 74 ++++++++++--------- .../calendar/CalendarReminderWorker.kt | 4 +- .../CalendarReminderSettingsScreen.kt | 2 +- .../loggedIn/chess/ChessDismissedGamesData.kt | 8 +- .../loggedIn/chess/ChessViewModelNew.kt | 2 +- 13 files changed, 124 insertions(+), 81 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 431f57e5c3..26697a4fcb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -79,8 +79,12 @@ import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilde import com.vitorpamplona.amethyst.model.torState.AccountsTorStateConnector import com.vitorpamplona.amethyst.model.torState.TorRelayState import com.vitorpamplona.amethyst.napplet.DataStoreNappletPermissionStore +import com.vitorpamplona.amethyst.service.calendar.CALENDAR_REMINDER_LOG_STORE +import com.vitorpamplona.amethyst.service.calendar.CALENDAR_REMINDER_SETTINGS_STORE import com.vitorpamplona.amethyst.service.calendar.CalendarReminderWorker +import com.vitorpamplona.amethyst.service.calendar.calendarReminderLogMigrations import com.vitorpamplona.amethyst.service.calendar.calendarReminderSettings +import com.vitorpamplona.amethyst.service.calendar.calendarReminderSettingsMigrations import com.vitorpamplona.amethyst.service.cast.CastRegistry import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager import com.vitorpamplona.amethyst.service.crashreports.CrashReportCache @@ -257,6 +261,8 @@ class AppModules( // One file per account, so the migration is per name rather than a constant. name.startsWith(CashuPreferences.FILE_PREFIX) -> listOf(CashuPreferences.legacyMigration(appContext, name.removePrefix(CashuPreferences.FILE_PREFIX))) + name == CALENDAR_REMINDER_SETTINGS_STORE -> calendarReminderSettingsMigrations(appContext) + name == CALENDAR_REMINDER_LOG_STORE -> calendarReminderLogMigrations(appContext) else -> emptyList() } }, @@ -855,8 +861,10 @@ class AppModules( */ val nappletAccountScope: () -> String = { sessionManager.loggedInAccount()?.pubKey ?: "" } - // Singleton stores for napplet permissions — DataStore v1 enforces one instance per file. - val nappletPermissionStore by lazy { DataStoreNappletPermissionStore(appContext, nappletAccountScope) } + // Singleton stores for napplet permissions. The holder is what enforces + // DataStore's one-instance-per-file rule now; this stays a lazy val so the + // ledger below and the broker share one object. + val nappletPermissionStore by lazy { DataStoreNappletPermissionStore(appStores.getDataStore("napplet_permissions"), nappletAccountScope) } /** * The one napplet permission ledger for the main process. Its persistent half is just the store @@ -1347,7 +1355,7 @@ class AppModules( Filter(kinds = listOf(CalendarDateSlotEvent.KIND, CalendarTimeSlotEvent.KIND)), ).conflate() .collect { - if (appContext.calendarReminderSettings().load().enabled && + if (calendarReminderSettings().load().enabled && CalendarReminderWorker.couldStillFire(CalendarReminderWorker.acceptedRsvpsInCache(), TimeUtils.now()) ) { CalendarReminderWorker.schedule(appContext) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryRegistry.kt index 5564139886..e9988b23e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryRegistry.kt @@ -21,9 +21,11 @@ package com.vitorpamplona.amethyst.favorites import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.quartz.nip01Core.core.JsonMapper import com.vitorpamplona.quartz.utils.Log @@ -37,7 +39,15 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -private val Context.browserHistoryDataStore by preferencesDataStore(name = "browser_history") +/** + * The browser-history file, on the app-wide holder rather than a `Context` delegate. + * Same path the delegate resolved to, so nothing migrates. + * + * Main process only: [Amethyst.instance] is deliberately unset in the + * `:napplet` sandbox. + */ +private val browserHistoryDataStore: DataStore + get() = Amethyst.instance.appStores.getDataStore("browser_history") /** * One device-local visited site, keyed by full [url]. [visitCount]/[lastVisitedAt] drive frecency ranking @@ -83,7 +93,7 @@ object BrowserHistoryRegistry { val ctx = context.applicationContext appContext = ctx scope.launch { - val json = ctx.browserHistoryDataStore.data.first()[KEY] + val json = browserHistoryDataStore.data.first()[KEY] val loaded = if (json != null) decode(json) else emptyList() // Merge disk under anything already recorded this session (session wins, newest-first). update { current -> dedupeNewestFirst(current + loaded) } @@ -136,9 +146,9 @@ object BrowserHistoryRegistry { } private fun persist(json: String) { - val ctx = appContext ?: return + appContext ?: return scope.launch { - ctx.browserHistoryDataStore.edit { it[KEY] = json } + browserHistoryDataStore.edit { it[KEY] = json } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppsRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppsRegistry.kt index 99e36cf19b..ad9d03d325 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppsRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppsRegistry.kt @@ -21,9 +21,11 @@ package com.vitorpamplona.amethyst.favorites import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.quartz.nip01Core.core.JsonMapper import com.vitorpamplona.quartz.utils.Log @@ -38,7 +40,15 @@ import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import java.util.concurrent.ConcurrentHashMap -private val Context.favoriteAppsDataStore by preferencesDataStore(name = "favorite_apps") +/** + * The favorite-apps file, on the app-wide holder rather than a `Context` delegate. + * Same path the delegate resolved to, so nothing migrates. + * + * Main process only: [Amethyst.instance] is deliberately unset in the + * `:napplet` sandbox. + */ +private val favoriteAppsDataStore: DataStore + get() = Amethyst.instance.appStores.getDataStore("favorite_apps") /** * The user's device-local list of [FavoriteApp]s — the single source of truth shared by the bottom @@ -81,7 +91,7 @@ object FavoriteAppsRegistry { val ctx = context.applicationContext appContext = ctx scope.launch { - val prefs = ctx.favoriteAppsDataStore.data.first() + val prefs = favoriteAppsDataStore.data.first() val loaded = prefs[KEY]?.let { decode(it) } ?: emptyList() // Don't clobber adds made in this session before hydration finished, and don't resurrect // anything the user removed in that same window. @@ -139,16 +149,16 @@ object FavoriteAppsRegistry { } private fun persist(json: String) { - val ctx = appContext ?: return + appContext ?: return scope.launch { - ctx.favoriteAppsDataStore.edit { it[KEY] = json } + favoriteAppsDataStore.edit { it[KEY] = json } } } private fun persistManifests(json: String) { - val ctx = appContext ?: return + appContext ?: return scope.launch { - ctx.favoriteAppsDataStore.edit { it[MANIFESTS_KEY] = json } + favoriteAppsDataStore.edit { it[MANIFESTS_KEY] = json } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/DataStoreNappletPermissionStore.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/DataStoreNappletPermissionStore.kt index 073d458f83..7550c41334 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/DataStoreNappletPermissionStore.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/DataStoreNappletPermissionStore.kt @@ -20,19 +20,15 @@ */ package com.vitorpamplona.amethyst.napplet -import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import com.vitorpamplona.amethyst.commons.napplet.NappletCapability import com.vitorpamplona.amethyst.commons.napplet.permissions.GrantState import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionStore import kotlinx.coroutines.flow.first -private val Context.nappletPermissionsDataStore by preferencesDataStore(name = "napplet_permissions") - /** * Persists the standing napplet grants ([GrantState.ALLOW_ALWAYS] / [GrantState.DENY]) in a * dedicated DataStore. Keyed by `"\u0000"` so a coordinate's grants can @@ -43,9 +39,6 @@ class DataStoreNappletPermissionStore( private val dataStore: DataStore, private val accountPubKey: () -> String, ) : NappletPermissionStore { - constructor(context: Context, accountPubKey: () -> String) : - this(context.applicationContext.nappletPermissionsDataStore, accountPubKey) - /** * Grants belong to one account. [accountPubKey] is read at call time, so an account switch moves * every read and write to that account's namespace with no rebuild — a grant made by one account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/DataStoreNappletStorage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/DataStoreNappletStorage.kt index 14d3499371..4c3141b6e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/DataStoreNappletStorage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/DataStoreNappletStorage.kt @@ -20,17 +20,13 @@ */ package com.vitorpamplona.amethyst.napplet -import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import com.vitorpamplona.amethyst.commons.napplet.NappletStorage import kotlinx.coroutines.flow.first -private val Context.nappletStorageDataStore by preferencesDataStore(name = "napplet_storage") - /** * DataStore-backed [NappletStorage]. Every key is prefixed with the **active account** and then the * applet's coordinate, so one napplet's keys can never collide with another's, one account's data is @@ -44,9 +40,6 @@ class DataStoreNappletStorage( private val dataStore: DataStore, private val accountPubKey: () -> String, ) : NappletStorage { - constructor(context: Context, accountPubKey: () -> String) : - this(context.applicationContext.nappletStorageDataStore, accountPubKey) - override suspend fun get( coordinate: String, key: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt index de5815af52..7f98885be1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt @@ -90,7 +90,7 @@ class NappletBrokerService : Service() { private val signerLedger by lazy { NostrSignerPermissionLedger(Amethyst.instance.signerPermissionStore) } // Per-applet sandboxed key-value store (namespaced by account + coordinate inside the impl). - private val storage by lazy { DataStoreNappletStorage(applicationContext, Amethyst.instance.nappletAccountScope) } + private val storage by lazy { DataStoreNappletStorage(Amethyst.instance.appStores.getDataStore("napplet_storage"), Amethyst.instance.nappletAccountScope) } private val incoming by lazy { Messenger(Handler(Looper.getMainLooper(), ::handleMessage)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletNetworkRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletNetworkRegistry.kt index 3dc4e46da8..1e95e1fb02 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletNetworkRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletNetworkRegistry.kt @@ -21,9 +21,11 @@ package com.vitorpamplona.amethyst.napplet import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore +import com.vitorpamplona.amethyst.Amethyst import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -32,7 +34,15 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap -private val Context.nappletNetworkDataStore by preferencesDataStore(name = "napplet_network") +/** + * The per-napplet routing file, on the app-wide holder rather than a `Context` delegate. + * Same path the delegate resolved to, so nothing migrates. + * + * Main process only: [Amethyst.instance] is deliberately unset in the + * `:napplet` sandbox. + */ +private val nappletNetworkDataStore: DataStore + get() = Amethyst.instance.appStores.getDataStore("napplet_network") /** * Per-nSite network-routing preference: whether a site's traffic goes through **Tor** (the default) @@ -69,7 +79,7 @@ object NappletNetworkRegistry { appContext = ctx hydration = scope.launch { - ctx.nappletNetworkDataStore.data.first().asMap().forEach { (key, value) -> + nappletNetworkDataStore.data.first().asMap().forEach { (key, value) -> // putIfAbsent: never clobber a choice made in this session before hydration finished. modes.putIfAbsent(key.name, value != OPEN_WEB) } @@ -95,9 +105,9 @@ object NappletNetworkRegistry { useTor: Boolean, ) { modes[coordinate] = useTor - val ctx = appContext ?: return + appContext ?: return scope.launch { - ctx.nappletNetworkDataStore.edit { it[stringPreferencesKey(coordinate)] = if (useTor) TOR else OPEN_WEB } + nappletNetworkDataStore.edit { it[stringPreferencesKey(coordinate)] = if (useTor) TOR else OPEN_WEB } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebAppNetworkRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebAppNetworkRegistry.kt index 2038417d1a..a274fb036c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebAppNetworkRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebAppNetworkRegistry.kt @@ -22,9 +22,11 @@ package com.vitorpamplona.amethyst.napplet import android.content.Context import androidx.core.net.toUri +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore +import com.vitorpamplona.amethyst.Amethyst import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -33,7 +35,16 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap -private val Context.webUrlNetworkDataStore by preferencesDataStore(name = "weburl_network") +/** + * The per-site routing file, on the app-wide holder rather than a `Context` + * delegate. Same path the delegate resolved to, so nothing migrates. + * + * Main process only: [Amethyst.instance] is deliberately unset in the + * `:napplet` sandbox, and this registry is only touched from the browser + * chrome that runs in the main process. + */ +private val webUrlNetworkDataStore: DataStore + get() = Amethyst.instance.appStores.getDataStore("weburl_network") /** * Per-web-client network-routing preference: whether a favorited URL / browsed site routes through @@ -67,7 +78,7 @@ object WebAppNetworkRegistry { appContext = ctx hydration = scope.launch { - ctx.webUrlNetworkDataStore.data.first().asMap().forEach { (key, value) -> + webUrlNetworkDataStore.data.first().asMap().forEach { (key, value) -> // putIfAbsent: never clobber a choice made in this session before hydration finished. modes.putIfAbsent(key.name, value != OPEN_WEB) } @@ -98,9 +109,9 @@ object WebAppNetworkRegistry { ) { val host = hostKeyOf(url) modes[host] = useTor - val ctx = appContext ?: return + appContext ?: return scope.launch { - ctx.webUrlNetworkDataStore.edit { it[stringPreferencesKey(host)] = if (useTor) TOR else OPEN_WEB } + webUrlNetworkDataStore.edit { it[stringPreferencesKey(host)] = if (useTor) TOR else OPEN_WEB } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderStores.kt index e7105b31b8..e430a79bc0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderStores.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderStores.kt @@ -21,9 +21,11 @@ package com.vitorpamplona.amethyst.service.calendar import android.content.Context +import androidx.datastore.core.DataMigration +import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.preferencesDataStore +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.preferences.CalendarReminderLogStore import com.vitorpamplona.amethyst.commons.model.preferences.CalendarReminderSettings import com.vitorpamplona.amethyst.commons.model.preferences.CalendarReminderSettingsStore @@ -34,43 +36,47 @@ import com.vitorpamplona.amethyst.commons.model.preferences.CopyOnceMigration * * The store classes live in commons; only the file location and the one-off * lift out of the legacy SharedPreferences are Android's business. + * + * Both files sit on `AppPreferenceStores` rather than a `Context` delegate. + * The names below are the delegate's names, and the holder reproduces the path + * it resolved to, so nothing migrates. The migrations move with them: DataStore + * runs a file's migrations once, when that file is first opened, so they have + * to be attached to the file by the holder rather than by whoever opens it. */ private const val LEGACY_SETTINGS_FILE = "amethyst_calendar_reminder_prefs" private const val LEGACY_LOG_FILE = "amethyst_calendar_reminders" -private val Context.calendarReminderSettingsData by preferencesDataStore( - name = "calendar_reminder_settings", - produceMigrations = { context -> - listOf( - CopyOnceMigration("migrated.calendarReminderSettings") { out -> - val legacy = context.getSharedPreferences(LEGACY_SETTINGS_FILE, Context.MODE_PRIVATE) - if (legacy.contains("enabled")) { - out[booleanPreferencesKey("enabled")] = legacy.getBoolean("enabled", CalendarReminderSettings.DEFAULT_ENABLED) - } - if (legacy.contains("lead_minutes")) { - out[intPreferencesKey("lead_minutes")] = legacy.getInt("lead_minutes", CalendarReminderSettings.DEFAULT_LEAD_MINUTES) - } - }, - ) - }, -) +/** Store (and file) names, as [Amethyst.appStores] keys them. */ +const val CALENDAR_REMINDER_SETTINGS_STORE = "calendar_reminder_settings" +const val CALENDAR_REMINDER_LOG_STORE = "calendar_reminder_log" -private val Context.calendarReminderLogData by preferencesDataStore( - name = "calendar_reminder_log", - produceMigrations = { context -> - listOf( - CopyOnceMigration("migrated.calendarReminderLog") { out -> - val legacy = context.getSharedPreferences(LEGACY_LOG_FILE, Context.MODE_PRIVATE) - // Values are the event-start times the reminders fired for; anything - // else in the file is not ours and is left behind. - legacy.all.forEach { (key, value) -> - if (value is Long) out[CalendarReminderLogStore.keyFor(key.removePrefix("notified:"))] = value - } - }, - ) - }, -) +/** The one-off copy of the reminder settings out of the legacy prefs file. */ +fun calendarReminderSettingsMigrations(context: Context): List> = + listOf( + CopyOnceMigration("migrated.calendarReminderSettings") { out -> + val legacy = context.getSharedPreferences(LEGACY_SETTINGS_FILE, Context.MODE_PRIVATE) + if (legacy.contains("enabled")) { + out[booleanPreferencesKey("enabled")] = legacy.getBoolean("enabled", CalendarReminderSettings.DEFAULT_ENABLED) + } + if (legacy.contains("lead_minutes")) { + out[intPreferencesKey("lead_minutes")] = legacy.getInt("lead_minutes", CalendarReminderSettings.DEFAULT_LEAD_MINUTES) + } + }, + ) -fun Context.calendarReminderSettings() = CalendarReminderSettingsStore(calendarReminderSettingsData) +/** The one-off copy of the fired-reminder log out of the legacy prefs file. */ +fun calendarReminderLogMigrations(context: Context): List> = + listOf( + CopyOnceMigration("migrated.calendarReminderLog") { out -> + val legacy = context.getSharedPreferences(LEGACY_LOG_FILE, Context.MODE_PRIVATE) + // Values are the event-start times the reminders fired for; anything + // else in the file is not ours and is left behind. + legacy.all.forEach { (key, value) -> + if (value is Long) out[CalendarReminderLogStore.keyFor(key.removePrefix("notified:"))] = value + } + }, + ) -fun Context.calendarReminderLog() = CalendarReminderLogStore(calendarReminderLogData) +fun calendarReminderSettings() = CalendarReminderSettingsStore(Amethyst.instance.appStores.getDataStore(CALENDAR_REMINDER_SETTINGS_STORE)) + +fun calendarReminderLog() = CalendarReminderLogStore(Amethyst.instance.appStores.getDataStore(CALENDAR_REMINDER_LOG_STORE)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt index 40e1134f8d..b3f34ae3ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt @@ -65,7 +65,7 @@ class CalendarReminderWorker( ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { runCatching { Amethyst.instance.resourceUsage.add(UsageKeys.workerRuns("calendarReminder"), 1) } - val settings = applicationContext.calendarReminderSettings().load() + val settings = calendarReminderSettings().load() if (!settings.enabled) { Log.d(TAG) { "Reminders disabled; ending periodic chain." } // The settings toggle re-schedules on enable; no reason to keep @@ -75,7 +75,7 @@ class CalendarReminderWorker( } val now = TimeUtils.now() val windowEnd = now + settings.leadMinutes * 60L - val store = applicationContext.calendarReminderLog() + val store = calendarReminderLog() // Walk every kind-31925 RSVP authored by an account on this device. We don't have a // multi-account "all logged-in pubkeys" view here, so we accept any RSVP that's diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarReminderSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarReminderSettingsScreen.kt index ce1888a33d..1115110ab4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarReminderSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarReminderSettingsScreen.kt @@ -68,7 +68,7 @@ import kotlinx.coroutines.launch fun CalendarReminderSettingsScreen(nav: INav) { val context = LocalContext.current val scope = rememberCoroutineScope() - val store = remember(context) { context.calendarReminderSettings() } + val store = remember { calendarReminderSettings() } // DataStore reads are suspend, so the first frame renders the defaults and // the stored values arrive right after. Collecting the flow rather than diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt index dc63800518..a0cba559e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessDismissedGamesData.kt @@ -20,8 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess -import android.content.Context -import androidx.datastore.preferences.preferencesDataStore +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import com.vitorpamplona.amethyst.Amethyst /** * Where Android keeps the dismissed-chess-games store. @@ -31,4 +32,5 @@ import androidx.datastore.preferences.preferencesDataStore * data over is not worth the code. Anyone who had dismissed a game sees it once * more and dismisses it again. */ -internal val Context.chessDismissedGamesData by preferencesDataStore(name = "chess_dismissed_games_v2") +internal val chessDismissedGamesData: DataStore + get() = Amethyst.instance.appStores.getDataStore("chess_dismissed_games_v2") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt index ae46d80463..9bd3819de4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt @@ -61,7 +61,7 @@ class ChessViewModelNew( private val publisher = AndroidChessPublisher(account) private val fetcher = AndroidRelayFetcher(account) private val metadataProvider = AndroidMetadataProvider() - private val dismissedStorage = ChessDismissedGamesStore(application.chessDismissedGamesData) + private val dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData) // Shared business logic (creates its own ChessLobbyState internally) private val logic =