diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 8f028fb72..e1bcb40c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -103,10 +103,21 @@ class ServiceManager( if (myAccount != null) { when (myAccount.settings.torSettings.torType.value) { TorType.INTERNAL -> { - Log.d("TorManager", "Relays Service Connected ${TorManager.socksPort()}") - HttpClientManager.setDefaultProxyOnPort(TorManager.socksPort()) + // Tor's lib will automatically set this port. + if (TorManager.isSocksReady()) { + HttpClientManager.setDefaultProxyOnPort(TorManager.socksPort()) + } else { + HttpClientManager.setProxyNotReady() + } + } + TorType.EXTERNAL -> { + val port = myAccount.settings.torSettings.externalSocksPort.value + if (port > 0) { + HttpClientManager.setDefaultProxyOnPort(port) + } else { + HttpClientManager.setProxyNotReady() + } } - TorType.EXTERNAL -> HttpClientManager.setDefaultProxyOnPort(myAccount.settings.torSettings.externalSocksPort.value) else -> HttpClientManager.setDefaultProxy(null) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 65f697d0c..9de50211c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS @@ -3648,12 +3647,6 @@ class Account( fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME) - fun httpClientForCoil() = HttpClientManager.getHttpClient(shouldUseTorForImageDownload()) - - fun httpClientForRelay(dirtyUrl: String) = HttpClientManager.getHttpClient(shouldUseTorForDirty(dirtyUrl)) - - fun httpClientForPreviewUrl(url: String) = HttpClientManager.getHttpClient(shouldUseTorForPreviewUrl(url)) - fun shouldUseTorForImageDownload() = when (settings.torSettings.torType.value) { TorType.OFF -> false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt index f1ffc9174..2cb3ee404 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.okhttp +import android.R.attr.port import android.util.Log import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import okhttp3.OkHttpClient @@ -35,6 +36,8 @@ object HttpClientManager { .followSslRedirects(true) .build() + val DEFAULT_TOR_PROXY = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) + val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L) val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L) @@ -43,13 +46,13 @@ object HttpClientManager { private var defaultHttpClientWithoutProxy: OkHttpClient? = null private var userAgent: String = "Amethyst" - private var currentProxy: Proxy? = null + private var currentProxy: Proxy? = DEFAULT_TOR_PROXY private val cache = EncryptionKeyCache() fun setDefaultProxy(proxy: Proxy?) { if (currentProxy != proxy) { - Log.d("HttpClient", "Changing proxy to: ${proxy != null}") + Log.d("HttpClient", "Changing proxy to: $proxy") currentProxy = proxy // recreates singleton @@ -117,6 +120,11 @@ object HttpClientManager { defaultHttpClientWithoutProxy!! } + fun setProxyNotReady() { + // this blocks all connections unless Orbot is live. + setDefaultProxy(DEFAULT_TOR_PROXY) + } + fun setDefaultProxyOnPort(port: Int) { setDefaultProxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port))) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 78398f156..b2c77c96c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -46,7 +46,6 @@ import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme -import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress @@ -112,8 +111,6 @@ class MainActivity : AppCompatActivity() { checkLanguage(locales.get(0).language) } - TorManager.startTor(this) - Log.d("Lifetime Event", "MainActivity.onResume") // starts muted every time @@ -160,8 +157,6 @@ class MainActivity : AppCompatActivity() { (getSystemService(ConnectivityManager::class.java) as ConnectivityManager) .unregisterNetworkCallback(networkCallback) - TorManager.stopTor(this) - super.onPause() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index 13ecd28de..ea1c383f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -50,6 +50,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextFieldDefaults.contentPadding import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -246,7 +247,9 @@ private fun DialogContent( val writeStoragePermissionState = rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { isGranted -> if (isGranted) { - saveMediaToGallery(myContent, localContext, accountViewModel) + scope.launch { + saveMediaToGallery(myContent, localContext, accountViewModel) + } scope.launch { Toast .makeText( @@ -264,7 +267,9 @@ private fun DialogContent( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || writeStoragePermissionState.status.isGranted ) { - saveMediaToGallery(myContent, localContext, accountViewModel) + scope.launch { + saveMediaToGallery(myContent, localContext, accountViewModel) + } scope.launch { Toast .makeText( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 473796764..a69208190 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -39,13 +39,16 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel @@ -58,8 +61,13 @@ import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.tor.TorManager +import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable fun AccountScreen( @@ -136,6 +144,8 @@ fun LoggedInPage( accountViewModel.restartServices() } + ManageTorInstance(accountViewModel) + ListenToExternalSignerIfNeeded(accountViewModel) AppNavigation( @@ -145,6 +155,55 @@ fun LoggedInPage( ) } +@Composable +fun ManageTorInstance(accountViewModel: AccountViewModel) { + val torSettings by accountViewModel.account.settings.torSettings.torType + .collectAsStateWithLifecycle() + if (torSettings == TorType.INTERNAL) { + ManageTorInstanceInner(accountViewModel) + } +} + +@Composable +fun ManageTorInstanceInner(accountViewModel: AccountViewModel) { + val context = LocalContext.current.applicationContext + val lifeCycleOwner = LocalLifecycleOwner.current + + val scope = rememberCoroutineScope() + var job = remember { null } + + DisposableEffect(key1 = accountViewModel) { + job?.cancel() + job = null + TorManager.startTorIfNotAlreadyOn(context) + + val observer = + LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + job?.cancel() + job = null + TorManager.startTorIfNotAlreadyOn(context) + } + Lifecycle.Event.ON_PAUSE -> { + job = + scope.launch { + delay(5000) // 5 seconds + TorManager.stopTor(context) + } + } + else -> {} + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + TorManager.stopTor(context) + } + } +} + @Composable private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) { if (accountViewModel.account.signer is NostrSignerExternal) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt index 446476e24..a25fc1340 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt @@ -30,20 +30,40 @@ import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import org.torproject.jni.TorService import org.torproject.jni.TorService.LocalBinder +import java.util.concurrent.atomic.AtomicBoolean object TorManager { + var runningIntent: Intent? = null var torService: TorService? = null + // To make sure we don't start two services. + var isConnectingMutex = AtomicBoolean(false) + fun startTorIfNotAlreadyOn(ctx: Context) { - if (torService == null) { - startTor(ctx) + if (runningIntent == null && isConnectingMutex.compareAndSet(false, true)) { + try { + startTor(ctx) + } finally { + isConnectingMutex.set(false) + } } } - fun startTor(ctx: Context) { + fun stopTor(ctx: Context) { + Log.d("TorManager", "Stopping Tor Service") + runningIntent?.let { + ctx.stopService(runningIntent) + } + runningIntent = null + torService = null + } + + private fun startTor(ctx: Context) { Log.d("TorManager", "Binding Tor Service") + val currentIntent = Intent(ctx, TorService::class.java) + runningIntent = currentIntent ctx.bindService( - Intent(ctx, TorService::class.java), + currentIntent, object : ServiceConnection { override fun onServiceConnected( name: ComponentName, @@ -66,6 +86,7 @@ object TorManager { } override fun onServiceDisconnected(name: ComponentName) { + runningIntent = null torService = null Log.d("TorManager", "Tor Service Disconected") } @@ -74,12 +95,6 @@ object TorManager { ) } - fun stopTor(ctx: Context) { - Log.d("TorManager", "Stopping Tor Service") - torService = null - ctx.stopService(Intent(ctx, TorService::class.java)) - } - fun isSocksReady() = torService?.let { it.socksPort > 0