package com.peachpasswords.sync import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton /** * Sync is exactly three named choices (docs/sync-lan-retirement.md, mirroring * the browser's `sync-settings.ts`): * * - [PUBLIC] — Peach's relay. The default, zero setup. * - [PRIVATE] — a self-hosted peach-relay this mesh points at. * - [OFF] — no sync; the vault moves by Codex/CSV export instead. * * LAN peer-to-peer sync is retired, not deprecated: there is no fourth mode and * no "local network only" opt-out. */ enum class SyncMode { PUBLIC, PRIVATE, OFF, ; fun wire(): String = when (this) { PUBLIC -> "public" PRIVATE -> "private" OFF -> "off" } companion object { fun parse(raw: String?): SyncMode? = when (raw) { "public" -> PUBLIC "private" -> PRIVATE "off" -> OFF else -> null } } } /** * This handset's sync configuration. Plain SharedPreferences rather than the * encrypted store: none of it is a secret, and it must not travel to other * devices the way a vault setting would. */ @Singleton class RelaySettingsStore @Inject constructor( @ApplicationContext context: Context, ) { private val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE) fun syncMode(): SyncMode { SyncMode.parse(prefs.getString(KEY_MODE, null))?.let { return it } // Migration. Someone who set lanOnly chose "no relay involved", and LAN // is now gone, so the honest landing spot is OFF rather than silently // enrolling them in a relay they opted out of. Everyone else lands on // public/private by whether they had configured a custom URL. return when { prefs.getBoolean(LEGACY_KEY_LAN_ONLY, false) -> SyncMode.OFF storedRelayUrl() != Rendezvous.DEFAULT_RELAY_URL -> SyncMode.PRIVATE else -> SyncMode.PUBLIC } } fun setSyncMode(mode: SyncMode) { prefs.edit().putString(KEY_MODE, mode.wire()).apply() } fun isSyncEnabled(): Boolean = syncMode() != SyncMode.OFF /** The relay in force: always the default under [SyncMode.PUBLIC]. */ fun relayUrl(): String = if (syncMode() == SyncMode.PUBLIC) Rendezvous.DEFAULT_RELAY_URL else storedRelayUrl() /** * Point this mesh at a self-hosted relay. Implies [SyncMode.PRIVATE] — * offering a custom relay and not switching to it would be a trap. */ fun setPrivateRelayUrl(input: String): String { val normalized = Rendezvous.normalizeRelayUrl(input) prefs.edit() .putString(KEY_URL, normalized) .putString(KEY_MODE, SyncMode.PRIVATE.wire()) .apply() return normalized } private fun storedRelayUrl(): String = prefs.getString(KEY_URL, null) ?.let { runCatching { Rendezvous.normalizeRelayUrl(it) }.getOrNull() } ?: Rendezvous.DEFAULT_RELAY_URL companion object { const val PREFS_FILE = "peach_relay_settings" const val KEY_URL = "relay_url" const val KEY_MODE = "sync_mode" /** Retired `lanOnly` flag, read once for migration and never written. */ const val LEGACY_KEY_LAN_ONLY = "lan_only" fun readRelayUrl(context: Context): String = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE) .getString(KEY_URL, null) ?.let { runCatching { Rendezvous.normalizeRelayUrl(it) }.getOrNull() } ?: Rendezvous.DEFAULT_RELAY_URL } }