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))
}