More refactoring for built-in Tor changes and fixes

This commit is contained in:
pokkst 2023-12-09 04:21:14 -06:00
parent bd67d6d4bd
commit a128617342
No known key found for this signature in database
GPG key ID: EC4FAAA66859FAA4
14 changed files with 131 additions and 113 deletions

View file

@ -10,8 +10,8 @@ android {
applicationId "net.mynero.wallet"
minSdkVersion 21
targetSdkVersion 34
versionCode 50400
versionName "0.5.4 'Fluorine Fermi'"
versionCode 50500
versionName "0.5.5 'Fluorine Fermi'"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {

View file

@ -45,7 +45,6 @@ class MainActivity : AppCompatActivity(), MoneroHandlerThread.Listener, Password
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ProxyService(this)
val walletFile = File(applicationInfo.dataDir, Constants.WALLET_NAME)
val walletKeysFile = File(applicationInfo.dataDir, Constants.WALLET_NAME + ".keys")
if (walletKeysFile.exists()) {

View file

@ -2,6 +2,7 @@ package net.mynero.wallet
import android.app.Application
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.NightmodeHelper
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@ -13,6 +14,7 @@ class MoneroApplication : Application() {
override fun onCreate() {
super.onCreate()
PrefService(this)
ProxyService(this)
NightmodeHelper.preferredNightmode
executor = Executors.newFixedThreadPool(16)
}

View file

@ -1,6 +1,7 @@
package net.mynero.wallet.fragment.home
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -78,7 +79,7 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
ProxyService.instance?.proxyChangeEvents?.observe(viewLifecycleOwner) { proxy ->
lifecycleScope.launch(Dispatchers.IO) {
Timber.d("Updating proxy:: $proxy")
Log.d("HomeFragment", "Updating proxy:: $proxy")
WalletManager.instance?.setProxy(proxy)
WalletManager.instance?.wallet?.setProxy(proxy)
WalletManager.instance?.wallet?.init(0)
@ -88,7 +89,7 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
DaemonService.instance?.daemonChangeEvents?.observe(viewLifecycleOwner) { daemon ->
lifecycleScope.launch(Dispatchers.IO) {
Timber.d("Updating daemon:: $daemon")
Log.d("HomeFragment", "Updating daemon:: $daemon")
WalletManager.instance?.setDaemon(daemon)
WalletManager.instance?.wallet?.init(0)
WalletManager.instance?.wallet?.setTrustedDaemon(daemon.trusted)

View file

@ -169,12 +169,7 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
samouraiTorManager?.getTorStateLiveData()?.observe(viewLifecycleOwner) { state ->
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if(socketAddress.toString().isEmpty()) return@let
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
if(mViewModel?.useProxy?.value == true && mViewModel?.useBundledTor?.value == true) {
mViewModel?.setProxyAddress(address)
mViewModel?.setProxyPort(port)
torIcon?.visibility = View.VISIBLE
indicatorCircle?.visibility = View.INVISIBLE
}
@ -188,11 +183,7 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.INVISIBLE
}
EnumTorState.STARTING -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.VISIBLE
}
EnumTorState.STOPPING -> {
EnumTorState.STARTING, EnumTorState.STOPPING -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.VISIBLE
}
@ -290,11 +281,6 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
}
useBundledTor?.setOnCheckedChangeListener { _, isChecked ->
if(!isChecked) {
mViewModel?.setProxyAddress("")
mViewModel?.setProxyPort("")
}
mViewModel?.setUseBundledTor(isChecked)
}
}

View file

@ -40,7 +40,7 @@ class OnboardingViewModel : ViewModel() {
_useProxy.value = ProxyService.instance?.usingProxy
_useBundledTor.value = ProxyService.instance?.useBundledTor
}
val enableButton = combineLiveDatas(
seedType,
_useProxy,
@ -49,14 +49,16 @@ class OnboardingViewModel : ViewModel() {
_useBundledTor,
_passphrase,
_confirmedPassphrase,
_creatingWallet
) { seedType, useProxy, proxyAddress, proxyPort, useBundledTor, passphrase, confirmedPassphrase, creatingWallet ->
_creatingWallet,
ProxyService.instance?.samouraiTorManager?.getTorStateLiveData()
) { seedType, useProxy, proxyAddress, proxyPort, useBundledTor, passphrase, confirmedPassphrase, creatingWallet, torState ->
if(seedType == null || useProxy == null || proxyAddress == null || proxyPort == null || useBundledTor == null || passphrase == null || confirmedPassphrase == null || creatingWallet == null) return@combineLiveDatas false
if((passphrase.isNotEmpty() || confirmedPassphrase.isNotEmpty()) && passphrase != confirmedPassphrase) return@combineLiveDatas false
if(creatingWallet) return@combineLiveDatas false
if(seedType == SeedType.POLYSEED && (passphrase.isEmpty() || confirmedPassphrase.isEmpty())) return@combineLiveDatas false
if(useProxy && (proxyAddress.isEmpty() || proxyPort.isEmpty())) return@combineLiveDatas false
if(useBundledTor && (proxyAddress.isEmpty() || proxyPort.isEmpty())) return@combineLiveDatas false
if(useProxy && (proxyAddress.isEmpty() || proxyPort.isEmpty()) && !useBundledTor) return@combineLiveDatas false
val progress = torState?.progressIndicator ?: 0
if(useBundledTor && progress < 100) return@combineLiveDatas false
return@combineLiveDatas true
}
@ -225,18 +227,16 @@ class OnboardingViewModel : ViewModel() {
fun setProxyAddress(address: String) {
_proxyAddress.value = address
if(address.isEmpty()) PrefService.instance?.deleteProxy()
val port = _proxyPort.value ?: return
val proxyAddress = "$address:$port"
if(proxyAddress == ":") return
PrefService.instance?.edit()?.putString(Constants.PREF_PROXY, proxyAddress)?.apply()
ProxyService.instance?.updateProxy(address, port)
}
fun setProxyPort(port: String) {
_proxyPort.value = port
if(port.isEmpty()) PrefService.instance?.deleteProxy()
val address = _proxyAddress.value ?: return
val proxyAddress = "$address:$port"
if(proxyAddress == ":") return
PrefService.instance?.edit()?.putString(Constants.PREF_PROXY, proxyAddress)?.apply()
ProxyService.instance?.updateProxy(address, port)
}
fun setUseBundledTor(useBundled: Boolean) {
@ -244,7 +244,7 @@ class OnboardingViewModel : ViewModel() {
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USE_BUNDLED_TOR, useBundled)?.apply()
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if(useBundled && useProxy.value == true) {
if(useBundled && ProxyService.instance?.usingProxy == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
@ -256,7 +256,7 @@ class OnboardingViewModel : ViewModel() {
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_PROXY, useProxy)?.apply()
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if(useProxy && useBundledTor.value == true) {
if(useProxy && ProxyService.instance?.useBundledTor == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()

View file

@ -46,8 +46,6 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
private var walletProxyAddressEditText: EditText? = null
private var walletProxyPortEditText: EditText? = null
private var selectNodeButton: Button? = null
private var cachedProxyAddress: String = ""
private var cachedProxyPort: String = ""
private var streetModeSwitch: SwitchCompat? = null
private var monerochanSwitch: SwitchCompat? = null
private var donationSwitch: SwitchCompat? = null
@ -87,8 +85,8 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
donationSwitch?.isChecked = PrefService.instance?.getBoolean(Constants.PREF_DONATE_PER_TX, false) == true
val usesProxy = ProxyService.instance?.usingProxy == true
cachedProxyAddress = ProxyService.instance?.proxyAddress ?: return
cachedProxyPort = ProxyService.instance?.proxyPort ?: return
val cachedProxyAddress = ProxyService.instance?.proxyAddress ?: return
val cachedProxyPort = ProxyService.instance?.proxyPort ?: return
if (ProxyService.instance?.hasProxySet() == true) {
initProxyStuff(cachedProxyAddress, cachedProxyPort)
}
@ -199,11 +197,7 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.INVISIBLE
}
EnumTorState.STARTING -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.VISIBLE
}
EnumTorState.STOPPING -> {
EnumTorState.STARTING, EnumTorState.STOPPING -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.VISIBLE
}
@ -215,11 +209,7 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
private fun refreshProxy() {
val proxyAddress = walletProxyAddressEditText?.text.toString()
val proxyPort = walletProxyPortEditText?.text.toString()
if((proxyAddress != cachedProxyAddress) || (proxyPort != cachedProxyPort)) {
ProxyService.instance?.updateProxy(proxyAddress, proxyPort)
cachedProxyAddress = proxyAddress
cachedProxyPort = proxyPort
}
ProxyService.instance?.updateProxy(proxyAddress, proxyPort)
}
private fun displaySeedDialog(password: String) {

View file

@ -26,7 +26,7 @@ class SettingsViewModel : ViewModel() {
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_PROXY, use)?.apply()
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if(use && useBundledTor.value == true) {
if(use && ProxyService.instance?.useBundledTor == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
@ -34,11 +34,11 @@ class SettingsViewModel : ViewModel() {
}
fun setUseBundledTor(use: Boolean) {
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USE_BUNDLED_TOR, use)?.apply()
_useBundledTor.value = use
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USE_BUNDLED_TOR, use)?.apply()
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if(use && useProxy.value == true) {
if(use && ProxyService.instance?.usingProxy == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()

View file

@ -3,7 +3,7 @@ package net.mynero.wallet.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
@ -12,7 +12,8 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
source6: LiveData<T6>,
source7: LiveData<T7>,
source8: LiveData<T8>,
func: (T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?) -> S?
source9: LiveData<T9>?,
func: (T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?, T9?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
@ -24,7 +25,8 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -37,7 +39,8 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -50,7 +53,8 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -63,7 +67,8 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -76,7 +81,8 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -89,7 +95,8 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -102,7 +109,8 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -115,9 +123,27 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value
source8.value,
source9?.value
)?.run { result.value = this }
}
source9?.let { src9 ->
result.addSource(src9) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value,
src9.value
)?.run { result.value = this }
}
}
return result
}

View file

@ -15,6 +15,7 @@
*/
package net.mynero.wallet.model
import android.util.Log
import android.util.Pair
import net.mynero.wallet.data.Subaddress
import timber.log.Timber
@ -60,7 +61,7 @@ class Wallet {
}
fun setAccountIndex(accountIndex: Int) {
Timber.d("setAccountIndex(%d)", accountIndex)
Log.d("Wallet.kt", "setAccountIndex($accountIndex)")
this.accountIndex = accountIndex
history?.setAccountFor(this)
}
@ -159,33 +160,33 @@ class Wallet {
var daemonUsername = WalletManager.instance?.daemonUsername
var daemonPassword = WalletManager.instance?.daemonPassword
var proxyAddress = WalletManager.instance?.proxy
Timber.d("init(")
Log.d("Wallet.kt", "init(")
if (daemonAddress != null) {
Timber.d(daemonAddress.toString())
Log.d("Wallet.kt", daemonAddress.toString())
} else {
Timber.d("daemon_address == null")
Log.d("Wallet.kt", "daemon_address == null")
daemonAddress = ""
}
Timber.d("upper_transaction_size_limit = 0 (probably)")
Log.d("Wallet.kt", "upper_transaction_size_limit = 0 (probably)")
if (daemonUsername != null) {
Timber.d(daemonUsername)
Log.d("Wallet.kt", daemonUsername)
} else {
Timber.d("daemon_username == null")
Log.d("Wallet.kt", "daemon_username == null")
daemonUsername = ""
}
if (daemonPassword != null) {
Timber.d(daemonPassword)
Log.d("Wallet.kt", daemonPassword)
} else {
Timber.d("daemon_password == null")
Log.d("Wallet.kt", "daemon_password == null")
daemonPassword = ""
}
if (proxyAddress != null) {
Timber.d(proxyAddress)
Log.d("Wallet.kt", proxyAddress)
} else {
Timber.d("proxy_address == null")
Log.d("Wallet.kt", "proxy_address == null")
proxyAddress = ""
}
Timber.d(");")
Log.d("Wallet.kt", ");")
return initJ(
daemonAddress, upperTransactionSizeLimit,
daemonUsername, daemonPassword,
@ -419,7 +420,7 @@ class Wallet {
val timeStamp = SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.US).format(Date())
addSubaddress(accountIndex, timeStamp)
val subaddress = getLastSubaddress(accountIndex)
Timber.d("%d: %s", getNumSubaddresses(accountIndex) - 1, subaddress)
Log.d("Wallet.kt", "${(getNumSubaddresses(accountIndex) - 1)}: $subaddress")
return subaddress
}

View file

@ -2,6 +2,7 @@ package net.mynero.wallet.service
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import net.mynero.wallet.MoneroApplication
import net.mynero.wallet.data.DefaultNodes
import net.mynero.wallet.data.Node
@ -79,6 +80,23 @@ class PrefService(application: MoneroApplication) : ServiceBase(null) {
return value == true
}
fun saveProxy(address: String, port: String): String? {
if(address.isEmpty() || port.isEmpty()) {
deleteProxy()
return null
}
val proxyAddress = "$address:$port"
if(proxyAddress == ":") return null
Log.d("PrefService", "Setting proxy. proxyAddress=$proxyAddress")
edit()?.putString(Constants.PREF_PROXY, proxyAddress)?.apply()
return proxyAddress
}
fun deleteProxy() {
Log.d("PrefService", "Deleting proxy...")
edit()?.putString(Constants.PREF_PROXY, "")?.apply()
}
companion object {
private var preferences: SharedPreferences? = null

View file

@ -1,35 +1,33 @@
package net.mynero.wallet.service
import android.app.Application
import android.util.Log
import android.util.Patterns
import net.mynero.wallet.MainActivity
import net.mynero.wallet.livedata.SingleLiveEvent
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Constants
class ProxyService(activity: MainActivity) : ServiceBase(null) {
class ProxyService(application: Application) : ServiceBase(null) {
val proxyChangeEvents: SingleLiveEvent<String> = SingleLiveEvent()
var samouraiTorManager: SamouraiTorManager? = null
init {
samouraiTorManager = SamouraiTorManager(activity.application, TorKmpManager(activity.application))
instance = this
samouraiTorManager = SamouraiTorManager(application, TorKmpManager(application))
activity.runOnUiThread {
samouraiTorManager?.getTorStateLiveData()?.observeForever {
samouraiTorManager?.getProxy()?.address()?.let { socketAddress ->
if(socketAddress.toString().isEmpty()) return@let
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
if(usingProxy && useBundledTor)
updateProxy(address, port)
}
samouraiTorManager?.getTorStateLiveData()?.observeForever {
samouraiTorManager?.getProxy()?.address()?.let { socketAddress ->
if(socketAddress.toString().isEmpty()) return@let
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
if(usingProxy && useBundledTor)
updateProxy(address, port)
}
}
if(useBundledTor) {
samouraiTorManager?.start()
}
if(useBundledTor && usingProxy) {
samouraiTorManager?.start()
}
}
@ -43,16 +41,16 @@ class ProxyService(activity: MainActivity) : ServiceBase(null) {
if (!usingProxy || isNodeLocalIp) {
// User is not using proxy, or is using local node currently, so we will disable proxy here.
proxyChangeEvents.postValue("")
Log.d("ProxyService", "Not using proxy...")
return
}
// We are using proxy at this point, but user set them to empty. We will fallback to Tor defaults here.
if (proxyAddress.isEmpty()) finalProxyAddress = "127.0.0.1"
if (proxyPort.isEmpty()) finalProxyPort = "9050"
val validIpAddress = Patterns.IP_ADDRESS.matcher(proxyAddress).matches()
val validIpAddress = Patterns.IP_ADDRESS.matcher(finalProxyAddress).matches()
if (validIpAddress) {
val proxy = "$finalProxyAddress:$finalProxyPort"
PrefService.instance?.edit()?.putString(Constants.PREF_PROXY, proxy)?.apply()
proxyChangeEvents.postValue(proxy)
val proxy = PrefService.instance?.saveProxy(finalProxyAddress, finalProxyPort)
proxy?.let { proxyChangeEvents.postValue(it) }
}
}

View file

@ -1,44 +1,41 @@
package net.mynero.wallet.service
import android.app.Application
import android.util.Log
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.TorState
import org.json.JSONException
import org.json.JSONObject
import java.net.Proxy
class SamouraiTorManager(val appContext: Application?, val torKmpManager: TorKmpManager?) {
class SamouraiTorManager(private val appContext: Application?, private val torKmpManager: TorKmpManager) {
fun getTorStateLiveData(): MutableLiveData<TorState> {
return torKmpManager!!.torStateLiveData
return torKmpManager.torStateLiveData
}
fun getTorState(): TorState {
return torKmpManager!!.torState
return torKmpManager.torState
}
fun isConnected(): Boolean {
return torKmpManager?.isConnected() ?: false
return torKmpManager.isConnected()
}
fun isStarting(): Boolean {
return torKmpManager?.isStarting() ?: false
return torKmpManager.isStarting()
}
fun stop() {
torKmpManager?.torOperationManager?.stopQuietly()
torKmpManager.torOperationManager.stopQuietly()
}
fun start() {
torKmpManager?.torOperationManager?.startQuietly()
torKmpManager.torOperationManager.startQuietly()
}
fun getProxy(): Proxy? {
return torKmpManager?.proxy
return torKmpManager.proxy
}
fun newIdentity() {
torKmpManager?.newIdentity(appContext!!)
appContext?.let { torKmpManager.newIdentity(it) }
}
companion object {

View file

@ -16,7 +16,6 @@
android:text="@string/settings"
android:layout_marginTop="24dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/settings_tor_loading_progressindicator"
@ -28,10 +27,10 @@
android:layout_height="32dp"
android:src="@drawable/tor"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="@id/settings_tor_loading_progressindicator"
app:layout_constraintStart_toStartOf="@id/settings_tor_loading_progressindicator"
app:layout_constraintTop_toTopOf="@id/settings_tor_loading_progressindicator"
app:layout_constraintBottom_toBottomOf="@id/settings_tor_loading_progressindicator"/>
android:layout_marginEnd="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/settings_textview"
app:layout_constraintBottom_toBottomOf="@id/settings_textview"/>
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/settings_tor_loading_progressindicator"
@ -39,9 +38,10 @@
android:layout_height="32dp"
android:indeterminate="true"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/settings_textview"
app:layout_constraintBottom_toBottomOf="@id/settings_textview"/>
app:layout_constraintEnd_toEndOf="@id/settings_tor_icon"
app:layout_constraintStart_toStartOf="@id/settings_tor_icon"
app:layout_constraintTop_toTopOf="@id/settings_tor_icon"
app:layout_constraintBottom_toBottomOf="@id/settings_tor_icon"/>
<TextView
android:id="@+id/wallet_settings_textview"