diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c7929cf2..7ba5daf74 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,9 @@ - + + + diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 001c4ddde..55421207f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.hexToByteArray +import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent @@ -55,6 +56,8 @@ private object PrefKeys { const val LATEST_CONTACT_LIST = "latestContactList" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog" const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog" + const val USE_PROXY = "use_proxy" + const val PROXY_PORT = "proxy_port" val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } } @@ -209,6 +212,8 @@ object LocalPreferences { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) + putBoolean(PrefKeys.USE_PROXY, account.proxy != null) + putInt(PrefKeys.PROXY_PORT, account.proxyPort) }.apply() } @@ -283,6 +288,9 @@ object LocalPreferences { val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) + val useProxy = getBoolean(PrefKeys.USE_PROXY, false) + val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050) + val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) val a = Account( Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()), @@ -301,7 +309,9 @@ object LocalPreferences { zapPaymentRequestServer, hideDeleteRequestDialog, hideBlockAlertDialog, - latestContactList + latestContactList, + proxy, + proxyPort ) return a diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 480097a68..f283fa812 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource @@ -26,7 +27,7 @@ object ServiceManager { fun start() { val myAccount = account - + HttpClient.start(account) if (myAccount != null) { Client.connect(myAccount.activeRelays() ?: myAccount.convertLocalRelays()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 9fa2c65f6..fe5cbba79 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.launch import nostr.postr.Persona import nostr.postr.Utils import java.math.BigDecimal +import java.net.Proxy import java.util.Locale val DefaultChannels = setOf( @@ -59,7 +60,9 @@ class Account( var zapPaymentRequest: Nip47URI? = null, var hideDeleteRequestDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false, - var backupContactList: ContactListEvent? = null + var backupContactList: ContactListEvent? = null, + var proxy: Proxy?, + var proxyPort: Int ) { var transientHiddenUsers: Set = setOf() @@ -936,7 +939,7 @@ class Account( fun activeRelays(): Array? { var usersRelayList = userProfile().latestContactList?.relays()?.map { val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() - Relay(it.key, it.value.read, it.value.write, localFeedTypes) + Relay(it.key, it.value.read, it.value.write, localFeedTypes, proxy) } ?: return null // Ugly, but forces nostr.band as the only search-supporting relay today. @@ -946,7 +949,8 @@ class Account( Constants.forcedRelayForSearch.url, Constants.forcedRelayForSearch.read, Constants.forcedRelayForSearch.write, - Constants.forcedRelayForSearch.feedTypes + Constants.forcedRelayForSearch.feedTypes, + proxy ) } @@ -955,7 +959,7 @@ class Account( fun convertLocalRelays(): Array { return localRelays.map { - Relay(it.url, it.read, it.write, it.feedTypes) + Relay(it.url, it.read, it.write, it.feedTypes, proxy) }.toTypedArray() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt new file mode 100644 index 000000000..bf2448ece --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/HttpClient.kt @@ -0,0 +1,34 @@ +package com.vitorpamplona.amethyst.service + +import com.vitorpamplona.amethyst.model.Account +import okhttp3.OkHttpClient +import java.net.InetSocketAddress +import java.net.Proxy +import java.time.Duration + +object HttpClient { + private var proxy: Proxy? = null + + fun start(account: Account?) { + this.proxy = account?.proxy + } + + fun getHttpClient(): OkHttpClient { + val seconds = if (proxy != null) 20L else 10L + val duration = Duration.ofSeconds(seconds) + return OkHttpClient.Builder() + .proxy(proxy) + .readTimeout(duration) + .connectTimeout(duration) + .writeTimeout(duration) + .build() + } + + fun getProxy(): Proxy? { + return proxy + } + + fun initProxy(useProxy: Boolean, hostname: String, port: Int): Proxy? { + return if (useProxy) Proxy(Proxy.Type.SOCKS, InetSocketAddress(hostname, port)) else null + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt index 2a0ee3240..e86a85633 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip05Verifier.kt @@ -9,12 +9,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.Call import okhttp3.Callback -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response -class Nip05Verifier { - val client = OkHttpClient.Builder().build() +class Nip05Verifier() { + val client = HttpClient.getHttpClient() fun assembleUrl(nip05address: String): String? { val parts = nip05address.trim().split("@") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index a9459b21c..78ea52018 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service.lnurl import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.service.HttpClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -10,14 +11,13 @@ import kotlinx.coroutines.withContext import nostr.postr.Bech32 import okhttp3.Call import okhttp3.Callback -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import java.math.BigDecimal import java.net.URLEncoder -class LightningAddressResolver { - val client = OkHttpClient.Builder().build() +class LightningAddressResolver() { + val client = HttpClient.getHttpClient() fun assembleUrl(lnaddress: String): String? { val parts = lnaddress.split("@") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index d3cb9d0c5..f3772e3e4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -4,13 +4,13 @@ import android.util.Log import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.model.RelayAuthEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody @@ -54,7 +54,7 @@ class RegisterAccounts( .post(body) .build() - val client = OkHttpClient.Builder().build() + val client = HttpClient.getHttpClient() client.newCall(request).execute() } catch (e: java.lang.Exception) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 256e1b804..3fb91722f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service.relays +import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.EventInterface import kotlinx.coroutines.DelicateCoroutinesApi @@ -91,7 +92,7 @@ object Client : RelayPool.Listener { @OptIn(DelicateCoroutinesApi::class) fun newSporadicRelay(url: String, feedTypes: Set?, onConnected: (Relay) -> Unit, onDone: (() -> Unit)?) { - val relay = Relay(url, true, true, feedTypes ?: emptySet()) + val relay = Relay(url, true, true, feedTypes ?: emptySet(), HttpClient.getProxy()) RelayPool.addRelay(relay) relay.requestAndWatch { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt index a167a2d99..d62179f8d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.service.relays import com.vitorpamplona.amethyst.model.RelaySetupInfo +import com.vitorpamplona.amethyst.service.HttpClient object Constants { val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS) @@ -10,7 +11,7 @@ object Constants { fun convertDefaultRelays(): Array { return defaultRelays.map { - Relay(it.url, it.read, it.write, it.feedTypes) + Relay(it.url, it.read, it.write, it.feedTypes, HttpClient.getProxy()) }.toTypedArray() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 280e7bbe7..b4e0a9341 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -11,6 +11,8 @@ import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener +import java.net.Proxy +import java.time.Duration import java.util.Date enum class FeedType { @@ -23,9 +25,16 @@ class Relay( var url: String, var read: Boolean = true, var write: Boolean = true, - var activeTypes: Set = FeedType.values().toSet() + var activeTypes: Set = FeedType.values().toSet(), + proxy: Proxy? ) { + val seconds = if (proxy != null) 20L else 10L + val duration = Duration.ofSeconds(seconds) private val httpClient = OkHttpClient.Builder() + .proxy(proxy) + .readTimeout(duration) + .connectTimeout(duration) + .writeTimeout(duration) .followRedirects(true) .followSslRedirects(true) .build() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt index 6514c5683..29ba8da53 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageSaver.kt @@ -10,6 +10,7 @@ import android.provider.MediaStore import android.webkit.MimeTypeMap import androidx.annotation.RequiresApi import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.service.HttpClient import okhttp3.* import okio.BufferedSource import okio.IOException @@ -32,7 +33,7 @@ object ImageSaver { onSuccess: () -> Any?, onError: (Throwable) -> Any? ) { - val client = OkHttpClient.Builder().build() + val client = HttpClient.getHttpClient() val request = Request.Builder() .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt index 52a83020f..4f0d840af 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt @@ -6,6 +6,7 @@ import android.provider.OpenableColumns import android.webkit.MimeTypeMap import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.service.HttpClient import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okio.BufferedSink @@ -70,7 +71,7 @@ object ImageUploader { val fileName = randomChars() val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" - val client = OkHttpClient.Builder().build() + val client = HttpClient.getHttpClient() val requestBody: RequestBody val requestBuilder = Request.Builder() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index defddc0c9..7fc79e207 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.AlertDialog import androidx.compose.material.Divider import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon @@ -24,7 +25,9 @@ import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ScaffoldState import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -45,13 +48,17 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @@ -100,7 +107,12 @@ fun DrawerContent( } @Composable -fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffoldState: ScaffoldState, navController: NavController) { +fun ProfileContent( + baseAccountUser: User, + modifier: Modifier = Modifier, + scaffoldState: ScaffoldState, + navController: NavController +) { val coroutineScope = rememberCoroutineScope() val accountUserState by baseAccountUser.live().metadata.observeAsState() @@ -197,11 +209,17 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol }) ) { Row() { - Text("${accountUserFollows.cachedFollowCount() ?: "--"}", fontWeight = FontWeight.Bold) + Text( + "${accountUserFollows.cachedFollowCount() ?: "--"}", + fontWeight = FontWeight.Bold + ) Text(stringResource(R.string.following)) } Row(modifier = Modifier.padding(start = 10.dp)) { - Text("${accountUserFollows.cachedFollowerCount() ?: "--"}", fontWeight = FontWeight.Bold) + Text( + "${accountUserFollows.cachedFollowerCount() ?: "--"}", + fontWeight = FontWeight.Bold + ) Text(stringResource(R.string.followers)) } } @@ -221,6 +239,10 @@ fun ListContent( ) { val coroutineScope = rememberCoroutineScope() var backupDialogOpen by remember { mutableStateOf(false) } + var checked by remember { mutableStateOf(account.proxy != null) } + val openDialog = remember { mutableStateOf(false) } + var conectOrbotDialogOpen by remember { mutableStateOf(false) } + var proxyPort = remember { mutableStateOf(account.proxyPort.toString()) } Column(modifier = modifier.fillMaxHeight()) { if (accountUser != null) { @@ -259,6 +281,21 @@ fun ListContent( onClick = { backupDialogOpen = true } ) + val textTorProxy = if (checked) stringResource(R.string.disconnect_from_your_orbot_setup) else stringResource(R.string.connect_via_tor) + + IconRow( + title = textTorProxy, + icon = R.drawable.ic_tor, + tint = MaterialTheme.colors.onBackground, + onClick = { + if (checked) { + openDialog.value = true + } else { + conectOrbotDialogOpen = true + } + } + ) + Spacer(modifier = Modifier.weight(1f)) IconRow( @@ -272,6 +309,58 @@ fun ListContent( if (backupDialogOpen) { AccountBackupDialog(account, onClose = { backupDialogOpen = false }) } + + if (conectOrbotDialogOpen) { + ConnectOrbotDialog( + onClose = { conectOrbotDialogOpen = false }, + onPost = { + conectOrbotDialogOpen = false + openDialog.value = false + checked = true + enableTor(account, true, proxyPort) + }, + proxyPort + ) + } + + if (openDialog.value) { + AlertDialog( + text = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor)) }, + onDismissRequest = { }, + confirmButton = { + TextButton( + onClick = { + openDialog.value = false + checked = false + enableTor(account, false, proxyPort) + } + ) { + Text(text = stringResource(R.string.yes)) + } + }, + dismissButton = { + TextButton( + onClick = { + openDialog.value = false + } + ) { + Text(text = stringResource(R.string.no)) + } + } + ) + } +} + +private fun enableTor( + account: Account, + checked: Boolean, + portNumber: MutableState +) { + account.proxyPort = portNumber.value.toInt() + account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort) + LocalPreferences.saveToEncryptedStorage(account) + ServiceManager.pause() + ServiceManager.start() } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 04a81c790..bf25694a2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.hexToByteArray +import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.nip19.Nip19 import fr.acinq.secp256k1.Hex import kotlinx.coroutines.CoroutineScope @@ -39,21 +40,22 @@ class AccountStateViewModel() : ViewModel() { } } - fun startUI(key: String) { + fun startUI(key: String, useProxy: Boolean, proxyPort: Int) { val pattern = Pattern.compile(".+@.+\\.[a-z]+") val parsed = Nip19.uriToRoute(key) val pubKeyParsed = parsed?.hex?.hexToByteArray() + val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) val account = if (key.startsWith("nsec")) { - Account(Persona(privKey = key.bechToBytes())) + Account(Persona(privKey = key.bechToBytes()), proxy = proxy, proxyPort = proxyPort) } else if (pubKeyParsed != null) { - Account(Persona(pubKey = pubKeyParsed)) + Account(Persona(pubKey = pubKeyParsed), proxy = proxy, proxyPort = proxyPort) } else if (pattern.matcher(key).matches()) { // Evaluate NIP-5 - Account(Persona()) + Account(Persona(), proxy = proxy, proxyPort = proxyPort) } else { - Account(Persona(Hex.decode(key))) + Account(Persona(Hex.decode(key)), proxy = proxy, proxyPort = proxyPort) } LocalPreferences.updatePrefsForLogin(account) @@ -66,8 +68,9 @@ class AccountStateViewModel() : ViewModel() { tryLoginExistingAccount() } - fun newKey() { - val account = Account(Persona()) + fun newKey(useProxy: Boolean, proxyPort: Int) { + val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) + val account = Account(Persona(), proxy = proxy, proxyPort = proxyPort) // saves to local preferences LocalPreferences.updatePrefsForLogin(account) startUI(account) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt new file mode 100644 index 000000000..53c2ac84e --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ConnectOrbotDialog.kt @@ -0,0 +1,120 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.material.MaterialRichText +import com.halilibo.richtext.ui.resolveDefaults +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.actions.PostButton +import kotlinx.coroutines.launch + +@Composable +fun ConnectOrbotDialog(onClose: () -> Unit, onPost: () -> Unit, portNumber: MutableState) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + Dialog( + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .padding(10.dp) + .background(MaterialTheme.colors.background) + .fillMaxSize() + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + CloseButton(onCancel = { + onClose() + }) + + val toastMessage = stringResource(R.string.invalid_port_number) + + PostButton( + onPost = { + try { + Integer.parseInt(portNumber.value) + } catch (_: Exception) { + scope.launch { + Toast.makeText( + context, + toastMessage, + Toast.LENGTH_LONG + ).show() + } + return@PostButton + } + + onPost() + }, + isActive = true + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 30.dp) + ) { + MaterialRichText( + style = RichTextStyle().resolveDefaults() + ) { + Markdown( + content = stringResource(R.string.connect_through_your_orbot_setup_markdown) + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + OutlinedTextField( + value = portNumber.value, + onValueChange = { portNumber.value = it }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Number + ), + label = { Text(text = stringResource(R.string.orbot_socks_port)) }, + placeholder = { + Text( + text = "9050", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + ) + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index ec45c3aff..450b14777 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedOff +import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -38,6 +39,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog import java.util.* @OptIn(ExperimentalComposeUiApi::class) @@ -55,6 +57,9 @@ fun LoginPage( var dialogOpen by remember { mutableStateOf(false) } + val useProxy = remember { mutableStateOf(false) } + val proxyPort = remember { mutableStateOf("9050") } + var connectOrbotDialogOpen by remember { mutableStateOf(false) } Column( modifier = Modifier @@ -158,7 +163,7 @@ fun LoginPage( keyboardActions = KeyboardActions( onGo = { try { - accountViewModel.startUI(key.value.text) + accountViewModel.startUI(key.value.text, useProxy.value, proxyPort.value.toInt()) } catch (e: Exception) { errorMessage = context.getString(R.string.invalid_key) } @@ -221,6 +226,32 @@ fun LoginPage( } } + if (isPackageInstalled(context, "org.torproject.android")) { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = useProxy.value, + onCheckedChange = { + if (it) { + connectOrbotDialogOpen = true + } + } + ) + + Text(stringResource(R.string.connect_via_tor)) + } + + if (connectOrbotDialogOpen) { + ConnectOrbotDialog( + onClose = { connectOrbotDialogOpen = false }, + onPost = { + connectOrbotDialogOpen = false + useProxy.value = true + }, + proxyPort + ) + } + } + Spacer(modifier = Modifier.height(20.dp)) Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) { @@ -237,7 +268,7 @@ fun LoginPage( if (acceptedTerms.value && key.value.text.isNotBlank()) { try { - accountViewModel.startUI(key.value.text) + accountViewModel.startUI(key.value.text, useProxy.value, proxyPort.value.toInt()) } catch (e: Exception) { errorMessage = context.getString(R.string.invalid_key) } @@ -265,7 +296,7 @@ fun LoginPage( .fillMaxWidth(), onClick = { if (acceptedTerms.value) { - accountViewModel.newKey() + accountViewModel.newKey(useProxy.value, proxyPort.value.toInt()) } else { termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required) @@ -280,3 +311,7 @@ fun LoginPage( ) } } + +fun isPackageInstalled(context: Context, target: String): Boolean { + return context.packageManager.getInstalledApplications(0).find { info -> info.packageName == target } != null +} diff --git a/app/src/main/res/drawable/ic_tor.xml b/app/src/main/res/drawable/ic_tor.xml new file mode 100644 index 000000000..bafee0c19 --- /dev/null +++ b/app/src/main/res/drawable/ic_tor.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f2ceb9b0a..c7323b8a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -296,6 +296,11 @@ Poll authors can\'t vote in their own polls. #zappoll + Connect through your Orbot setup + Do you really want to disconnect from your Orbot setup? + Yes + No + Disconnect from your Orbot setup Image is the same since the post Image has changed. The author might not have seen the change @@ -360,6 +365,17 @@ Follow List All Follows Global + + ## Connect through your Orbot setup + \n\n1. Install [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) + \n2. Start Orbot + \n3. In Orbot check the Socks port. By default it uses the port 9050 + \n4. If necessary change the port + \n5. Configure the Socks port in this screen + \n6. Press the post button to use Orbot as a proxy + + Orbot Socks Port + Invalid port number DefaultChannelID New notification arrived