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 {
|
try {
|
||||||
val preferences = context.sharedPreferencesDataStore.data.first()
|
val preferences = context.sharedPreferencesDataStore.data.first()
|
||||||
I2pSettings(
|
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,
|
externalSocksPort = preferences[EXTERNAL_SOCKS_PORT_KEY] ?: 4447,
|
||||||
i2pRelaysViaI2p = preferences[I2P_RELAYS_VIA_I2P_KEY] ?: true,
|
i2pRelaysViaI2p = preferences[I2P_RELAYS_VIA_I2P_KEY] ?: true,
|
||||||
dmRelaysViaI2p = preferences[DM_RELAYS_VIA_I2P_KEY] ?: false,
|
dmRelaysViaI2p = preferences[DM_RELAYS_VIA_I2P_KEY] ?: false,
|
||||||
|
|||||||
@@ -35,18 +35,16 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mirror of TorManager, but EXTERNAL-only for now — no embedded I2P daemon ships
|
* EXTERNAL-only I2P manager. Mirrors TorManager's status surface but never starts
|
||||||
* with this branch. When [I2pType.EXTERNAL] is selected, this manager surfaces the
|
* a daemon itself: I2P bootstrap on Android is structurally minutes-long (no
|
||||||
* configured SOCKS port; otherwise it emits [I2pServiceStatus.Off].
|
* 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
|
* There is intentionally no Connecting state, no bootstrap timeout, and no
|
||||||
* "session bypass": with EXTERNAL the daemon's lifecycle is the user's
|
* "session bypass" — when EXTERNAL is selected the daemon's lifecycle is the
|
||||||
* responsibility (i2pd / Java I2P running on the device), and the connection
|
* user's responsibility and the connection either works or it doesn't.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
class I2pManager(
|
class I2pManager(
|
||||||
i2pPrefs: I2pSharedPreferences,
|
i2pPrefs: I2pSharedPreferences,
|
||||||
@@ -59,8 +57,6 @@ class I2pManager(
|
|||||||
) { type, port ->
|
) { type, port ->
|
||||||
when (type) {
|
when (type) {
|
||||||
I2pType.OFF -> I2pServiceStatus.Off
|
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
|
I2pType.EXTERNAL -> if (port > 0) I2pServiceStatus.Active(port) else I2pServiceStatus.Off
|
||||||
}
|
}
|
||||||
}.catch { e ->
|
}.catch { e ->
|
||||||
|
|||||||
@@ -28,6 +28,5 @@ val I2pType.resourceId: Int
|
|||||||
get() =
|
get() =
|
||||||
when (this) {
|
when (this) {
|
||||||
I2pType.OFF -> R.string.i2p_off
|
I2pType.OFF -> R.string.i2p_off
|
||||||
I2pType.INTERNAL -> R.string.i2p_internal
|
|
||||||
I2pType.EXTERNAL -> R.string.i2p_external
|
I2pType.EXTERNAL -> R.string.i2p_external
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ import com.vitorpamplona.amethyst.ui.tor.SwitchSettingsRow
|
|||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
// Settings body for I2P. Mirrors PrivacySettingsBody (Tor) but without presets —
|
// 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
|
// only take effect when I2P is also the preferred clearnet transport (see the
|
||||||
// global picker above this section in PrivacyOptionsScreen).
|
// global picker above this section in PrivacyOptionsScreen).
|
||||||
//
|
//
|
||||||
// INTERNAL is intentionally hidden from the picker until an embedded I2P daemon
|
// I2P is EXTERNAL-only: connects to a user-run i2pd / Java I2P installation on
|
||||||
// ships. EXTERNAL connects to a user-run i2pd / Java I2P installation on the
|
// the device (default SOCKS port 4447). We intentionally do not embed a router —
|
||||||
// device (default SOCKS port 4447).
|
// see commons I2pType for the rationale.
|
||||||
@Composable
|
@Composable
|
||||||
fun I2pSettingsBody(dialogViewModel: I2pDialogViewModel) {
|
fun I2pSettingsBody(dialogViewModel: I2pDialogViewModel) {
|
||||||
Column(
|
Column(
|
||||||
@@ -69,11 +69,8 @@ fun I2pSettingsBody(dialogViewModel: I2pDialogViewModel) {
|
|||||||
TitleExplainer(stringRes(I2pType.OFF.resourceId)),
|
TitleExplainer(stringRes(I2pType.OFF.resourceId)),
|
||||||
TitleExplainer(stringRes(I2pType.EXTERNAL.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) {
|
when (dialogViewModel.i2pType.value) {
|
||||||
I2pType.OFF -> 0
|
I2pType.OFF -> 0
|
||||||
I2pType.INTERNAL -> 0 // Treat any persisted INTERNAL as OFF until daemon ships.
|
|
||||||
I2pType.EXTERNAL -> 1
|
I2pType.EXTERNAL -> 1
|
||||||
},
|
},
|
||||||
) { idx ->
|
) { idx ->
|
||||||
|
|||||||
@@ -1103,7 +1103,6 @@
|
|||||||
<string name="use_i2p_explainer">Use an external I2P router (i2pd / Java I2P)</string>
|
<string name="use_i2p_explainer">Use an external I2P router (i2pd / Java I2P)</string>
|
||||||
|
|
||||||
<string name="i2p_off">Off</string>
|
<string name="i2p_off">Off</string>
|
||||||
<string name="i2p_internal">Internal</string>
|
|
||||||
<string name="i2p_external">External Router</string>
|
<string name="i2p_external">External Router</string>
|
||||||
|
|
||||||
<string name="i2p_socks_port">I2P Socks Port</string>
|
<string name="i2p_socks_port">I2P Socks Port</string>
|
||||||
|
|||||||
@@ -41,17 +41,20 @@ data class I2pSettings(
|
|||||||
val mediaUploadsViaI2p: Boolean = false,
|
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(
|
enum class I2pType(
|
||||||
val screenCode: Int,
|
val screenCode: Int,
|
||||||
) {
|
) {
|
||||||
OFF(0),
|
OFF(0),
|
||||||
INTERNAL(1),
|
|
||||||
EXTERNAL(2),
|
EXTERNAL(2),
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseI2pType(code: Int?): I2pType =
|
fun parseI2pType(code: Int?): I2pType =
|
||||||
when (code) {
|
when (code) {
|
||||||
I2pType.INTERNAL.screenCode -> I2pType.INTERNAL
|
|
||||||
I2pType.EXTERNAL.screenCode -> I2pType.EXTERNAL
|
I2pType.EXTERNAL.screenCode -> I2pType.EXTERNAL
|
||||||
else -> I2pType.OFF
|
else -> I2pType.OFF
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -38,7 +38,7 @@ class PrivacyRouterTest {
|
|||||||
i2pOn: Boolean = false,
|
i2pOn: Boolean = false,
|
||||||
preferred: PrivacyTransport = PrivacyTransport.DIRECT,
|
preferred: PrivacyTransport = PrivacyTransport.DIRECT,
|
||||||
tor: TorSettings = TorSettings(torType = if (torOn) TorType.INTERNAL else TorType.OFF),
|
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(
|
) = PrivacySettings(
|
||||||
tor = tor,
|
tor = tor,
|
||||||
i2p = i2p,
|
i2p = i2p,
|
||||||
@@ -125,7 +125,7 @@ class PrivacyRouterTest {
|
|||||||
settings(
|
settings(
|
||||||
i2pOn = true,
|
i2pOn = true,
|
||||||
preferred = PrivacyTransport.I2P,
|
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))
|
assertEquals(PrivacyRoute.I2p, PrivacyRouter.route(clearnet, FeatureRole.VIDEO, s))
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ class PrivacyRouterTest {
|
|||||||
settings(
|
settings(
|
||||||
i2pOn = true,
|
i2pOn = true,
|
||||||
preferred = PrivacyTransport.I2P,
|
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))
|
assertEquals(PrivacyRoute.Direct, PrivacyRouter.route(clearnet, FeatureRole.VIDEO, s))
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ class PrivacyRouterTest {
|
|||||||
torOn = false,
|
torOn = false,
|
||||||
i2pOn = true,
|
i2pOn = true,
|
||||||
preferred = PrivacyTransport.TOR,
|
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))
|
assertEquals(PrivacyRoute.Direct, PrivacyRouter.route(clearnet, FeatureRole.IMAGE, s))
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ class PrivacyRouterTest {
|
|||||||
i2pOn = true,
|
i2pOn = true,
|
||||||
preferred = PrivacyTransport.I2P,
|
preferred = PrivacyTransport.I2P,
|
||||||
tor = TorSettings(torType = TorType.INTERNAL, imagesViaTor = true),
|
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))
|
assertEquals(PrivacyRoute.I2p, PrivacyRouter.route(clearnet, FeatureRole.IMAGE, s))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user