RelaySettingsStore.kt

3.6 KB · raw

1package com.peachpasswords.sync23import android.content.Context4import dagger.hilt.android.qualifiers.ApplicationContext5import javax.inject.Inject6import javax.inject.Singleton78/**9 * Sync is exactly three named choices (docs/sync-lan-retirement.md, mirroring10 * the browser's `sync-settings.ts`):11 *12 *  - [PUBLIC]  — Peach's relay. The default, zero setup.13 *  - [PRIVATE] — a self-hosted peach-relay this mesh points at.14 *  - [OFF]     — no sync; the vault moves by Codex/CSV export instead.15 *16 * LAN peer-to-peer sync is retired, not deprecated: there is no fourth mode and17 * no "local network only" opt-out.18 */19enum class SyncMode {20    PUBLIC,21    PRIVATE,22    OFF,23    ;2425    fun wire(): String = when (this) {26        PUBLIC -> "public"27        PRIVATE -> "private"28        OFF -> "off"29    }3031    companion object {32        fun parse(raw: String?): SyncMode? = when (raw) {33            "public" -> PUBLIC34            "private" -> PRIVATE35            "off" -> OFF36            else -> null37        }38    }39}4041/**42 * This handset's sync configuration. Plain SharedPreferences rather than the43 * encrypted store: none of it is a secret, and it must not travel to other44 * devices the way a vault setting would.45 */46@Singleton47class RelaySettingsStore @Inject constructor(48    @ApplicationContext context: Context,49) {50    private val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)5152    fun syncMode(): SyncMode {53        SyncMode.parse(prefs.getString(KEY_MODE, null))?.let { return it }54        // Migration. Someone who set lanOnly chose "no relay involved", and LAN55        // is now gone, so the honest landing spot is OFF rather than silently56        // enrolling them in a relay they opted out of. Everyone else lands on57        // public/private by whether they had configured a custom URL.58        return when {59            prefs.getBoolean(LEGACY_KEY_LAN_ONLY, false) -> SyncMode.OFF60            storedRelayUrl() != Rendezvous.DEFAULT_RELAY_URL -> SyncMode.PRIVATE61            else -> SyncMode.PUBLIC62        }63    }6465    fun setSyncMode(mode: SyncMode) {66        prefs.edit().putString(KEY_MODE, mode.wire()).apply()67    }6869    fun isSyncEnabled(): Boolean = syncMode() != SyncMode.OFF7071    /** The relay in force: always the default under [SyncMode.PUBLIC]. */72    fun relayUrl(): String =73        if (syncMode() == SyncMode.PUBLIC) Rendezvous.DEFAULT_RELAY_URL else storedRelayUrl()7475    /**76     * Point this mesh at a self-hosted relay. Implies [SyncMode.PRIVATE] —77     * offering a custom relay and not switching to it would be a trap.78     */79    fun setPrivateRelayUrl(input: String): String {80        val normalized = Rendezvous.normalizeRelayUrl(input)81        prefs.edit()82            .putString(KEY_URL, normalized)83            .putString(KEY_MODE, SyncMode.PRIVATE.wire())84            .apply()85        return normalized86    }8788    private fun storedRelayUrl(): String = prefs.getString(KEY_URL, null)89        ?.let { runCatching { Rendezvous.normalizeRelayUrl(it) }.getOrNull() }90        ?: Rendezvous.DEFAULT_RELAY_URL9192    companion object {93        const val PREFS_FILE = "peach_relay_settings"94        const val KEY_URL = "relay_url"95        const val KEY_MODE = "sync_mode"96        /** Retired `lanOnly` flag, read once for migration and never written. */97        const val LEGACY_KEY_LAN_ONLY = "lan_only"9899        fun readRelayUrl(context: Context): String =100            context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)101                .getString(KEY_URL, null)102                ?.let { runCatching { Rendezvous.normalizeRelayUrl(it) }.getOrNull() }103                ?: Rendezvous.DEFAULT_RELAY_URL104    }105}