1package com.peachpasswords.sync23import android.content.Context4import dagger.hilt.android.qualifiers.ApplicationContext5import javax.inject.Inject6import javax.inject.Singleton78910111213141516171819enum 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}40414243444546sealed interface RelayDecision {47 @JvmInline48 value class Dial(val relayUrl: String) : RelayDecision49 data class Refuse(val reason: SyncOutcomeReason) : RelayDecision50}5152535455565758fun isPrivateRelayUnconfigured(mode: SyncMode, storedRelayUrl: String): Boolean =59 mode == SyncMode.PRIVATE && storedRelayUrl == Rendezvous.DEFAULT_RELAY_URL6061626364656667fun modeForRelayUrl(normalizedRelayUrl: String): SyncMode =68 if (normalizedRelayUrl == Rendezvous.DEFAULT_RELAY_URL) SyncMode.PUBLIC else SyncMode.PRIVATE69707172737475fun privateRelayUrlOf(storedRelayUrl: String): String =76 storedRelayUrl.takeIf { it != Rendezvous.DEFAULT_RELAY_URL }.orEmpty()777879fun privateRelayDraftOf(configuredPrivateUrl: String): String =80 configuredPrivateUrl.ifBlank { "wss://" }8182fun relayInForce(mode: SyncMode, storedRelayUrl: String): RelayDecision = when {83 mode == SyncMode.OFF -> RelayDecision.Refuse(SyncOutcomeReason.SYNC_DISABLED)84 isPrivateRelayUnconfigured(mode, storedRelayUrl) ->85 RelayDecision.Refuse(SyncOutcomeReason.PRIVATE_RELAY_UNSET)86 mode == SyncMode.PUBLIC -> RelayDecision.Dial(Rendezvous.DEFAULT_RELAY_URL)87 else -> RelayDecision.Dial(storedRelayUrl)88}89909192939495@Singleton96class RelaySettingsStore @Inject constructor(97 @ApplicationContext context: Context,98 private val devices: PairedDeviceStore,99) {100 private val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)101102 fun syncMode(): SyncMode {103 SyncMode.parse(prefs.getString(KEY_MODE, null))?.let { return it }104 105 106 107 108 return when {109 prefs.getBoolean(LEGACY_KEY_LAN_ONLY, false) -> SyncMode.OFF110 storedRelayUrl() != Rendezvous.DEFAULT_RELAY_URL -> SyncMode.PRIVATE111 else -> SyncMode.PUBLIC112 }113 }114115 116117118119120121 fun setSyncMode(mode: SyncMode) {122 devices.propagateRelaySetting(mode, storedRelayUrl())123 prefs.edit().putString(KEY_MODE, mode.wire()).apply()124 }125126 127 fun relayInForce(): RelayDecision = relayInForce(syncMode(), storedRelayUrl())128129 130 fun privateRelayUrl(): String = privateRelayUrlOf(storedRelayUrl())131132 133134135136137138139140 fun setRelayUrl(input: String): String {141 val normalized = Rendezvous.normalizeRelayUrl(input)142 devices.propagateRelaySetting(modeForRelayUrl(normalized), normalized)143 prefs.edit()144 .putString(KEY_URL, normalized)145 .putString(KEY_MODE, modeForRelayUrl(normalized).wire())146 .apply()147 return normalized148 }149150 fun reset() {151 prefs.edit().clear().apply()152 }153154 private fun storedRelayUrl(): String = prefs.getString(KEY_URL, null)155 ?.let { runCatching { Rendezvous.normalizeRelayUrl(it) }.getOrNull() }156 ?: Rendezvous.DEFAULT_RELAY_URL157158 companion object {159 const val PREFS_FILE = "peach_relay_settings"160 const val KEY_URL = "relay_url"161 const val KEY_MODE = "sync_mode"162 163 const val LEGACY_KEY_LAN_ONLY = "lan_only"164165 fun readRelayUrl(context: Context): String =166 context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)167 .getString(KEY_URL, null)168 ?.let { runCatching { Rendezvous.normalizeRelayUrl(it) }.getOrNull() }169 ?: Rendezvous.DEFAULT_RELAY_URL170 }171}