refactor(privacy): drop I2pType.INTERNAL — EXTERNAL is the permanent answer
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
This commit is contained in:
+7
-1
@@ -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,
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -1103,7 +1103,6 @@
|
||||
<string name="use_i2p_explainer">Use an external I2P router (i2pd / Java I2P)</string>
|
||||
|
||||
<string name="i2p_off">Off</string>
|
||||
<string name="i2p_internal">Internal</string>
|
||||
<string name="i2p_external">External Router</string>
|
||||
|
||||
<string name="i2p_socks_port">I2P Socks Port</string>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+5
-5
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user