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