From 40995b9670f1628a44398f78e392ee110d70f531 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 20:37:53 +0000 Subject: [PATCH] =?UTF-8?q?refactor(privacy):=20drop=20I2pType.INTERNAL=20?= =?UTF-8?q?=E2=80=94=20EXTERNAL=20is=20the=20permanent=20answer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Earlier commits kept I2pType.INTERNAL as a placeholder for a follow-up embedded daemon. We're not shipping that: I2P bootstrap on Android is structurally minutes-long (no equivalent of Tor's hardcoded directory authorities — NetDB peer discovery is protocol-inherent), so an embedded router would mean a permanently-warming UX. Users who want I2P run i2pd / Java I2P independently and point Amethyst at its SOCKS port. Changes: - commons I2pType drops the INTERNAL variant; parseI2pType collapses unknown codes to OFF - I2pManager loses its INTERNAL switch arm - I2pSettingsDialog drops the "treat INTERNAL as OFF" mapping it used to carry — the enum no longer has the case - Android UI I2pSettings.resourceId drops the i2p_internal branch - strings.xml drops the now-unused i2p_internal string - I2pSharedPreferences hardens load: a stored "INTERNAL" from an earlier branch is no longer a valid I2pType, so runCatching swallows the IllegalArgumentException and the user lands on OFF - PrivacyRouterTest fixtures use I2pType.EXTERNAL throughout --- .../model/preferences/I2pSharedPreferences.kt | 8 +++++++- .../amethyst/ui/i2p/I2pManager.kt | 20 ++++++++----------- .../amethyst/ui/i2p/I2pSettings.kt | 1 - .../amethyst/ui/i2p/I2pSettingsDialog.kt | 11 ++++------ amethyst/src/main/res/values/strings.xml | 1 - .../amethyst/commons/i2p/I2pSettings.kt | 7 +++++-- .../commons/privacy/PrivacyRouterTest.kt | 10 +++++----- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/I2pSharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/I2pSharedPreferences.kt index ccaf45356..8d0a082f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/I2pSharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/I2pSharedPreferences.kt @@ -83,7 +83,13 @@ class I2pSharedPreferences( try { val preferences = context.sharedPreferencesDataStore.data.first() I2pSettings( - i2pType = preferences[I2P_TYPE_KEY]?.let { I2pType.valueOf(it) } ?: I2pType.OFF, + // A stored "INTERNAL" from an earlier branch is no longer a valid I2pType — + // runCatching swallows the IllegalArgumentException so the user lands on OFF + // and can re-enable EXTERNAL from the settings screen. + i2pType = + preferences[I2P_TYPE_KEY] + ?.let { runCatching { I2pType.valueOf(it) }.getOrNull() } + ?: I2pType.OFF, externalSocksPort = preferences[EXTERNAL_SOCKS_PORT_KEY] ?: 4447, i2pRelaysViaI2p = preferences[I2P_RELAYS_VIA_I2P_KEY] ?: true, dmRelaysViaI2p = preferences[DM_RELAYS_VIA_I2P_KEY] ?: false, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pManager.kt index d4c68a622..4735d2254 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pManager.kt @@ -35,18 +35,16 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** - * Mirror of TorManager, but EXTERNAL-only for now — no embedded I2P daemon ships - * with this branch. When [I2pType.EXTERNAL] is selected, this manager surfaces the - * configured SOCKS port; otherwise it emits [I2pServiceStatus.Off]. + * EXTERNAL-only I2P manager. Mirrors TorManager's status surface but never starts + * a daemon itself: I2P bootstrap on Android is structurally minutes-long (no + * equivalent of Tor's hardcoded directory authorities — the protocol requires + * NetDB peer discovery), so the embedded route would ship a permanently-warming + * experience. Users who want I2P run i2pd / Java I2P independently and point + * Amethyst at its SOCKS port via the Privacy Options screen. * * There is intentionally no Connecting state, no bootstrap timeout, and no - * "session bypass": with EXTERNAL the daemon's lifecycle is the user's - * responsibility (i2pd / Java I2P running on the device), and the connection - * either works or it doesn't — there's no in-app bootstrap to bypass. - * - * When INTERNAL is wired up in a follow-up, it will land here as a third branch - * that starts an embedded daemon and emits Connecting / Active over its - * lifecycle, mirroring the Tor flow. + * "session bypass" — when EXTERNAL is selected the daemon's lifecycle is the + * user's responsibility and the connection either works or it doesn't. */ class I2pManager( i2pPrefs: I2pSharedPreferences, @@ -59,8 +57,6 @@ class I2pManager( ) { type, port -> when (type) { I2pType.OFF -> I2pServiceStatus.Off - // Persisted INTERNAL is treated as Off until an embedded daemon ships. - I2pType.INTERNAL -> I2pServiceStatus.Off I2pType.EXTERNAL -> if (port > 0) I2pServiceStatus.Active(port) else I2pServiceStatus.Off } }.catch { e -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pSettings.kt index b48085585..7497a8008 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pSettings.kt @@ -28,6 +28,5 @@ val I2pType.resourceId: Int get() = when (this) { I2pType.OFF -> R.string.i2p_off - I2pType.INTERNAL -> R.string.i2p_internal I2pType.EXTERNAL -> R.string.i2p_external } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pSettingsDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pSettingsDialog.kt index 80a10552d..e608e1be6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pSettingsDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/i2p/I2pSettingsDialog.kt @@ -49,13 +49,13 @@ import com.vitorpamplona.amethyst.ui.tor.SwitchSettingsRow import kotlinx.collections.immutable.persistentListOf // Settings body for I2P. Mirrors PrivacySettingsBody (Tor) but without presets — -// no canonical "default I2P configuration" exists yet, and the per-feature toggles +// no canonical "default I2P configuration" exists, and the per-feature toggles // only take effect when I2P is also the preferred clearnet transport (see the // global picker above this section in PrivacyOptionsScreen). // -// INTERNAL is intentionally hidden from the picker until an embedded I2P daemon -// ships. EXTERNAL connects to a user-run i2pd / Java I2P installation on the -// device (default SOCKS port 4447). +// I2P is EXTERNAL-only: connects to a user-run i2pd / Java I2P installation on +// the device (default SOCKS port 4447). We intentionally do not embed a router — +// see commons I2pType for the rationale. @Composable fun I2pSettingsBody(dialogViewModel: I2pDialogViewModel) { Column( @@ -69,11 +69,8 @@ fun I2pSettingsBody(dialogViewModel: I2pDialogViewModel) { TitleExplainer(stringRes(I2pType.OFF.resourceId)), TitleExplainer(stringRes(I2pType.EXTERNAL.resourceId)), ), - // OFF screenCode is 0, EXTERNAL is 2. Spinner indices are 0/1. - // Map: dropdown index 0 → OFF, 1 → EXTERNAL. when (dialogViewModel.i2pType.value) { I2pType.OFF -> 0 - I2pType.INTERNAL -> 0 // Treat any persisted INTERNAL as OFF until daemon ships. I2pType.EXTERNAL -> 1 }, ) { idx -> diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 099194301..cc56b7301 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1103,7 +1103,6 @@ Use an external I2P router (i2pd / Java I2P) Off - Internal External Router I2P Socks Port diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/i2p/I2pSettings.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/i2p/I2pSettings.kt index f0efd6ea9..cd27ba36d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/i2p/I2pSettings.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/i2p/I2pSettings.kt @@ -41,17 +41,20 @@ data class I2pSettings( val mediaUploadsViaI2p: Boolean = false, ) +// EXTERNAL-only: this app does not embed an I2P router. Bootstrap on Android is +// minutes-long for a fresh netDb and the protocol provides no shortcut analogous +// to Tor's hardcoded directory authorities; we'd ship a "permanently warming up" +// experience. Users who want I2P run i2pd or Java I2P independently and point +// Amethyst at its SOCKS port (default 4447). See I2pManager. enum class I2pType( val screenCode: Int, ) { OFF(0), - INTERNAL(1), EXTERNAL(2), } fun parseI2pType(code: Int?): I2pType = when (code) { - I2pType.INTERNAL.screenCode -> I2pType.INTERNAL I2pType.EXTERNAL.screenCode -> I2pType.EXTERNAL else -> I2pType.OFF } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/privacy/PrivacyRouterTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/privacy/PrivacyRouterTest.kt index 22e1b713c..5a7522962 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/privacy/PrivacyRouterTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/privacy/PrivacyRouterTest.kt @@ -38,7 +38,7 @@ class PrivacyRouterTest { i2pOn: Boolean = false, preferred: PrivacyTransport = PrivacyTransport.DIRECT, tor: TorSettings = TorSettings(torType = if (torOn) TorType.INTERNAL else TorType.OFF), - i2p: I2pSettings = I2pSettings(i2pType = if (i2pOn) I2pType.INTERNAL else I2pType.OFF), + i2p: I2pSettings = I2pSettings(i2pType = if (i2pOn) I2pType.EXTERNAL else I2pType.OFF), ) = PrivacySettings( tor = tor, i2p = i2p, @@ -125,7 +125,7 @@ class PrivacyRouterTest { settings( i2pOn = true, preferred = PrivacyTransport.I2P, - i2p = I2pSettings(i2pType = I2pType.INTERNAL, videosViaI2p = true), + i2p = I2pSettings(i2pType = I2pType.EXTERNAL, videosViaI2p = true), ) assertEquals(PrivacyRoute.I2p, PrivacyRouter.route(clearnet, FeatureRole.VIDEO, s)) } @@ -136,7 +136,7 @@ class PrivacyRouterTest { settings( i2pOn = true, preferred = PrivacyTransport.I2P, - i2p = I2pSettings(i2pType = I2pType.INTERNAL, videosViaI2p = false), + i2p = I2pSettings(i2pType = I2pType.EXTERNAL, videosViaI2p = false), ) assertEquals(PrivacyRoute.Direct, PrivacyRouter.route(clearnet, FeatureRole.VIDEO, s)) } @@ -162,7 +162,7 @@ class PrivacyRouterTest { torOn = false, i2pOn = true, preferred = PrivacyTransport.TOR, - i2p = I2pSettings(i2pType = I2pType.INTERNAL, imagesViaI2p = true), + i2p = I2pSettings(i2pType = I2pType.EXTERNAL, imagesViaI2p = true), ) assertEquals(PrivacyRoute.Direct, PrivacyRouter.route(clearnet, FeatureRole.IMAGE, s)) } @@ -176,7 +176,7 @@ class PrivacyRouterTest { i2pOn = true, preferred = PrivacyTransport.I2P, tor = TorSettings(torType = TorType.INTERNAL, imagesViaTor = true), - i2p = I2pSettings(i2pType = I2pType.INTERNAL, imagesViaI2p = true), + i2p = I2pSettings(i2pType = I2pType.EXTERNAL, imagesViaI2p = true), ) assertEquals(PrivacyRoute.I2p, PrivacyRouter.route(clearnet, FeatureRole.IMAGE, s)) }