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 "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
testImplementation "junit:junit:4.13.2"
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
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);
wallet->startRefresh();
}

View file

@ -45,6 +45,7 @@ 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()) {
@ -87,7 +88,6 @@ class MainActivity : AppCompatActivity(), MoneroHandlerThread.Listener, Password
historyService = HistoryService(thread)
blockchainService = BlockchainService(thread)
daemonService = DaemonService(thread)
proxyService = ProxyService(thread)
utxoService = UTXOService(thread)
thread.start()
}

View file

@ -10,6 +10,7 @@ import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
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.PrefService
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 timber.log.Timber

View file

@ -18,8 +18,12 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.SwitchCompat
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
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.R
import net.mynero.wallet.data.Node
@ -35,26 +39,6 @@ import net.mynero.wallet.util.Constants
class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListener {
private var useOffset = true
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 walletProxyPortEditText: EditText? = null
private var walletPasswordEditText: EditText? = null
@ -113,7 +97,7 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
advancedOptionsLayout?.visibility = View.GONE
}
}
mViewModel?.enableCreateButton?.observe(viewLifecycleOwner) { enable: Boolean ->
mViewModel?.enableButton?.observe(viewLifecycleOwner) { enable: Boolean ->
createWalletButton?.isEnabled = enable
}
mViewModel?.seedType?.observe(viewLifecycleOwner) { seedType: SeedType ->
@ -131,9 +115,27 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
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() {
val useBundledTor = view?.findViewById<CheckBox>(R.id.bundled_tor_checkbox)
seedOffsetCheckbox?.isChecked = useOffset
// Disable onBack click
val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
@ -147,12 +149,9 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
useOffset = b
}
createWalletButton?.setOnClickListener {
prepareDefaultNode()
onBackPressedCallback.isEnabled = false
(getActivity()?.application as MoneroApplication).executor?.execute {
createOrImportWallet(
walletPasswordEditText?.text.toString(),
walletPasswordConfirmEditText?.text.toString(),
walletSeedEditText?.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 afterTextChanged(editable: Editable) {
val text = editable.toString()
mViewModel?.setPassphrase(text)
if (text.isEmpty()) {
walletPasswordConfirmEditText?.text = null
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 {
override fun beforeTextChanged(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() }
torSwitch?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_TOR, b)?.apply()
removeProxyTextListeners()
if (b) {
if (ProxyService.instance?.hasProxySet() == true) {
val proxyAddress =
ProxyService.instance?.proxyAddress ?: return@setOnCheckedChangeListener
val proxyPort =
ProxyService.instance?.proxyPort ?: return@setOnCheckedChangeListener
initProxyStuff(proxyAddress, proxyPort)
} else {
initProxyStuff("127.0.0.1", "9050")
}
addProxyTextListeners()
useBundledTor?.visibility = View.VISIBLE
walletProxyAddressEditText?.visibility = View.VISIBLE
walletProxyPortEditText?.visibility = View.VISIBLE
} else {
useBundledTor?.visibility = View.GONE
walletProxyAddressEditText?.visibility = View.GONE
walletProxyPortEditText?.visibility = View.GONE
}
mViewModel?.updateProxy(getActivity()?.application as MoneroApplication)
mViewModel?.setUseProxy(b)
}
showXmrchanSwitch?.isChecked =
PrefService.instance?.getBoolean(Constants.PREF_MONEROCHAN, true) == true
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)
}
})
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 ->
PrefService.instance?.edit()?.putBoolean(Constants.PREF_MONEROCHAN, b)?.apply()
if (b) {
xmrchanOnboardingImage?.visibility = View.VISIBLE
} else {
@ -220,6 +238,21 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
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() {
@ -233,46 +266,20 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
mViewModel?.setSeedType(newSeedType)
}
private fun prepareDefaultNode() {
PrefService.instance?.node
}
private fun createOrImportWallet(
walletPassword: String,
confirmedPassword: String,
walletSeed: String,
restoreHeightText: String
) {
val activity: Activity? = activity
if (activity != null) {
mViewModel?.createOrImportWallet(
activity,
walletPassword,
confirmedPassword,
walletSeed,
restoreHeightText,
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)
lifecycleScope.launch(Dispatchers.IO) {
mViewModel?.createOrImportWallet(
activity,
walletSeed,
restoreHeightText,
useOffset
)
}
}
}
@ -284,7 +291,7 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
getString(R.string.node_selected),
Toast.LENGTH_SHORT
).show()
mViewModel?.updateProxy(activity?.application as MoneroApplication)
refreshProxy()
}
override fun onClickedEditNode(node: Node?) {}
@ -303,4 +310,10 @@ class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListe
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.MoneroApplication
import net.mynero.wallet.R
import net.mynero.wallet.livedata.combineLatestIgnoreNull
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.RestoreHeight
import java.io.File
@ -19,168 +21,179 @@ import java.util.Calendar
class OnboardingViewModel : ViewModel() {
private val _showMoreOptions = MutableLiveData(false)
private val _enableCreateButton = MutableLiveData(true)
private val _creatingWallet = MutableLiveData(false)
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 enableCreateButton: LiveData<Boolean> = _enableCreateButton
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() {
val currentValue = showMoreOptions.value ?: false
val newValue = !currentValue
_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?) {
_seedType.value = seedType
}
fun setProxyAddress(address: String) {
proxyAddress = address
}
fun setProxyPort(port: String) {
proxyPort = port
}
fun createOrImportWallet(
mainActivity: Activity,
walletPassword: String,
confirmedPassword: String,
walletSeed: String,
restoreHeightText: String,
useOffset: Boolean
) {
val passphrase = _passphrase.value ?: return
val confirmedPassphrase = _confirmedPassphrase.value ?: return
val useProxy = _useProxy.value ?: return
val application = mainActivity.application as MoneroApplication
application.executor?.execute {
_enableCreateButton.postValue(false)
val offset = if (useOffset) walletPassword else ""
if (walletPassword.isNotEmpty()) {
if (walletPassword != confirmedPassword) {
_enableCreateButton.postValue(true)
mainActivity.runOnUiThread {
Toast.makeText(
mainActivity,
application.getString(R.string.invalid_confirmed_password),
Toast.LENGTH_SHORT
).show()
}
return@execute
}
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_PASSWORD, true)
?.apply()
}
var restoreHeight = newRestoreHeight
val walletFile = File(mainActivity.applicationInfo.dataDir, Constants.WALLET_NAME)
var wallet: Wallet? = null
if (offset.isNotEmpty()) {
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_OFFSET, true)?.apply()
}
val seedTypeValue = seedType.value ?: return@execute
if (walletSeed.isEmpty()) {
if (seedTypeValue == SeedType.POLYSEED) {
wallet = if (offset.isEmpty()) {
mainActivity.runOnUiThread {
_enableCreateButton.postValue(true)
Toast.makeText(
mainActivity,
application.getString(R.string.invalid_empty_passphrase),
Toast.LENGTH_SHORT
).show()
}
return@execute
} else {
WalletManager.instance?.createWalletPolyseed(
walletFile,
walletPassword,
offset,
Constants.MNEMONIC_LANGUAGE
)
}
} else if (seedTypeValue == SeedType.LEGACY) {
val tmpWalletFile =
File(mainActivity.applicationInfo.dataDir, Constants.WALLET_NAME + "_tmp")
val tmpWallet =
createTempWallet(tmpWalletFile) //we do this to get seed, then recover wallet so we can use seed offset
tmpWallet?.let {
wallet = WalletManager.instance?.recoveryWallet(
walletFile,
walletPassword,
tmpWallet.getSeed("") ?: return@let,
offset,
restoreHeight
)
tmpWalletFile.delete()
}
}
} else {
if (getMnemonicType(walletSeed) == SeedType.UNKNOWN) {
mainActivity.runOnUiThread {
_enableCreateButton.postValue(true)
Toast.makeText(
mainActivity,
application.getString(R.string.invalid_mnemonic_code),
Toast.LENGTH_SHORT
).show()
}
return@execute
}
if (restoreHeightText.isNotEmpty()) {
restoreHeight = restoreHeightText.toLong()
}
if (seedTypeValue == SeedType.POLYSEED) {
wallet = WalletManager.instance?.recoveryWalletPolyseed(
walletFile,
walletPassword,
walletSeed,
offset
)
} else if (seedTypeValue == SeedType.LEGACY) {
wallet = WalletManager.instance?.recoveryWallet(
walletFile,
walletPassword,
walletSeed,
offset,
restoreHeight
)
}
}
val walletStatus = wallet?.status
wallet?.close()
val ok = walletStatus?.isOk
walletFile.delete() // cache is broken for some reason when recovering wallets. delete the file here. this happens in monerujo too.
if (ok == true) {
(mainActivity as MainActivity).init(walletFile, walletPassword)
mainActivity.runOnUiThread { mainActivity.onBackPressed() }
} else {
_creatingWallet.postValue(true)
val offset = if (useOffset) confirmedPassphrase else ""
if (passphrase.isNotEmpty()) {
if (passphrase != confirmedPassphrase) {
_creatingWallet.postValue(false)
mainActivity.runOnUiThread {
_enableCreateButton.postValue(true)
Toast.makeText(
mainActivity,
application.getString(
R.string.create_wallet_failed,
walletStatus?.errorString
),
application.getString(R.string.invalid_confirmed_password),
Toast.LENGTH_SHORT
).show()
}
return
}
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_PASSWORD, true)
?.apply()
}
var restoreHeight = newRestoreHeight
val walletFile = File(mainActivity.applicationInfo.dataDir, Constants.WALLET_NAME)
var wallet: Wallet? = null
if (offset.isNotEmpty()) {
PrefService.instance?.edit()?.putBoolean(Constants.PREF_USES_OFFSET, true)?.apply()
}
val seedTypeValue = seedType.value ?: return
if (walletSeed.isEmpty()) {
if (seedTypeValue == SeedType.POLYSEED) {
wallet = if (offset.isEmpty()) {
mainActivity.runOnUiThread {
_creatingWallet.postValue(false)
Toast.makeText(
mainActivity,
application.getString(R.string.invalid_empty_passphrase),
Toast.LENGTH_SHORT
).show()
}
return
} else {
WalletManager.instance?.createWalletPolyseed(
walletFile,
passphrase,
offset,
Constants.MNEMONIC_LANGUAGE
)
}
} else if (seedTypeValue == SeedType.LEGACY) {
val tmpWalletFile =
File(mainActivity.applicationInfo.dataDir, Constants.WALLET_NAME + "_tmp")
val tmpWallet =
createTempWallet(tmpWalletFile) //we do this to get seed, then recover wallet so we can use seed offset
tmpWallet?.let {
wallet = WalletManager.instance?.recoveryWallet(
walletFile,
passphrase,
tmpWallet.getSeed("") ?: return@let,
offset,
restoreHeight
)
tmpWalletFile.delete()
}
}
} else {
if (getMnemonicType(walletSeed) == SeedType.UNKNOWN) {
mainActivity.runOnUiThread {
_creatingWallet.postValue(false)
Toast.makeText(
mainActivity,
application.getString(R.string.invalid_mnemonic_code),
Toast.LENGTH_SHORT
).show()
}
return
}
if (restoreHeightText.isNotEmpty()) {
restoreHeight = restoreHeightText.toLong()
}
if (seedTypeValue == SeedType.POLYSEED) {
wallet = WalletManager.instance?.recoveryWalletPolyseed(
walletFile,
passphrase,
walletSeed,
offset
)
} else if (seedTypeValue == SeedType.LEGACY) {
wallet = WalletManager.instance?.recoveryWallet(
walletFile,
passphrase,
walletSeed,
offset,
restoreHeight
)
}
}
val walletStatus = wallet?.status
wallet?.close()
val ok = walletStatus?.isOk
walletFile.delete() // cache is broken for some reason when recovering wallets. delete the file here. this happens in monerujo too.
if (ok == true) {
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() }
} else {
mainActivity.runOnUiThread {
_creatingWallet.postValue(false)
Toast.makeText(
mainActivity,
application.getString(
R.string.create_wallet_failed,
walletStatus?.errorString
),
Toast.LENGTH_SHORT
).show()
}
}
}
@ -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) {
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.ViewGroup
import android.widget.Button
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.SwitchCompat
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.NavHostFragment
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.data.Node
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.PasswordListener
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.BlockchainService
import net.mynero.wallet.service.DaemonService
import net.mynero.wallet.service.HistoryService
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.Constants
import org.json.JSONArray
import timber.log.Timber
class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListener, AddNodeListener,
EditNodeListener {
@ -75,6 +66,12 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
view.findViewById<ConstraintLayout>(R.id.wallet_proxy_settings_layout)
walletProxyAddressEditText = view.findViewById(R.id.wallet_proxy_address_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 =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
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()
}
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
cachedProxyPort = ProxyService.instance?.proxyPort ?: return
if (ProxyService.instance?.hasProxySet() == true) {
@ -106,7 +103,7 @@ class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListen
proxySettingsLayout.visibility = View.GONE
}
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 (ProxyService.instance?.hasProxySet() == true) {
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) {
override fun handleOnBackPressed() {
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
}
external fun startRefresh()
fun startRefresh() {
startRefreshJ()
}
private external fun startRefreshJ()
external fun pauseRefresh()
external fun refresh(): Boolean
external fun refreshAsync()

View file

@ -51,7 +51,7 @@ class MoneroHandlerThread(name: String, val listener: Listener?, wallet: Wallet)
override fun run() {
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 isLocalIp =
currentNode?.address?.startsWith("10.") == true ||

View file

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

View file

@ -1,29 +1,48 @@
package net.mynero.wallet.service
import android.app.Application
import android.util.Patterns
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.data.Node
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(thread: MoneroHandlerThread) : ServiceBase(thread) {
class ProxyService(activity: MainActivity) : ServiceBase(null) {
val proxyChangeEvents: SingleLiveEvent<String> = SingleLiveEvent()
var samouraiTorManager: SamouraiTorManager? = null
init {
samouraiTorManager = SamouraiTorManager(activity.application, TorKmpManager(activity.application))
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) {
var finalProxyAddress = proxyAddress
var finalProxyPort = proxyPort
val usesProxy = PrefService.instance?.getBoolean(Constants.PREF_USES_TOR, false) == true
val curretNode = PrefService.instance?.node
val isNodeLocalIp =
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) }
if (!usesProxy || isNodeLocalIp) {
if (!usingProxy || isNodeLocalIp) {
// User is not using proxy, or is using local node currently, so we will disable proxy here.
proxyChangeEvents.postValue("")
return
@ -44,6 +63,12 @@ class ProxyService(thread: MoneroHandlerThread) : ServiceBase(thread) {
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?
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 MNEMONIC_LANGUAGE = "English"
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_NODE_2 = "pref_node_2"
const val PREF_CUSTOM_NODES = "pref_custom_nodes"
@ -13,6 +13,8 @@ object Constants {
const val PREF_MONEROCHAN = "pref_monerochan"
const val PREF_DONATE_PER_TX = "pref_donate_per_tx"
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_ARG_AMOUNT = "tx_amount"
const val URI_ARG_AMOUNT2 = "amount"

View file

@ -1,11 +1,7 @@
<?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>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Color when the row is selected -->
<item android:drawable="@drawable/edittext_bg_enabled" android:state_enabled="true" />
<!-- Standard background color -->
<item android:drawable="@drawable/edittext_bg_disabled" />
</selector>

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

View file

@ -237,11 +237,24 @@
app:layout_constraintTop_toBottomOf="@id/tor_switch"
app:layout_constraintStart_toStartOf="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
android:id="@+id/wallet_proxy_address_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:layout_marginStart="24dp"
android:background="@drawable/edittext_bg"
@ -250,7 +263,7 @@
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_port_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/bundled_tor_checkbox" />
<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_colorOnError">#ffffff</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_txBackgroundColor">#060606</color>
<color name="oled_dialogBackgroundColor">#0E0E0E</color>

View file

@ -30,6 +30,7 @@
<color name="oled_colorOnError">@color/oled_colorBackground</color>
<color name="edittext_bg_color">#CCCCCC</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_txBackgroundColor">#FBFBFB</color>
<color name="oled_dialogBackgroundColor">#E8E8E8</color>

View file

@ -136,6 +136,7 @@
<string name="block_height">Block Height</string>
<string name="use_password_as_seed_offset">Use passphrase as seed offset</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="previous_addresses">Previous addresses</string>
<string name="donate_label">Donate to Mysu</string>

View file

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