WIP: Embedded Tor

This commit is contained in:
pokkst 2023-12-08 19:08:05 -06:00
parent dc178f10d9
commit 3b77ae3673
No known key found for this signature in database
GPG key ID: EC4FAAA66859FAA4
26 changed files with 1236 additions and 249 deletions

View file

@ -138,6 +138,13 @@ dependencies {
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// Tor
def vTor = '4.8.6-0'
def vKmpTor = '1.4.4'
implementation 'io.samourai.code.wallet:android-tor-binary:0.4.7.12'
implementation "io.matthewnelson.kotlin-components:kmp-tor:$vTor-$vKmpTor"
//noinspection GradleDependency //noinspection GradleDependency
testImplementation "junit:junit:4.13.2" testImplementation "junit:junit:4.13.2"
testImplementation "org.mockito:mockito-all:1.10.19" testImplementation "org.mockito:mockito-all:1.10.19"

View file

@ -1080,7 +1080,7 @@ Java_net_mynero_wallet_model_Wallet_getMaximumAllowedAmount(JNIEnv *env, jclass
} }
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_net_mynero_wallet_model_Wallet_startRefresh(JNIEnv *env, jobject instance) { Java_net_mynero_wallet_model_Wallet_startRefreshJ(JNIEnv *env, jobject instance) {
Monero::Wallet *wallet = getHandle<Monero::Wallet>(env, instance); Monero::Wallet *wallet = getHandle<Monero::Wallet>(env, instance);
wallet->startRefresh(); wallet->startRefresh();
} }

View file

@ -45,6 +45,7 @@ class MainActivity : AppCompatActivity(), MoneroHandlerThread.Listener, Password
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
ProxyService(this)
val walletFile = File(applicationInfo.dataDir, Constants.WALLET_NAME) val walletFile = File(applicationInfo.dataDir, Constants.WALLET_NAME)
val walletKeysFile = File(applicationInfo.dataDir, Constants.WALLET_NAME + ".keys") val walletKeysFile = File(applicationInfo.dataDir, Constants.WALLET_NAME + ".keys")
if (walletKeysFile.exists()) { if (walletKeysFile.exists()) {
@ -87,7 +88,6 @@ class MainActivity : AppCompatActivity(), MoneroHandlerThread.Listener, Password
historyService = HistoryService(thread) historyService = HistoryService(thread)
blockchainService = BlockchainService(thread) blockchainService = BlockchainService(thread)
daemonService = DaemonService(thread) daemonService = DaemonService(thread)
proxyService = ProxyService(thread)
utxoService = UTXOService(thread) utxoService = UTXOService(thread)
thread.start() thread.start()
} }

View file

@ -10,6 +10,7 @@ import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
@ -30,6 +31,8 @@ import net.mynero.wallet.service.DaemonService
import net.mynero.wallet.service.HistoryService import net.mynero.wallet.service.HistoryService
import net.mynero.wallet.service.PrefService import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.service.SamouraiTorManager
import net.mynero.wallet.service.TorKmpManager
import net.mynero.wallet.util.Constants import net.mynero.wallet.util.Constants
import timber.log.Timber import timber.log.Timber

View file

