From e3914e56a51ae0153e40843b0f0b1411a873793c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 20:03:58 +0000 Subject: [PATCH] Add built-in Tor support via kmptor library Introduces embedded Tor functionality as an alternative to the existing Orbot proxy integration, so users no longer need an external app to route traffic through the Tor network. Key changes: - Add TorMode enum (DISABLED, ORBOT, BUILTIN) replacing the useProxy boolean - Create TorManager (free-flavor only) using io.matthewnelson.kmp-tor:runtime and resource-exec-tor to manage the Tor daemon lifecycle - Offline flavor gets a no-op TorManager stub to maintain build compatibility - AmberSettings: replace useProxy field with torMode enum; useProxy becomes a computed property (torMode != DISABLED) for backward compatibility - LocalPreferences: migrate old use_proxy boolean to TorMode on first load, save/load torMode, update proxy config per mode - Amber: start built-in Tor on app launch when torMode == BUILTIN; isSocksProxyAlive returns TorManager.isRunning for built-in mode - ConnectOrbotDialog: add Built-in Tor section above the Orbot section - SettingsScreen: show mode-specific disconnect label (built-in vs orbot) - HttpClientManager: add clearProxy() public method - AndroidManifest: add INTERNET permission explicitly - gradle: add kmptor 2.6.0 + resource-exec-tor 408.19.0 dependencies, enable jniLibs.useLegacyPackaging for native binary extraction https://claude.ai/code/session_01JwcwxdYrDnr3QyHqTBz33a --- app/build.gradle | 3 + .../nostrsigner/service/TorManager.kt | 86 +++++++++++++++++++ app/src/main/AndroidManifest.xml | 1 + .../java/com/greenart7c3/nostrsigner/Amber.kt | 8 ++ .../nostrsigner/LocalPreferences.kt | 42 +++++++-- .../nostrsigner/models/AmberSettings.kt | 6 +- .../greenart7c3/nostrsigner/models/TorMode.kt | 7 ++ .../nostrsigner/okhttp/HttpClientManager.kt | 4 + .../nostrsigner/ui/AccountStateViewModel.kt | 5 +- .../greenart7c3/nostrsigner/ui/MainScreen.kt | 21 +++++ .../nostrsigner/ui/SettingsScreen.kt | 20 +++-- .../ui/actions/ConnectOrbotDialog.kt | 44 ++++++++++ app/src/main/res/values/strings.xml | 10 ++- .../nostrsigner/service/TorManager.kt | 24 ++++++ gradle.properties | 1 + gradle/libs.versions.toml | 4 + 16 files changed, 266 insertions(+), 20 deletions(-) create mode 100644 app/src/free/java/com/greenart7c3/nostrsigner/service/TorManager.kt create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/models/TorMode.kt create mode 100644 app/src/offline/java/com/greenart7c3/nostrsigner/service/TorManager.kt diff --git a/app/build.gradle b/app/build.gradle index 79e217f5..6620ef41 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -164,6 +164,7 @@ android { excludes += '/META-INF/{AL2.0,LGPL2.1}' excludes +='META-INF/versions/9/OSGI-INF/MANIFEST.MF' } + jniLibs.useLegacyPackaging = true } } @@ -233,6 +234,8 @@ dependencies { freeImplementation libs.okhttp freeImplementation libs.okhttpCoroutines + freeImplementation libs.kmptor.runtime + freeImplementation libs.kmptor.resource.exec // Load images from the web. implementation libs.coil.compose diff --git a/app/src/free/java/com/greenart7c3/nostrsigner/service/TorManager.kt b/app/src/free/java/com/greenart7c3/nostrsigner/service/TorManager.kt new file mode 100644 index 00000000..8a955daa --- /dev/null +++ b/app/src/free/java/com/greenart7c3/nostrsigner/service/TorManager.kt @@ -0,0 +1,86 @@ +package com.greenart7c3.nostrsigner.service + +import android.content.Context +import android.util.Log +import com.greenart7c3.nostrsigner.okhttp.HttpClientManager +import io.matthewnelson.kmp.tor.resource.exec.tor.ResourceLoaderTorExec +import io.matthewnelson.kmp.tor.runtime.RuntimeEvent +import io.matthewnelson.kmp.tor.runtime.TorRuntime +import io.matthewnelson.kmp.tor.runtime.core.OnEvent +import io.matthewnelson.kmp.tor.runtime.core.config.TorOption +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +object TorManager { + private const val TAG = "TorManager" + + @Volatile + private var torRuntime: TorRuntime? = null + + private val _isRunning = MutableStateFlow(false) + val isRunning: StateFlow = _isRunning.asStateFlow() + + fun start(context: Context, scope: CoroutineScope) { + if (torRuntime != null) return + scope.launch(Dispatchers.IO) { + try { + val workDir = context.filesDir.resolve("kmptor") + val cacheDir = context.cacheDir.resolve("kmptor") + + val env = TorRuntime.Environment.Builder(workDir, cacheDir, ResourceLoaderTorExec::getOrCreate) + + val runtime = TorRuntime.Builder(env) { + // Log all runtime events for debugging + RuntimeEvent.entries().forEach { event -> + observerStatic(event, OnEvent.Executor.Immediate) { data -> + Log.d(TAG, data.toString()) + } + } + + // Observe SOCKS listeners to get the assigned port + observerStatic(RuntimeEvent.LISTENERS, OnEvent.Executor.Immediate) { listeners -> + val addr = listeners.socks.firstOrNull() + if (addr != null) { + try { + val port = addr.port.value + Log.i(TAG, "Built-in Tor SOCKS proxy on port $port") + HttpClientManager.setDefaultProxyOnPort(port) + } catch (e: Exception) { + Log.e(TAG, "Failed to read Tor SOCKS port", e) + } + } + } + + config { + TorOption.__SocksPort.configure { auto() } + } + } + + torRuntime = runtime + runtime.startDaemonSync() + _isRunning.value = true + Log.i(TAG, "Built-in Tor started") + } catch (e: Exception) { + Log.e(TAG, "Failed to start built-in Tor", e) + torRuntime = null + _isRunning.value = false + } + } + } + + fun stop() { + try { + torRuntime?.stopDaemonSync() + Log.i(TAG, "Built-in Tor stopped") + } catch (e: Exception) { + Log.e(TAG, "Error stopping built-in Tor", e) + } finally { + torRuntime = null + _isRunning.value = false + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 08a755fd..6a287b31 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt index e2e6f686..d4f7af9e 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt @@ -37,10 +37,12 @@ import com.greenart7c3.nostrsigner.okhttp.HttpClientManager import com.greenart7c3.nostrsigner.okhttp.OkHttpWebSocket import com.greenart7c3.nostrsigner.relays.AmberRelayStats import com.greenart7c3.nostrsigner.relays.NostrClientLoggerListener +import com.greenart7c3.nostrsigner.models.TorMode import com.greenart7c3.nostrsigner.service.ClearLogsWorker import com.greenart7c3.nostrsigner.service.ConnectivityService import com.greenart7c3.nostrsigner.service.NotificationSubscription import com.greenart7c3.nostrsigner.service.ProfileSubscription +import com.greenart7c3.nostrsigner.service.TorManager import com.greenart7c3.nostrsigner.service.crashreports.CrashReportCache import com.greenart7c3.nostrsigner.service.crashreports.UnexpectedCrashSaver import com.greenart7c3.nostrsigner.ui.ToastManager @@ -138,6 +140,9 @@ class Amber : val notificationCache = LruCache(10) fun isSocksProxyAlive(proxyHost: String, proxyPort: Int): Boolean { + if (settings.torMode == TorMode.BUILTIN) { + return TorManager.isRunning.value + } try { val socket = Socket() socket.connect(InetSocketAddress(proxyHost, proxyPort), 5000) // 3-second timeout @@ -245,6 +250,9 @@ class Amber : LocalPreferences.switchToAccount(this@Amber, LocalPreferences.allSavedAccounts(this@Amber).first().npub) } LocalPreferences.reloadApp() + if (settings.torMode == TorMode.BUILTIN) { + TorManager.start(this@Amber, applicationIOScope) + } checkForNewRelaysAndUpdateAllFilters(true) if (settings.killSwitch.value) { disconnectIntentionally() diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt index e0d076ba..3ceaef6a 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt @@ -10,6 +10,7 @@ import androidx.core.content.edit import androidx.core.os.LocaleListCompat import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.AmberSettings +import com.greenart7c3.nostrsigner.models.TorMode import com.greenart7c3.nostrsigner.models.defaultAppRelays import com.greenart7c3.nostrsigner.models.defaultIndexerRelays import com.greenart7c3.nostrsigner.okhttp.HttpClientManager @@ -47,6 +48,7 @@ private enum class SettingsKeys(val key: String) { SAVED_ACCOUNTS("all_saved_accounts"), USE_PROXY("use_proxy"), PROXY_PORT("proxy_port"), + TOR_MODE("tor_mode"), BATERRY_OPTIMIZATION("battery_optimization"), KILL_SWITCH("kill_switch"), LANGUAGE_PREFS("languagePreferences"), @@ -119,7 +121,7 @@ object LocalPreferences { putBoolean(SettingsKeys.USE_AUTH.key, settings.useAuth) putInt(SettingsKeys.BIOMETRICS_TYPE.key, settings.biometricsTimeType.screenCode) putBoolean(SettingsKeys.USE_PIN.key, settings.usePin) - putBoolean(SettingsKeys.USE_PROXY.key, settings.useProxy) + putString(SettingsKeys.TOR_MODE.key, settings.torMode.name) putInt(SettingsKeys.PROXY_PORT.key, settings.proxyPort) putBoolean(SettingsKeys.KILL_SWITCH.key, settings.killSwitch.value) putString(SettingsKeys.LANGUAGE_PREFS.key, settings.language) @@ -192,8 +194,23 @@ object LocalPreferences { fun loadSettingsFromEncryptedStorage(context: Context = Amber.instance): AmberSettings { checkNotInMainThread() sharedPrefs(context).apply { + // Migrate old use_proxy boolean to TorMode enum + val torMode = if (contains(SettingsKeys.TOR_MODE.key)) { + try { + TorMode.valueOf(getString(SettingsKeys.TOR_MODE.key, TorMode.DISABLED.name)!!) + } catch (e: IllegalArgumentException) { + TorMode.DISABLED + } + } else if (getBoolean(SettingsKeys.USE_PROXY.key, false)) { + TorMode.ORBOT + } else { + TorMode.DISABLED + } + val proxyPort = getInt(SettingsKeys.PROXY_PORT.key, 9050) - HttpClientManager.setDefaultProxyOnPort(proxyPort) + if (torMode == TorMode.ORBOT) { + HttpClientManager.setDefaultProxyOnPort(proxyPort) + } return AmberSettings( defaultRelays = getStringSet(SettingsKeys.DEFAULT_RELAYS.key, null)?.mapNotNull { @@ -206,8 +223,8 @@ object LocalPreferences { useAuth = getBoolean(SettingsKeys.USE_AUTH.key, false), biometricsTimeType = parseBiometricsTimeType(getInt(SettingsKeys.BIOMETRICS_TYPE.key, 0)), usePin = getBoolean(SettingsKeys.USE_PIN.key, false), - useProxy = getBoolean(SettingsKeys.USE_PROXY.key, false), - proxyPort = getInt(SettingsKeys.PROXY_PORT.key, 9050), + torMode = torMode, + proxyPort = proxyPort, killSwitch = MutableStateFlow(getBoolean(SettingsKeys.KILL_SWITCH.key, false)), language = getString(SettingsKeys.LANGUAGE_PREFS.key, null), ) @@ -425,15 +442,28 @@ object LocalPreferences { context: Context, useProxy: Boolean, port: Int, + ) { + val torMode = if (useProxy) TorMode.ORBOT else TorMode.DISABLED + updateTorMode(context, torMode, port) + } + + fun updateTorMode( + context: Context, + torMode: TorMode, + port: Int = Amber.instance.settings.proxyPort, ) { sharedPrefs(context).edit { apply { - putBoolean(SettingsKeys.USE_PROXY.key, useProxy) + putString(SettingsKeys.TOR_MODE.key, torMode.name) putInt(SettingsKeys.PROXY_PORT.key, port) } } Amber.instance.settings = loadSettingsFromEncryptedStorage() - HttpClientManager.setDefaultProxyOnPort(port) + when (torMode) { + TorMode.ORBOT -> HttpClientManager.setDefaultProxyOnPort(port) + TorMode.DISABLED -> HttpClientManager.clearProxy() + TorMode.BUILTIN -> { /* Proxy port set by TorManager once Tor starts */ } + } } suspend fun loadFromEncryptedStorage(context: Context, npub: String): Account? { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt index d1d02d3f..cc739c90 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt @@ -25,8 +25,10 @@ data class AmberSettings( val useAuth: Boolean = false, val biometricsTimeType: BiometricsTimeType = BiometricsTimeType.EVERY_TIME, val usePin: Boolean = false, - val useProxy: Boolean = false, + val torMode: TorMode = TorMode.DISABLED, val proxyPort: Int = 9050, val killSwitch: MutableStateFlow = MutableStateFlow(false), var language: String? = null, -) +) { + val useProxy: Boolean get() = torMode != TorMode.DISABLED +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/TorMode.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/TorMode.kt new file mode 100644 index 00000000..f05bdb6b --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/TorMode.kt @@ -0,0 +1,7 @@ +package com.greenart7c3.nostrsigner.models + +enum class TorMode { + DISABLED, + ORBOT, + BUILTIN, +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/okhttp/HttpClientManager.kt b/app/src/main/java/com/greenart7c3/nostrsigner/okhttp/HttpClientManager.kt index beb24417..42f4f262 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/okhttp/HttpClientManager.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/okhttp/HttpClientManager.kt @@ -110,4 +110,8 @@ object HttpClientManager { fun setDefaultProxyOnPort(port: Int) { setDefaultProxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port))) } + + fun clearProxy() { + setDefaultProxy(null) + } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt index 67ab6d6a..2feb5ad7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AccountStateViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.models.Account +import com.greenart7c3.nostrsigner.models.TorMode import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal @@ -111,7 +112,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() { if (LocalPreferences.allSavedAccounts(Amber.instance).isEmpty()) { Amber.instance.settings = Amber.instance.settings.copy( - useProxy = useProxy, + torMode = if (useProxy) TorMode.ORBOT else TorMode.DISABLED, proxyPort = proxyPort, ) LocalPreferences.saveSettingsToEncryptedStorage( @@ -145,7 +146,7 @@ class AccountStateViewModel(npub: String?) : ViewModel() { ) if (LocalPreferences.allSavedAccounts(Amber.instance).isEmpty()) { Amber.instance.settings = Amber.instance.settings.copy( - useProxy = useProxy, + torMode = if (useProxy) TorMode.ORBOT else TorMode.DISABLED, proxyPort = proxyPort, ) LocalPreferences.saveSettingsToEncryptedStorage( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index 197d47ab..c247b5dd 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -58,10 +58,13 @@ import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.BuildFlavorChecker import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.okhttp.HttpClientManager import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.AmberBunkerRequest import com.greenart7c3.nostrsigner.models.IntentData import com.greenart7c3.nostrsigner.models.IntentResultType +import com.greenart7c3.nostrsigner.models.TorMode +import com.greenart7c3.nostrsigner.service.TorManager import com.greenart7c3.nostrsigner.service.crashreports.DisplayCrashMessages import com.greenart7c3.nostrsigner.ui.actions.AccountBackupScreen import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet @@ -848,6 +851,9 @@ fun MainScreen( .padding(top = verticalPadding * 1.5f), onPost = { scope.launch(Dispatchers.IO) { + if (Amber.instance.settings.torMode == TorMode.BUILTIN) { + TorManager.stop() + } LocalPreferences.updateProxy(context, true, it) Amber.instance.checkForNewRelaysAndUpdateAllFilters() scope.launch { @@ -857,6 +863,21 @@ fun MainScreen( } } }, + onBuiltinTor = { + scope.launch(Dispatchers.IO) { + if (Amber.instance.settings.torMode == TorMode.ORBOT) { + HttpClientManager.clearProxy() + } + LocalPreferences.updateTorMode(context, TorMode.BUILTIN) + TorManager.start(context, Amber.instance.applicationIOScope) + Amber.instance.checkForNewRelaysAndUpdateAllFilters() + scope.launch { + navController.navigate(Route.Settings.route) { + popUpTo(0) + } + } + } + }, onError = { scope.launch { Toast.makeText( diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt index 94f3a0c0..a5f1a1ad 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/SettingsScreen.kt @@ -55,6 +55,7 @@ import com.greenart7c3.nostrsigner.BuildFlavorChecker import com.greenart7c3.nostrsigner.LocalPreferences import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account +import com.greenart7c3.nostrsigner.models.TorMode import com.greenart7c3.nostrsigner.ui.actions.LogoutDialog import com.greenart7c3.nostrsigner.ui.components.AmberButton import com.greenart7c3.nostrsigner.ui.components.IconRow @@ -78,7 +79,7 @@ fun SettingsScreen( ) { var logoutDialog by remember { mutableStateOf(false) } val context = LocalContext.current - var checked by remember { mutableStateOf(Amber.instance.settings.useProxy) } + var torMode by remember { mutableStateOf(Amber.instance.settings.torMode) } var disconnectTorDialog by remember { mutableStateOf(false) } val proxyPort = remember { mutableStateOf(Amber.instance.settings.proxyPort.toString()) } val scope = rememberCoroutineScope() @@ -178,10 +179,10 @@ fun SettingsScreen( .padding(vertical = 8.dp), ) { IconRow( - title = if (checked) { - stringResource(R.string.disconnect_from_your_orbot_setup) - } else { - stringResource(R.string.connect_via_tor_short) + title = when (torMode) { + TorMode.BUILTIN -> stringResource(R.string.disconnect_builtin_tor) + TorMode.ORBOT -> stringResource(R.string.disconnect_from_your_orbot_setup) + TorMode.DISABLED -> stringResource(R.string.connect_via_tor_short) }, icon = R.drawable.ic_tor, tint = MaterialTheme.colorScheme.onBackground, @@ -189,7 +190,7 @@ fun SettingsScreen( navController.navigate(Route.TorSettings.route) }, onClick = { - if (checked) { + if (torMode != TorMode.DISABLED) { disconnectTorDialog = true } else { navController.navigate(Route.TorSettings.route) @@ -369,9 +370,12 @@ fun SettingsScreen( TextButton( onClick = { disconnectTorDialog = false - checked = false + torMode = TorMode.DISABLED scope.launch(Dispatchers.IO) { - LocalPreferences.updateProxy(context, false, proxyPort.value.toInt()) + if (Amber.instance.settings.torMode == TorMode.BUILTIN) { + com.greenart7c3.nostrsigner.service.TorManager.stop() + } + LocalPreferences.updateTorMode(context, TorMode.DISABLED) Amber.instance.checkForNewRelaysAndUpdateAllFilters() } }, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt index 929b35c9..fe663657 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/ConnectOrbotDialog.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -39,12 +40,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.models.TorMode import com.greenart7c3.nostrsigner.ui.components.AmberButton import com.greenart7c3.nostrsigner.ui.theme.RichTextDefaults import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions @@ -57,6 +60,7 @@ import kotlinx.coroutines.CancellationException fun ConnectOrbotScreen( modifier: Modifier, onPost: (Int) -> Unit, + onBuiltinTor: () -> Unit, onError: (String) -> Unit, ) { val portNumber = remember { mutableStateOf(Amber.instance.settings.proxyPort.toString()) } @@ -64,6 +68,46 @@ fun ConnectOrbotScreen( Column( modifier = modifier, ) { + Text( + text = stringResource(R.string.builtin_tor_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.builtin_tor_description), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + AmberButton( + modifier = Modifier.fillMaxWidth(), + onClick = onBuiltinTor, + text = if (Amber.instance.settings.torMode == TorMode.BUILTIN) { + stringResource(R.string.builtin_tor_active) + } else { + stringResource(R.string.use_builtin_tor) + }, + enabled = Amber.instance.settings.torMode != TorMode.BUILTIN, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + HorizontalDivider() + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(R.string.orbot_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(8.dp)) + val myMarkDownStyle = RichTextDefaults.copy( stringStyle = RichTextDefaults.stringStyle?.copy( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ca303e0..9d8f6a79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,9 +109,15 @@ Invalid port number Use Orbot Disconnect Tor/Orbot - Tor/Orbot setup - Disconnect from your Orbot/Tor? + Tor setup + Disconnect from Tor? Your data will be immediately transferred in the regular network + Built-in Tor + Run Tor directly inside the app. No external app required. + Use Built-in Tor + Built-in Tor Active + Disconnect Built-in Tor + Orbot (External App) Yes No Could not connect to Tor diff --git a/app/src/offline/java/com/greenart7c3/nostrsigner/service/TorManager.kt b/app/src/offline/java/com/greenart7c3/nostrsigner/service/TorManager.kt new file mode 100644 index 00000000..42e71e4a --- /dev/null +++ b/app/src/offline/java/com/greenart7c3/nostrsigner/service/TorManager.kt @@ -0,0 +1,24 @@ +package com.greenart7c3.nostrsigner.service + +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +object TorManager { + private val _socksPort = MutableStateFlow(0) + val socksPort: StateFlow = _socksPort.asStateFlow() + + private val _isRunning = MutableStateFlow(false) + val isRunning: StateFlow = _isRunning.asStateFlow() + + @Suppress("UNUSED_PARAMETER") + fun start(context: Context, scope: CoroutineScope) { + // No-op in offline flavor + } + + fun stop() { + // No-op in offline flavor + } +} diff --git a/gradle.properties b/gradle.properties index e0ae6914..40ff5222 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,3 +23,4 @@ kotlin.code.style=official android.nonTransitiveRClass=true android.enableR8.fullMode=true org.gradle.configuration-cache=true +android.bundle.enableUncompressedNativeLibs=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f635dc8e..23d974d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,8 @@ collections = "0.4.0" pagingCommon = "3.4.1" mockk = "1.14.9" coroutinesTest = "1.10.2" +kmpTor = "2.6.0" +kmpTorResource = "408.19.0" [libraries] datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } @@ -81,6 +83,8 @@ paging-compose = { group = "androidx.paging", name = "paging-compose", version.r paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "pagingCommon" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesTest" } +kmptor-runtime = { module = "io.matthewnelson.kmp-tor:runtime", version.ref = "kmpTor" } +kmptor-resource-exec = { module = "io.matthewnelson.kmp-tor:resource-exec-tor", version.ref = "kmpTorResource" } [plugins] androidLibrary = { id = "com.android.library", version.ref = "agp" }