@ -18,8 +18,12 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.SwitchCompat import androidx.appcompat.widget.SwitchCompat
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.mynero.wallet.MoneroApplication import net.mynero.wallet.MoneroApplication
import net.mynero.wallet.R import net.mynero.wallet.R
import net.mynero.wallet.data.Node import net.mynero.wallet.data.Node
@ -35,26 +39,6 @@ import net.mynero.wallet.util.Constants
class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListener { class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListener {
private var useOffset = true private var useOffset = true
private var mViewModel: OnboardingViewModel? = null private var mViewModel: OnboardingViewModel? = null
private var proxyAddressListener: TextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (mViewModel != null) {
mViewModel?.setProxyAddress(editable.toString())
mViewModel?.updateProxy(activity?.application as MoneroApplication)
}
}
}
private var proxyPortListener: TextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (mViewModel != null) {
mViewModel?.setProxyPort(editable.toString())
mViewModel?.updateProxy(activity?.application as MoneroApplication)
}
}
}
private var walletProxyAddressEditText: EditText? = null private var walletProxyAddressEditText: EditText? = null
private var walletProxyPortEditText: EditText? = null private var walletProxyPortEditText: EditText? = null
private var walletPasswordEditText: EditText? = null private var walletPasswordEditText: EditText? = null
@ -113,7 +97,7 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
advancedOptionsLayout?.visibility = View.GONE advancedOptionsLayout?.visibility = View.GONE
} }
} }
mViewModel?.enableCreateButton?.observe(viewLifecycleOwner) { enable: Boolean -> mViewModel?.enableButton?.observe(viewLifecycleOwner) { enable: Boolean ->
createWalletButton?.isEnabled = enable createWalletButton?.isEnabled = enable
} }
mViewModel?.seedType?.observe(viewLifecycleOwner) { seedType: SeedType -> mViewModel?.seedType?.observe(viewLifecycleOwner) { seedType: SeedType ->
@ -131,9 +115,27 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
walletSeedEditText?.hint = getString(R.string.recovery_phrase_optional_polyseed) walletSeedEditText?.hint = getString(R.string.recovery_phrase_optional_polyseed)
} }
} }
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
samouraiTorManager?.getTorStateLiveData()?.observeForever {
println("STATE CHANGE:: ${it.state.name}")
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if(socketAddress.toString().isEmpty()) return@let
println("PROXY INIT")
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)
}
}
}
} }
private fun bindListeners() { private fun bindListeners() {
val useBundledTor = view?.findViewById<CheckBox>(R.id.bundled_tor_checkbox)
seedOffsetCheckbox?.isChecked = useOffset seedOffsetCheckbox?.isChecked = useOffset
// Disable onBack click // Disable onBack click
val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
@ -147,12 +149,9 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
useOffset = b useOffset = b
} }
createWalletButton?.setOnClickListener { createWalletButton?.setOnClickListener {
prepareDefaultNode()
onBackPressedCallback.isEnabled = false onBackPressedCallback.isEnabled = false
(getActivity()?.application as MoneroApplication).executor?.execute { (getActivity()?.application as MoneroApplication).executor?.execute {
createOrImportWallet( createOrImportWallet(
walletPasswordEditText?.text.toString(),
walletPasswordConfirmEditText?.text.toString(),
walletSeedEditText?.text.toString().trim { it <= ' ' }, walletSeedEditText?.text.toString().trim { it <= ' ' },
walletRestoreHeightEditText?.text.toString().trim { it <= ' ' } walletRestoreHeightEditText?.text.toString().trim { it <= ' ' }
) )
@ -163,6 +162,7 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) { override fun afterTextChanged(editable: Editable) {
val text = editable.toString() val text = editable.toString()
mViewModel?.setPassphrase(text)
if (text.isEmpty()) { if (text.isEmpty()) {
walletPasswordConfirmEditText?.text = null walletPasswordConfirmEditText?.text = null
walletPasswordConfirmEditText?.visibility = View.GONE walletPasswordConfirmEditText?.visibility = View.GONE
@ -171,6 +171,15 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
} }
} }
}) })
walletPasswordConfirmEditText?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
mViewModel?.setConfirmedPassphrase(text)
}
})
walletSeedEditText?.addTextChangedListener(object : TextWatcher { walletSeedEditText?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
@ -185,26 +194,35 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
}) })
seedTypeButton?.setOnClickListener { toggleSeedType() } seedTypeButton?.setOnClickListener { toggleSeedType() }
torSwitch?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean -> torSwitch?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_TOR, b)?.apply()
removeProxyTextListeners()
if (b) { if (b) {
if (ProxyService.instance?.hasProxySet() == true) { useBundledTor?.visibility = View.VISIBLE
val proxyAddress = walletProxyAddressEditText?.visibility = View.VISIBLE
ProxyService.instance?.proxyAddress ?: return@setOnCheckedChangeListener walletProxyPortEditText?.visibility = View.VISIBLE
val proxyPort =
ProxyService.instance?.proxyPort ?: return@setOnCheckedChangeListener
initProxyStuff(proxyAddress, proxyPort)
} else { } else {
initProxyStuff("127.0.0.1", "9050") useBundledTor?.visibility = View.GONE
walletProxyAddressEditText?.visibility = View.GONE
walletProxyPortEditText?.visibility = View.GONE
} }
addProxyTextListeners() mViewModel?.setUseProxy(b)
} }
mViewModel?.updateProxy(getActivity()?.application as MoneroApplication) walletProxyPortEditText?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
mViewModel?.setProxyPort(text)
} }
showXmrchanSwitch?.isChecked = })
PrefService.instance?.getBoolean(Constants.PREF_MONEROCHAN, true) == true walletProxyAddressEditText?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
mViewModel?.setProxyAddress(text)
}
})
showXmrchanSwitch?.isChecked = true
showXmrchanSwitch?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean -> showXmrchanSwitch?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PrefService.instance?.edit()?.putBoolean(Constants.PREF_MONEROCHAN, b)?.apply()
if (b) { if (b) {
xmrchanOnboardingImage?.visibility = View.VISIBLE xmrchanOnboardingImage?.visibility = View.VISIBLE
} else { } else {
@ -220,6 +238,21 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
dialog.show(fragmentManager, "node_selection_dialog") dialog.show(fragmentManager, "node_selection_dialog")
} }
} }
useBundledTor?.setOnCheckedChangeListener { _, isChecked ->
walletProxyPortEditText?.visibility = if (isChecked) View.GONE else View.VISIBLE
walletProxyAddressEditText?.visibility = if (isChecked) View.GONE else View.VISIBLE
if(isChecked) {
ProxyService.instance?.samouraiTorManager?.start()
} else {
ProxyService.instance?.samouraiTorManager?.stop()
mViewModel?.setProxyAddress("")
mViewModel?.setProxyPort("")
}
mViewModel?.setUseBundledTor(isChecked)
}
} }
private fun toggleSeedType() { private fun toggleSeedType() {
@ -233,47 +266,21 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
mViewModel?.setSeedType(newSeedType) mViewModel?.setSeedType(newSeedType)
} }
private fun prepareDefaultNode() {
PrefService.instance?.node
}
private fun createOrImportWallet( private fun createOrImportWallet(
walletPassword: String,
confirmedPassword: String,
walletSeed: String, walletSeed: String,
restoreHeightText: String restoreHeightText: String
) { ) {
val activity: Activity? = activity val activity: Activity? = activity
if (activity != null) { if (activity != null) {
lifecycleScope.launch(Dispatchers.IO) {
mViewModel?.createOrImportWallet( mViewModel?.createOrImportWallet(
activity, activity,
walletPassword,
confirmedPassword,
walletSeed, walletSeed,
restoreHeightText, restoreHeightText,
useOffset useOffset
) )
} }
} }
private fun removeProxyTextListeners() {
walletProxyAddressEditText?.removeTextChangedListener(proxyAddressListener)
walletProxyPortEditText?.removeTextChangedListener(proxyPortListener)
}
private fun addProxyTextListeners() {
walletProxyAddressEditText?.addTextChangedListener(proxyAddressListener)
walletProxyPortEditText?.addTextChangedListener(proxyPortListener)
}
private fun initProxyStuff(proxyAddress: String, proxyPort: String) {
val validIpAddress = Patterns.IP_ADDRESS.matcher(proxyAddress).matches()
if (validIpAddress) {
mViewModel?.setProxyAddress(proxyAddress)
mViewModel?.setProxyPort(proxyPort)
walletProxyAddressEditText?.setText(proxyAddress)
walletProxyPortEditText?.setText(proxyPort)
}
} }
override fun onNodeSelected() { override fun onNodeSelected() {
@ -284,7 +291,7 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
getString(R.string.node_selected), getString(R.string.node_selected),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
mViewModel?.updateProxy(activity?.application as MoneroApplication) refreshProxy()
} }
override fun onClickedEditNode(node: Node?) {} override fun onClickedEditNode(node: Node?) {}
@ -303,4 +310,10 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
dialog.show(fragmentManager, "node_selection_dialog") dialog.show(fragmentManager, "node_selection_dialog")
} }
} }
private fun refreshProxy() {
val proxyAddress = walletProxyAddressEditText?.text.toString()
val proxyPort = walletProxyPortEditText?.text.toString()
ProxyService.instance?.updateProxy(proxyAddress, proxyPort)
}
} }

View file

@ -9,9 +9,11 @@ import androidx.lifecycle.ViewModel
import net.mynero.wallet.MainActivity import net.mynero.wallet.MainActivity
import net.mynero.wallet.MoneroApplication import net.mynero.wallet.MoneroApplication
import net.mynero.wallet.R import net.mynero.wallet.R
import net.mynero.wallet.livedata.combineLatestIgnoreNull
import net.mynero.wallet.model.Wallet import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.WalletManager import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.PrefService import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.Constants import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.RestoreHeight import net.mynero.wallet.util.RestoreHeight
import java.io.File import java.io.File
@ -19,62 +21,65 @@ import java.util.Calendar
class OnboardingViewModel : ViewModel() { class OnboardingViewModel : ViewModel() {
private val _showMoreOptions = MutableLiveData(false) private val _showMoreOptions = MutableLiveData(false)
private val _enableCreateButton = MutableLiveData(true) private val _creatingWallet = MutableLiveData(false)
private val _seedType = MutableLiveData(SeedType.POLYSEED) private val _seedType = MutableLiveData(SeedType.POLYSEED)
private val _useProxy = MutableLiveData(false)
val useProxy: LiveData<Boolean> = _useProxy
private val _proxyAddress = MutableLiveData("")
private val _proxyPort = MutableLiveData("")
private val _useBundledTor = MutableLiveData(false)
val useBundledTor: LiveData<Boolean> = _useBundledTor
private val _passphrase = MutableLiveData("")
private val _confirmedPassphrase = MutableLiveData("")
var showMoreOptions: LiveData<Boolean> = _showMoreOptions var showMoreOptions: LiveData<Boolean> = _showMoreOptions
var enableCreateButton: LiveData<Boolean> = _enableCreateButton
var seedType: LiveData<SeedType> = _seedType var seedType: LiveData<SeedType> = _seedType
private var proxyAddress = ""
private var proxyPort = "" val enableButton = combineLatestIgnoreNull(
seedType,
_useProxy,
_proxyAddress,
_proxyPort,
_useBundledTor,
_passphrase,
_confirmedPassphrase,
_creatingWallet
) { seedType, useProxy, proxyAddress, proxyPort, useBundledTor, passphrase, confirmedPassphrase, creatingWallet ->
if(seedType == null || useProxy == null || proxyAddress == null || proxyPort == null || useBundledTor == null || passphrase == null || confirmedPassphrase == null || creatingWallet == null) return@combineLatestIgnoreNull false
if((passphrase.isNotEmpty() || confirmedPassphrase.isNotEmpty()) && passphrase != confirmedPassphrase) return@combineLatestIgnoreNull false
if(creatingWallet) return@combineLatestIgnoreNull false
if(seedType == SeedType.POLYSEED && passphrase.isEmpty()) return@combineLatestIgnoreNull false
if(useProxy && (proxyAddress.isEmpty() || proxyPort.isEmpty())) return@combineLatestIgnoreNull false
if(useBundledTor && (proxyAddress.isEmpty() || proxyPort.isEmpty())) return@combineLatestIgnoreNull false
return@combineLatestIgnoreNull true
}
fun onMoreOptionsClicked() { fun onMoreOptionsClicked() {
val currentValue = showMoreOptions.value ?: false val currentValue = showMoreOptions.value ?: false
val newValue = !currentValue val newValue = !currentValue
_showMoreOptions.value = newValue _showMoreOptions.value = newValue
} }
fun updateProxy(application: MoneroApplication) {
application.executor?.execute {
val usesProxy = PrefService.instance?.getBoolean(Constants.PREF_USES_TOR, false) == true
if (!usesProxy) {
return@execute
}
if (proxyAddress.isEmpty()) proxyAddress = "127.0.0.1"
if (proxyPort.isEmpty()) proxyPort = "9050"
val validIpAddress = Patterns.IP_ADDRESS.matcher(proxyAddress).matches()
if (validIpAddress) {
val proxy = "$proxyAddress:$proxyPort"
PrefService.instance?.edit()?.putString(Constants.PREF_PROXY, proxy)?.apply()
}
}
}
fun setSeedType(seedType: SeedType?) { fun setSeedType(seedType: SeedType?) {
_seedType.value = seedType _seedType.value = seedType
} }
fun setProxyAddress(address: String) {
proxyAddress = address
}
fun setProxyPort(port: String) {
proxyPort = port
}
fun createOrImportWallet( fun createOrImportWallet(
mainActivity: Activity, mainActivity: Activity,
walletPassword: String,
confirmedPassword: String,
walletSeed: String, walletSeed: String,
restoreHeightText: String, restoreHeightText: String,
useOffset: Boolean useOffset: Boolean
) { ) {
val passphrase = _passphrase.value ?: return
val confirmedPassphrase = _confirmedPassphrase.value ?: return
val useProxy = _useProxy.value ?: return
val application = mainActivity.application as MoneroApplication val application = mainActivity.application as MoneroApplication
application.executor?.execute { _creatingWallet.postValue(true)
_enableCreateButton.postValue(false) val offset = if (useOffset) confirmedPassphrase else ""
val offset = if (useOffset) walletPassword else "" if (passphrase.isNotEmpty()) {
if (walletPassword.isNotEmpty()) { if (passphrase != confirmedPassphrase) {
if (walletPassword != confirmedPassword) { _creatingWallet.postValue(false)
_enableCreateButton.postValue(true)
mainActivity.runOnUiThread { mainActivity.runOnUiThread {
Toast.makeText( Toast.makeText(
mainActivity, mainActivity,
@ -82,7 +87,7 @@ class OnboardingViewModel : ViewModel() {
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
return@execute return
} }
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_PASSWORD, true) PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_PASSWORD, true)
?.apply() ?.apply()
@ -93,23 +98,23 @@ class OnboardingViewModel : ViewModel() {
if (offset.isNotEmpty()) { if (offset.isNotEmpty()) {
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_OFFSET, true)?.apply() PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_OFFSET, true)?.apply()
} }
val seedTypeValue = seedType.value ?: return@execute val seedTypeValue = seedType.value ?: return
if (walletSeed.isEmpty()) { if (walletSeed.isEmpty()) {
if (seedTypeValue == SeedType.POLYSEED) { if (seedTypeValue == SeedType.POLYSEED) {
wallet = if (offset.isEmpty()) { wallet = if (offset.isEmpty()) {
mainActivity.runOnUiThread { mainActivity.runOnUiThread {
_enableCreateButton.postValue(true) _creatingWallet.postValue(false)
Toast.makeText( Toast.makeText(
mainActivity, mainActivity,
application.getString(R.string.invalid_empty_passphrase), application.getString(R.string.invalid_empty_passphrase),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
return@execute return
} else { } else {
WalletManager.instance?.createWalletPolyseed( WalletManager.instance?.createWalletPolyseed(
walletFile, walletFile,
walletPassword, passphrase,
offset, offset,
Constants.MNEMONIC_LANGUAGE Constants.MNEMONIC_LANGUAGE
) )
@ -122,7 +127,7 @@ class OnboardingViewModel : ViewModel() {
tmpWallet?.let { tmpWallet?.let {
wallet = WalletManager.instance?.recoveryWallet( wallet = WalletManager.instance?.recoveryWallet(
walletFile, walletFile,
walletPassword, passphrase,
tmpWallet.getSeed("") ?: return@let, tmpWallet.getSeed("") ?: return@let,
offset, offset,
restoreHeight restoreHeight
@ -133,14 +138,14 @@ class OnboardingViewModel : ViewModel() {
} else { } else {
if (getMnemonicType(walletSeed) == SeedType.UNKNOWN) { if (getMnemonicType(walletSeed) == SeedType.UNKNOWN) {
mainActivity.runOnUiThread { mainActivity.runOnUiThread {
_enableCreateButton.postValue(true) _creatingWallet.postValue(false)
Toast.makeText( Toast.makeText(
mainActivity, mainActivity,
application.getString(R.string.invalid_mnemonic_code), application.getString(R.string.invalid_mnemonic_code),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
return@execute return
} }
if (restoreHeightText.isNotEmpty()) { if (restoreHeightText.isNotEmpty()) {
restoreHeight = restoreHeightText.toLong() restoreHeight = restoreHeightText.toLong()
@ -148,14 +153,14 @@ class OnboardingViewModel : ViewModel() {
if (seedTypeValue == SeedType.POLYSEED) { if (seedTypeValue == SeedType.POLYSEED) {
wallet = WalletManager.instance?.recoveryWalletPolyseed( wallet = WalletManager.instance?.recoveryWalletPolyseed(
walletFile, walletFile,
walletPassword, passphrase,
walletSeed, walletSeed,
offset offset
) )
} else if (seedTypeValue == SeedType.LEGACY) { } else if (seedTypeValue == SeedType.LEGACY) {
wallet = WalletManager.instance?.recoveryWallet( wallet = WalletManager.instance?.recoveryWallet(
walletFile, walletFile,
walletPassword, passphrase,
walletSeed, walletSeed,
offset, offset,
restoreHeight restoreHeight
@ -167,11 +172,20 @@ class OnboardingViewModel : ViewModel() {
val ok = walletStatus?.isOk val ok = walletStatus?.isOk
walletFile.delete() // cache is broken for some reason when recovering wallets. delete the file here. this happens in monerujo too. walletFile.delete() // cache is broken for some reason when recovering wallets. delete the file here. this happens in monerujo too.
if (ok == true) { if (ok == true) {
(mainActivity as MainActivity).init(walletFile, walletPassword) var editor = PrefService.instance?.edit()
?.putBoolean(Constants.PREF_USE_BUNDLED_TOR, _useBundledTor.value == true)
?.putBoolean(Constants.PREF_USES_PROXY, useProxy)
if(useProxy) {
editor = editor?.putString(Constants.PREF_PROXY, "${_proxyAddress.value}:${_proxyPort.value}")
}
editor?.apply()
(mainActivity as MainActivity).init(walletFile, passphrase)
mainActivity.runOnUiThread { mainActivity.onBackPressed() } mainActivity.runOnUiThread { mainActivity.onBackPressed() }
} else { } else {
mainActivity.runOnUiThread { mainActivity.runOnUiThread {
_enableCreateButton.postValue(true) _creatingWallet.postValue(false)
Toast.makeText( Toast.makeText(
mainActivity, mainActivity,
application.getString( application.getString(
@ -183,7 +197,6 @@ class OnboardingViewModel : ViewModel() {
} }
} }
} }
}
private val newRestoreHeight: Long private val newRestoreHeight: Long
get() { get() {
@ -213,6 +226,30 @@ class OnboardingViewModel : ViewModel() {
) )
} }
fun setProxyAddress(address: String) {
_proxyAddress.value = address
}
fun setProxyPort(port: String) {
_proxyPort.value = port
}
fun setUseBundledTor(useBundled: Boolean) {
_useBundledTor.value = useBundled
}
fun setUseProxy(useProxy: Boolean) {
_useProxy.value = useProxy
}
fun setPassphrase(passphrase: String) {
_passphrase.value = passphrase
}
fun setConfirmedPassphrase(confirmedPassphrase: String) {
_confirmedPassphrase.value = confirmedPassphrase
}
enum class SeedType(val descResId: Int) { enum class SeedType(val descResId: Int) {
LEGACY(R.string.seed_desc_legacy), POLYSEED(R.string.seed_desc_polyseed), UNKNOWN(0) LEGACY(R.string.seed_desc_legacy), POLYSEED(R.string.seed_desc_polyseed), UNKNOWN(0)

View file

@ -6,21 +6,17 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.CheckBox
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.EditText import android.widget.EditText
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.SwitchCompat import androidx.appcompat.widget.SwitchCompat
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.mynero.wallet.R import net.mynero.wallet.R
import net.mynero.wallet.data.Node import net.mynero.wallet.data.Node
import net.mynero.wallet.data.Node.Companion.fromJson import net.mynero.wallet.data.Node.Companion.fromJson
@ -33,17 +29,12 @@ import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog.NodeSele
import net.mynero.wallet.fragment.dialog.PasswordBottomSheetDialog import net.mynero.wallet.fragment.dialog.PasswordBottomSheetDialog
import net.mynero.wallet.fragment.dialog.PasswordBottomSheetDialog.PasswordListener import net.mynero.wallet.fragment.dialog.PasswordBottomSheetDialog.PasswordListener
import net.mynero.wallet.fragment.dialog.WalletKeysBottomSheetDialog import net.mynero.wallet.fragment.dialog.WalletKeysBottomSheetDialog
import net.mynero.wallet.model.Wallet.ConnectionStatus
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.BalanceService import net.mynero.wallet.service.BalanceService
import net.mynero.wallet.service.BlockchainService
import net.mynero.wallet.service.DaemonService
import net.mynero.wallet.service.HistoryService import net.mynero.wallet.service.HistoryService
import net.mynero.wallet.service.PrefService import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.Constants import net.mynero.wallet.util.Constants
import org.json.JSONArray import org.json.JSONArray
import timber.log.Timber
class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListener, AddNodeListener, class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListener, AddNodeListener,
EditNodeListener { EditNodeListener {
@ -75,6 +66,12 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
view.findViewById<ConstraintLayout>(R.id.wallet_proxy_settings_layout) view.findViewById<ConstraintLayout>(R.id.wallet_proxy_settings_layout)
walletProxyAddressEditText = view.findViewById(R.id.wallet_proxy_address_edittext) walletProxyAddressEditText = view.findViewById(R.id.wallet_proxy_address_edittext)
walletProxyPortEditText = view.findViewById(R.id.wallet_proxy_port_edittext) walletProxyPortEditText = view.findViewById(R.id.wallet_proxy_port_edittext)
val useBundledTor = view.findViewById<CheckBox>(R.id.bundled_tor_checkbox)
useBundledTor.isChecked = ProxyService.instance?.useBundledTor == true
walletProxyPortEditText?.visibility = if (useBundledTor.isChecked) View.GONE else View.VISIBLE
walletProxyAddressEditText?.visibility = if (useBundledTor.isChecked) View.GONE else View.VISIBLE
streetModeSwitch.isChecked = streetModeSwitch.isChecked =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
streetModeSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean -> streetModeSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
@ -93,7 +90,7 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
PrefService.instance?.edit()?.putBoolean(Constants.PREF_DONATE_PER_TX, b)?.apply() PrefService.instance?.edit()?.putBoolean(Constants.PREF_DONATE_PER_TX, b)?.apply()
} }
val prefService = PrefService.instance ?: return val prefService = PrefService.instance ?: return
val usesProxy = prefService.getBoolean(Constants.PREF_USES_TOR, false) val usesProxy = ProxyService.instance?.usingProxy == true
cachedProxyAddress = ProxyService.instance?.proxyAddress ?: return cachedProxyAddress = ProxyService.instance?.proxyAddress ?: return
cachedProxyPort = ProxyService.instance?.proxyPort ?: return cachedProxyPort = ProxyService.instance?.proxyPort ?: return
if (ProxyService.instance?.hasProxySet() == true) { if (ProxyService.instance?.hasProxySet() == true) {
@ -106,7 +103,7 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
proxySettingsLayout.visibility = View.GONE proxySettingsLayout.visibility = View.GONE
} }
torSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean -> torSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
prefService.edit()?.putBoolean(Constants.PREF_USES_TOR, b)?.apply() prefService.edit()?.putBoolean(Constants.PREF_USES_PROXY, b)?.apply()
if (b) { if (b) {
if (ProxyService.instance?.hasProxySet() == true) { if (ProxyService.instance?.hasProxySet() == true) {
val proxyAddress = ProxyService.instance?.proxyAddress ?: return@setOnCheckedChangeListener val proxyAddress = ProxyService.instance?.proxyAddress ?: return@setOnCheckedChangeListener
@ -144,6 +141,18 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
} }
} }
useBundledTor?.setOnCheckedChangeListener { _, isChecked ->
walletProxyPortEditText?.visibility = if (isChecked) View.GONE else View.VISIBLE
walletProxyAddressEditText?.visibility = if (isChecked) View.GONE else View.VISIBLE
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USE_BUNDLED_TOR, isChecked)?.apply()
if(isChecked) {
ProxyService.instance?.samouraiTorManager?.start()
} else {
ProxyService.instance?.samouraiTorManager?.stop()
}
}
val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
refreshProxy() refreshProxy()

View file

@ -0,0 +1,414 @@
package net.mynero.wallet.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
fun <T1, T2, S> combineLatest(
source1: LiveData<T1>,
source2: LiveData<T2>,
func: (T1?, T2?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
result.value = func.invoke(source1.value, source2.value)
}
result.addSource(source2) {
result.value = func.invoke(source1.value, source2.value)
}
return result
}
fun <T1, T2, T3, S> combineLatest(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
func: (T1?, T2?, T3?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
result.value = func.invoke(source1.value, source2.value, source3.value)
}
result.addSource(source2) {
result.value = func.invoke(source1.value, source2.value, source3.value)
}
result.addSource(source3) {
result.value = func.invoke(source1.value, source2.value, source3.value)
}
return result
}
fun <T1, T2, T3, T4, S> combineLatest(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
source4: LiveData<T4>,
func: (T1?, T2?, T3?, T4?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
result.value = func.invoke(source1.value, source2.value, source3.value, source4.value)
}
result.addSource(source2) {
result.value = func.invoke(source1.value, source2.value, source3.value, source4.value)
}
result.addSource(source3) {
result.value = func.invoke(source1.value, source2.value, source3.value, source4.value)
}
result.addSource(source4) {
result.value = func.invoke(source1.value, source2.value, source3.value, source4.value)
}
return result
}
fun <T1, T2, T3, T4, T5, S> combineLatest(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
source4: LiveData<T4>,
source5: LiveData<T5>,
func: (T1?, T2?, T3?, T4?, T5?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
result.value = func.invoke(source1.value, source2.value, source3.value, source4.value, source5.value)
}
result.addSource(source2) {
result.value = func.invoke(source1.value, source2.value, source3.value, source4.value, source5.value)
}
result.addSource(source3) {
result.value = func.invoke(source1.value, source2.value, source3.value, source4.value, source5.value)
}
result.addSource(source4) {
result.value = func.invoke(source1.value, source2.value, source3.value, source4.value, source5.value)
}
result.addSource(source5) {
result.value = func.invoke(source1.value, source2.value, source3.value, source4.value, source5.value)
}
return result
}
fun <T1, T2, S> combineLatestIgnoreNull(
source1: LiveData<T1>,
source2: LiveData<T2>,
func: (T1?, T2?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
func.invoke(source1.value, source2.value)?.run { result.value = this }
}
result.addSource(source2) {
func.invoke(source1.value, source2.value)?.run { result.value = this }
}
return result
}
fun <T1, T2, T3, S> combineLatestIgnoreNull(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
func: (T1?, T2?, T3?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
func(source1.value, source2.value, source3.value)?.run { result.value = this }
}
result.addSource(source2) {
func(source1.value, source2.value, source3.value)?.run { result.value = this }
}
result.addSource(source3) {
func(source1.value, source2.value, source3.value)?.run { result.value = this }
}
return result
}
fun <T1, T2, T3, T4, S> combineLatestIgnoreNull(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
source4: LiveData<T4>,
func: (T1?, T2?, T3?, T4?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
func(source1.value, source2.value, source3.value, source4.value)?.run { result.value = this }
}
result.addSource(source2) {
func(source1.value, source2.value, source3.value, source4.value)?.run { result.value = this }
}
result.addSource(source3) {
func(source1.value, source2.value, source3.value, source4.value)?.run { result.value = this }
}
result.addSource(source4) {
func(source1.value, source2.value, source3.value, source4.value)?.run { result.value = this }
}
return result
}
fun <T1, T2, T3, T4, T5, S> combineLatestIgnoreNull(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
source4: LiveData<T4>,
source5: LiveData<T5>,
func: (T1?, T2?, T3?, T4?, T5?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value
)?.run { result.value = this }
}
result.addSource(source2) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value
)?.run { result.value = this }
}
result.addSource(source3) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value
)?.run { result.value = this }
}
result.addSource(source4) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value
)?.run { result.value = this }
}
result.addSource(source5) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value
)?.run { result.value = this }
}
return result
}
fun <T1, T2, T3, T4, T5, T6, S> combineLatestIgnoreNull(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
source4: LiveData<T4>,
source5: LiveData<T5>,
source6: LiveData<T6>,
func: (T1?, T2?, T3?, T4?, T5?, T6?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value
)?.run { result.value = this }
}
result.addSource(source2) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value
)?.run { result.value = this }
}
result.addSource(source3) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value
)?.run { result.value = this }
}
result.addSource(source4) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value
)?.run { result.value = this }
}
result.addSource(source5) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value
)?.run { result.value = this }
}
result.addSource(source6) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value
)?.run { result.value = this }
}
return result
}
fun <T1, T2, T3, T4, T5, T6, T7, T8, S> combineLatestIgnoreNull(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
source4: LiveData<T4>,
source5: LiveData<T5>,
source6: LiveData<T6>,
source7: LiveData<T7>,
source8: LiveData<T8>,
func: (T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value
)?.run { result.value = this }
}
result.addSource(source2) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value
)?.run { result.value = this }
}
result.addSource(source3) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value
)?.run { result.value = this }
}
result.addSource(source4) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value
)?.run { result.value = this }
}
result.addSource(source5) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value
)?.run { result.value = this }
}
result.addSource(source6) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value
)?.run { result.value = this }
}
result.addSource(source7) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value
)?.run { result.value = this }
}
result.addSource(source8) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value
)?.run { result.value = this }
}
return result
}

View file

@ -0,0 +1,8 @@
package net.mynero.wallet.model
enum class EnumTorState {
STARTING,
ON,
STOPPING,
OFF
}

View file

@ -0,0 +1,14 @@
package net.mynero.wallet.model
class TorState {
var state : EnumTorState = EnumTorState.OFF
get() = field
set(value) {
field = value
}
var progressIndicator : Int = 0
get() = field
set(value) {
field = value
}
}

View file

@ -249,7 +249,10 @@ class Wallet {
isSynchronized = true isSynchronized = true
} }
external fun startRefresh() fun startRefresh() {
startRefreshJ()
}
private external fun startRefreshJ()
external fun pauseRefresh() external fun pauseRefresh()
external fun refresh(): Boolean external fun refresh(): Boolean
external fun refreshAsync() external fun refreshAsync()

View file

@ -51,7 +51,7 @@ class MoneroHandlerThread(name: String, val listener: Listener?, wallet: Wallet)
override fun run() { override fun run() {
val prefService = PrefService.instance ?: return val prefService = PrefService.instance ?: return
val usesTor = prefService.getBoolean(Constants.PREF_USES_TOR, false) val usesTor = ProxyService.instance?.usingProxy == true
val currentNode = prefService.node val currentNode = prefService.node
val isLocalIp = val isLocalIp =
currentNode?.address?.startsWith("10.") == true || currentNode?.address?.startsWith("10.") == true ||

View file

@ -26,7 +26,7 @@ class PrefService(application: MoneroApplication) : ServiceBase(null) {
val node: Node? val node: Node?
get() { get() {
val usesProxy = getBoolean(Constants.PREF_USES_TOR, false) val usesProxy = ProxyService.instance?.usingProxy == true
var defaultNode = DefaultNodes.SAMOURAI var defaultNode = DefaultNodes.SAMOURAI
if (usesProxy) { if (usesProxy) {
val proxyPort = ProxyService.instance?.proxyPort val proxyPort = ProxyService.instance?.proxyPort

View file

@ -1,29 +1,48 @@
package net.mynero.wallet.service package net.mynero.wallet.service
import android.app.Application
import android.util.Patterns import android.util.Patterns
import androidx.lifecycle.LiveData import net.mynero.wallet.MainActivity
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.data.Node
import net.mynero.wallet.livedata.SingleLiveEvent import net.mynero.wallet.livedata.SingleLiveEvent
import net.mynero.wallet.model.WalletManager import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Constants import net.mynero.wallet.util.Constants
class ProxyService(thread: MoneroHandlerThread) : ServiceBase(thread) { class ProxyService(activity: MainActivity) : ServiceBase(null) {
val proxyChangeEvents: SingleLiveEvent<String> = SingleLiveEvent() val proxyChangeEvents: SingleLiveEvent<String> = SingleLiveEvent()
var samouraiTorManager: SamouraiTorManager? = null
init { init {
samouraiTorManager = SamouraiTorManager(activity.application, TorKmpManager(activity.application))
instance = this instance = this
activity.runOnUiThread {
samouraiTorManager?.getTorStateLiveData()?.observeForever {
println("STATE CHANGE:: ${it.state.name}")
samouraiTorManager?.getProxy()?.address()?.let { socketAddress ->
if(socketAddress.toString().isEmpty()) return@let
println("PROXY INIT")
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()
}
}
} }
fun updateProxy(proxyAddress: String, proxyPort: String) { fun updateProxy(proxyAddress: String, proxyPort: String) {
var finalProxyAddress = proxyAddress var finalProxyAddress = proxyAddress
var finalProxyPort = proxyPort var finalProxyPort = proxyPort
val usesProxy = PrefService.instance?.getBoolean(Constants.PREF_USES_TOR, false) == true
val curretNode = PrefService.instance?.node val curretNode = PrefService.instance?.node
val isNodeLocalIp = val isNodeLocalIp =
curretNode?.address?.startsWith("10.") == true || curretNode?.address?.startsWith("192.168.") == true || curretNode?.address == "localhost" || curretNode?.address == "127.0.0.1" curretNode?.address?.startsWith("10.") == true || curretNode?.address?.startsWith("192.168.") == true || curretNode?.address == "localhost" || curretNode?.address == "127.0.0.1"
curretNode?.trusted?.let { WalletManager.instance?.wallet?.setTrustedDaemon(it) } curretNode?.trusted?.let { WalletManager.instance?.wallet?.setTrustedDaemon(it) }
if (!usesProxy || isNodeLocalIp) { if (!usingProxy || isNodeLocalIp) {
// User is not using proxy, or is using local node currently, so we will disable proxy here. // User is not using proxy, or is using local node currently, so we will disable proxy here.
proxyChangeEvents.postValue("") proxyChangeEvents.postValue("")
return return
@ -44,6 +63,12 @@ class ProxyService(thread: MoneroHandlerThread) : ServiceBase(thread) {
return proxyString?.contains(":") == true return proxyString?.contains(":") == true
} }
val useBundledTor: Boolean
get() = PrefService.instance?.getBoolean(Constants.PREF_USE_BUNDLED_TOR, false) == true
val usingProxy: Boolean
get() = PrefService.instance?.getBoolean(Constants.PREF_USES_PROXY, false) == true
val proxy: String? val proxy: String?
get() = PrefService.instance?.getString(Constants.PREF_PROXY, "") get() = PrefService.instance?.getString(Constants.PREF_PROXY, "")

View file

@ -0,0 +1,47 @@
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?) {
fun getTorStateLiveData(): MutableLiveData<TorState> {
return torKmpManager!!.torStateLiveData
}
fun getTorState(): TorState {
return torKmpManager!!.torState
}
fun isConnected(): Boolean {
return torKmpManager?.isConnected() ?: false
}
fun isStarting(): Boolean {
return torKmpManager?.isStarting() ?: false
}
fun stop() {
torKmpManager?.torOperationManager?.stopQuietly()
}
fun start() {
torKmpManager?.torOperationManager?.startQuietly()
}
fun getProxy(): Proxy? {
return torKmpManager?.proxy
}
fun newIdentity() {
torKmpManager?.newIdentity(appContext!!)
}
companion object {
private const val TAG = "SamouraiTorManager"
}
}

View file

@ -0,0 +1,357 @@
package net.mynero.wallet.service
import android.app.Application
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.TorState
import io.matthewnelson.kmp.tor.KmpTorLoaderAndroid
import io.matthewnelson.kmp.tor.TorConfigProviderAndroid
import io.matthewnelson.kmp.tor.common.address.*
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Option.*
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Setting.*
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlInfoGet
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal
import io.matthewnelson.kmp.tor.controller.common.events.TorEvent
import io.matthewnelson.kmp.tor.manager.TorManager
import io.matthewnelson.kmp.tor.manager.TorServiceConfig
import io.matthewnelson.kmp.tor.manager.common.TorControlManager
import io.matthewnelson.kmp.tor.manager.common.TorOperationManager
import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent
import io.matthewnelson.kmp.tor.manager.common.state.isOff
import io.matthewnelson.kmp.tor.manager.common.state.isOn
import io.matthewnelson.kmp.tor.manager.common.state.isStarting
import io.matthewnelson.kmp.tor.manager.common.state.isStopping
import kotlinx.coroutines.*
import net.mynero.wallet.model.EnumTorState
import java.lang.Exception
import java.net.InetSocketAddress
import java.net.Proxy
class TorKmpManager(application : Application) {
private val TAG = "TorListener"
private val providerAndroid by lazy {
object : TorConfigProviderAndroid(context = application) {
override fun provide(): TorConfig {
return TorConfig.Builder {
// Set multiple ports for all of the things
val dns = Ports.Dns()
put(dns.set(AorDorPort.Value(PortProxy(9252))))
put(dns.set(AorDorPort.Value(PortProxy(9253))))
val socks = Ports.Socks()
put(socks.set(AorDorPort.Value(PortProxy(9254))))
put(socks.set(AorDorPort.Value(PortProxy(9255))))
val http = Ports.HttpTunnel()
put(http.set(AorDorPort.Value(PortProxy(9258))))
put(http.set(AorDorPort.Value(PortProxy(9259))))
val trans = Ports.Trans()
put(trans.set(AorDorPort.Value(PortProxy(9262))))
put(trans.set(AorDorPort.Value(PortProxy(9263))))
// If a port (9263) is already taken (by ^^^^ trans port above)
// this will take its place and "overwrite" the trans port entry
// because port 9263 is taken.
put(socks.set(AorDorPort.Value(PortProxy(9263))))
// Set Flags
socks.setFlags(setOf(
Ports.Socks.Flag.OnionTrafficOnly
)).setIsolationFlags(setOf(
Ports.IsolationFlag.IsolateClientAddr,
)).set(AorDorPort.Value(PortProxy(9264)))
put(socks)
// reset our socks object to defaults
socks.setDefault()
// Not necessary, as if ControlPort is missing it will be
// automatically added for you; but for demonstration purposes...
// put(Ports.Control().set(AorDorPort.Auto))
// Use a UnixSocket instead of TCP for the ControlPort.
//
// A unix domain socket will always be preferred on Android
// if neither Ports.Control or UnixSockets.Control are provided.
put(UnixSockets.Control().set(FileSystemFile(
workDir.builder {
// Put the file in the "data" directory
// so that we avoid any directory permission
// issues.
//
// Note that DataDirectory is automatically added
// for you if it is not present in your provided
// config. If you set a custom Path for it, you
// should use it here.
addSegment(DataDirectory.DEFAULT_NAME)
addSegment(UnixSockets.Control.DEFAULT_NAME)
}
)))
// Use a UnixSocket instead of TCP for the SocksPort.
put(UnixSockets.Socks().set(FileSystemFile(
workDir.builder {
// Put the file in the "data" directory
// so that we avoid any directory permission
// issues.
//
// Note that DataDirectory is automatically added
// for you if it is not present in your provided
// config. If you set a custom Path for it, you
// should use it here.
addSegment(DataDirectory.DEFAULT_NAME)
addSegment(UnixSockets.Socks.DEFAULT_NAME)
}
)))
// For Android, disabling & reducing connection padding is
// advisable to minimize mobile data usage.
put(ConnectionPadding().set(AorTorF.False))
put(ConnectionPaddingReduced().set(TorF.True))
// Tor default is 24h. Reducing to 10 min helps mitigate
// unnecessary mobile data usage.
put(DormantClientTimeout().set(Time.Minutes(10)))
// Tor defaults this setting to false which would mean if
// Tor goes dormant, the next time it is started it will still
// be in the dormant state and will not bootstrap until being
// set to "active". This ensures that if it is a fresh start,
// dormancy will be cancelled automatically.
put(DormantCanceledByStartup().set(TorF.True))
// If planning to use v3 Client Authentication in a persistent
// manner (where private keys are saved to disk via the "Persist"
// flag), this is needed to be set.
put(ClientOnionAuthDir().set(FileSystemDir(
workDir.builder { addSegment(ClientOnionAuthDir.DEFAULT_NAME) }
)))
val hsPath = workDir.builder {
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
addSegment("test_service")
}
// Add Hidden services
put(HiddenService()
.setPorts(ports = setOf(
// Use a unix domain socket to communicate via IPC instead of over TCP
HiddenService.UnixSocket(virtualPort = Port(80), targetUnixSocket = hsPath.builder {
addSegment(HiddenService.UnixSocket.DEFAULT_UNIX_SOCKET_NAME)
}),
))
.setMaxStreams(maxStreams = HiddenService.MaxStreams(value = 2))
.setMaxStreamsCloseCircuit(value = TorF.True)
.set(FileSystemDir(path = hsPath))
)
put(HiddenService()
.setPorts(ports = setOf(
HiddenService.Ports(virtualPort = Port(80), targetPort = Port(1030)), // http
HiddenService.Ports(virtualPort = Port(443), targetPort = Port(1030)) // https
))
.set(FileSystemDir(path =
workDir.builder {
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
addSegment("test_service_2")
}
))
)
}.build()
}
}
}
private val loaderAndroid by lazy {
KmpTorLoaderAndroid(provider = providerAndroid)
}
private val manager: TorManager by lazy {
TorManager.newInstance(application = application, loader = loaderAndroid, requiredEvents = null)
}
// only expose necessary interfaces
val torOperationManager: TorOperationManager get() = manager
val torControlManager: TorControlManager get() = manager
private val listener = TorListener()
val events: LiveData<String> get() = listener.eventLines
private val appScope by lazy {
CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
}
val torStateLiveData: MutableLiveData<TorState> = MutableLiveData()
get() = field
var torState: TorState = TorState()
get() = field
var proxy: Proxy? = null
get() = field
init {
manager.debug(true)
manager.addListener(listener)
listener.addLine(TorServiceConfig.getMetaData(application).toString())
}
fun isConnected(): Boolean {
return manager.state.isOn()
}
fun isStarting(): Boolean {
return manager.state.isStarting();
}
fun newIdentity(appContext: Application) {
appScope.launch(Dispatchers.IO) {
var result = manager.signal(TorControlSignal.Signal.NewNym)
appScope.launch(Dispatchers.Main) {
if (result.isSuccess) {
val msg = "You have changed Tor identity"
listener.addLine(msg)
Toast.makeText(appContext, msg, Toast.LENGTH_SHORT).show()
} else if (result.isFailure) {
val msg = "Tor identity change failed"
listener.addLine(msg)
Toast.makeText(appContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
}
private inner class TorListener: TorManagerEvent.Listener() {
private val _eventLines: MutableLiveData<String> = MutableLiveData("")
val eventLines: LiveData<String> = _eventLines
private val events: MutableList<String> = ArrayList(50)
fun addLine(line: String) {
synchronized(this) {
if (events.size > 49) {
events.removeAt(0)
}
events.add(line)
//Log.i(TAG, line)
try {
_eventLines.value = events.joinToString("\n")
} catch (e: Exception) {
_eventLines.postValue(events.joinToString("\n"))
}
}
}
override fun onEvent(event: TorManagerEvent) {
if (event is TorManagerEvent.State) {
val stateEvent: TorManagerEvent.State = event
val state = stateEvent.torState
torState.progressIndicator = state.bootstrap
val liveTorState = TorState()
liveTorState.progressIndicator = state.bootstrap
if (state.isOn()) {
torState.state = EnumTorState.ON
liveTorState.state = EnumTorState.ON
} else if (state.isStarting()) {
torState.state = EnumTorState.STARTING
liveTorState.state = EnumTorState.STARTING
} else if (state.isOff()) {
torState.state = EnumTorState.OFF
liveTorState.state = EnumTorState.OFF
} else if (state.isStopping()) {
torState.state = EnumTorState.STOPPING
liveTorState.state = EnumTorState.STOPPING
}
torStateLiveData.postValue(liveTorState)
}
addLine(event.toString())
super.onEvent(event)
}
override fun onEvent(event: TorEvent.Type.SingleLineEvent, output: String) {
addLine("$event - $output")
super.onEvent(event, output)
}
override fun onEvent(event: TorEvent.Type.MultiLineEvent, output: List<String>) {
addLine("multi-line event: $event. See Logs.")
// these events are many many many lines and should be moved
// off the main thread if ever needed to be dealt with.
val enabled = false
if (enabled) {
appScope.launch(Dispatchers.IO) {
Log.d(TAG, "-------------- multi-line event START: $event --------------")
for (line in output) {
Log.d(TAG, line)
}
Log.d(TAG, "--------------- multi-line event END: $event ---------------")
}
}
super.onEvent(event, output)
}
override fun managerEventError(t: Throwable) {
t.printStackTrace()
}
override fun managerEventAddressInfo(info: TorManagerEvent.AddressInfo) {
if (info.isNull) {
// Tear down HttpClient
} else {
info.socksInfoToProxyAddressOrNull()?.firstOrNull()?.let { proxyAddress ->
@Suppress("UNUSED_VARIABLE")
val socket = InetSocketAddress(proxyAddress.address.value, proxyAddress.port.value)
proxy = Proxy(Proxy.Type.SOCKS, socket)
}
}
}
override fun managerEventStartUpCompleteForTorInstance() {
// Do one-time things after we're bootstrapped
appScope.launch {
torControlManager.onionAddNew(
type = OnionAddress.PrivateKey.Type.ED25519_V3,
hsPorts = setOf(HiddenService.Ports(virtualPort = Port(443))),
flags = null,
maxStreams = null,
).onSuccess { hsEntry ->
addLine(
"New HiddenService: " +
"\n - Address: https://${hsEntry.address.canonicalHostname()}" +
"\n - PrivateKey: ${hsEntry.privateKey}"
)
torControlManager.onionDel(hsEntry.address).onSuccess {
addLine("Aaaaaaaaand it's gone...")
}.onFailure { t ->
t.printStackTrace()
}
}.onFailure { t ->
t.printStackTrace()
}
delay(20_000L)
torControlManager.infoGet(TorControlInfoGet.KeyWord.Uptime()).onSuccess { uptime ->
addLine("Uptime - $uptime")
}.onFailure { t ->
t.printStackTrace()
}
}
}
}
}

View file

@ -4,7 +4,7 @@ object Constants {
const val WALLET_NAME = "xmr_wallet" const val WALLET_NAME = "xmr_wallet"
const val MNEMONIC_LANGUAGE = "English" const val MNEMONIC_LANGUAGE = "English"
const val PREF_USES_PASSWORD = "pref_uses_password" const val PREF_USES_PASSWORD = "pref_uses_password"
const val PREF_USES_TOR = "pref_uses_tor" const val PREF_USES_PROXY = "pref_uses_tor"
const val PREF_PROXY = "pref_proxy" const val PREF_PROXY = "pref_proxy"
const val PREF_NODE_2 = "pref_node_2" const val PREF_NODE_2 = "pref_node_2"
const val PREF_CUSTOM_NODES = "pref_custom_nodes" const val PREF_CUSTOM_NODES = "pref_custom_nodes"
@ -13,6 +13,8 @@ object Constants {
const val PREF_MONEROCHAN = "pref_monerochan" const val PREF_MONEROCHAN = "pref_monerochan"
const val PREF_DONATE_PER_TX = "pref_donate_per_tx" const val PREF_DONATE_PER_TX = "pref_donate_per_tx"
const val PREF_FROZEN_COINS = "pref_frozen_coins" const val PREF_FROZEN_COINS = "pref_frozen_coins"
const val PREF_USE_BUNDLED_TOR = "pref_use_bundled_tor"
const val URI_PREFIX = "monero:" const val URI_PREFIX = "monero:"
const val URI_ARG_AMOUNT = "tx_amount" const val URI_ARG_AMOUNT = "tx_amount"
const val URI_ARG_AMOUNT2 = "amount" const val URI_ARG_AMOUNT2 = "amount"

View file

@ -1,11 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <selector xmlns:android="http://schemas.android.com/apk/res/android">
android:shape="rectangle"> <!-- Color when the row is selected -->
<padding <item android:drawable="@drawable/edittext_bg_enabled" android:state_enabled="true" />
android:bottom="8dp" <!-- Standard background color -->
android:left="12dp" <item android:drawable="@drawable/edittext_bg_disabled" />
android:right="12dp" </selector>
android:top="8dp" />
<solid android:color="@color/edittext_bg_color" />
<corners android:radius="8dp" />
</shape>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<padding
android:bottom="8dp"
android:left="12dp"
android:right="12dp"
android:top="8dp" />
<solid android:color="@color/edittext_bg_color_disabled" />
<corners android:radius="8dp" />
</shape>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<padding
android:bottom="8dp"
android:left="12dp"
android:right="12dp"
android:top="8dp" />
<solid android:color="@color/edittext_bg_color" />
<corners android:radius="8dp" />
</shape>

View file

@ -242,7 +242,17 @@
android:minWidth="48dp" android:minWidth="48dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/bundled_tor_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:text="@string/use_bundled_tor"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_address_edittext"
app:layout_constraintTop_toBottomOf="@id/tor_onboarding_switch"/>
<EditText <EditText
android:id="@+id/wallet_proxy_address_edittext" android:id="@+id/wallet_proxy_address_edittext"
android:layout_width="0dp" android:layout_width="0dp"
@ -250,10 +260,11 @@
android:background="@drawable/edittext_bg" android:background="@drawable/edittext_bg"
android:hint="@string/wallet_proxy_address_hint" android:hint="@string/wallet_proxy_address_hint"
android:minHeight="48dp" android:minHeight="48dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_port_edittext" app:layout_constraintBottom_toTopOf="@id/wallet_proxy_port_edittext"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tor_onboarding_switch" /> app:layout_constraintTop_toBottomOf="@id/bundled_tor_checkbox" />
<EditText <EditText
android:id="@+id/wallet_proxy_port_edittext" android:id="@+id/wallet_proxy_port_edittext"
@ -264,6 +275,7 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:inputType="number" android:inputType="number"
android:minHeight="48dp" android:minHeight="48dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -277,6 +289,7 @@
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:background="@drawable/button_bg" android:background="@drawable/button_bg"
android:text="@string/create_wallet" android:text="@string/create_wallet"
android:enabled="false"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/more_options_layout" /> app:layout_constraintTop_toBottomOf="@id/more_options_layout" />

View file

@ -237,11 +237,24 @@
app:layout_constraintTop_toBottomOf="@id/tor_switch" app:layout_constraintTop_toBottomOf="@id/tor_switch"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<CheckBox
android:id="@+id/bundled_tor_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:text="@string/use_bundled_tor"
android:visibility="visible"
android:layout_marginEnd="24dp"
android:layout_marginStart="24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_address_edittext"
app:layout_constraintTop_toTopOf="parent"/>
<EditText <EditText
android:id="@+id/wallet_proxy_address_edittext" android:id="@+id/wallet_proxy_address_edittext"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:background="@drawable/edittext_bg" android:background="@drawable/edittext_bg"
@ -250,7 +263,7 @@
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_port_edittext" app:layout_constraintBottom_toTopOf="@id/wallet_proxy_port_edittext"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toBottomOf="@id/bundled_tor_checkbox" />
<EditText <EditText
android:id="@+id/wallet_proxy_port_edittext" android:id="@+id/wallet_proxy_port_edittext"

View file

@ -29,6 +29,7 @@
<color name="oled_colorError">@color/oled_favouriteColor</color> <color name="oled_colorError">@color/oled_favouriteColor</color>
<color name="oled_colorOnError">#ffffff</color> <color name="oled_colorOnError">#ffffff</color>
<color name="edittext_bg_color">#202020</color> <color name="edittext_bg_color">#202020</color>
<color name="edittext_bg_color_disabled">#0E0E0E</color>
<color name="oled_locked_utxo">#956E43</color> <color name="oled_locked_utxo">#956E43</color>
<color name="oled_txBackgroundColor">#060606</color> <color name="oled_txBackgroundColor">#060606</color>
<color name="oled_dialogBackgroundColor">#0E0E0E</color> <color name="oled_dialogBackgroundColor">#0E0E0E</color>

View file

@ -30,6 +30,7 @@
<color name="oled_colorOnError">@color/oled_colorBackground</color> <color name="oled_colorOnError">@color/oled_colorBackground</color>
<color name="edittext_bg_color">#CCCCCC</color> <color name="edittext_bg_color">#CCCCCC</color>
<color name="button_disabled_bg_color">#454545</color> <color name="button_disabled_bg_color">#454545</color>
<color name="edittext_bg_color_disabled">#CCCCCC</color>
<color name="oled_locked_utxo">#B5895A</color> <color name="oled_locked_utxo">#B5895A</color>
<color name="oled_txBackgroundColor">#FBFBFB</color> <color name="oled_txBackgroundColor">#FBFBFB</color>
<color name="oled_dialogBackgroundColor">#E8E8E8</color> <color name="oled_dialogBackgroundColor">#E8E8E8</color>

View file

@ -136,6 +136,7 @@
<string name="block_height">Block Height</string> <string name="block_height">Block Height</string>
<string name="use_password_as_seed_offset">Use passphrase as seed offset</string> <string name="use_password_as_seed_offset">Use passphrase as seed offset</string>
<string name="trusted_daemon">Trusted daemon</string> <string name="trusted_daemon">Trusted daemon</string>
<string name="use_bundled_tor">Let Mysu start and manage a Tor daemon</string>
<string name="subbaddress_info_subtitle" translatable="false">#%1$d: %2$s</string> <string name="subbaddress_info_subtitle" translatable="false">#%1$d: %2$s</string>
<string name="previous_addresses">Previous addresses</string> <string name="previous_addresses">Previous addresses</string>
<string name="donate_label">Donate to Mysu</string> <string name="donate_label">Donate to Mysu</string>

View file

@ -17,6 +17,7 @@ allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
google() google()
maven { url 'https://jitpack.io' }
} }
} }