diff --git a/app/build.gradle b/app/build.gradle index 1ed048e..56139da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/cpp/monerujo.cpp b/app/src/main/cpp/monerujo.cpp index d0f7128..e4c3fa7 100644 --- a/app/src/main/cpp/monerujo.cpp +++ b/app/src/main/cpp/monerujo.cpp @@ -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(env, instance); wallet->startRefresh(); } diff --git a/app/src/main/java/net/mynero/wallet/MainActivity.kt b/app/src/main/java/net/mynero/wallet/MainActivity.kt index 1b0b9a6..7655faa 100644 --- a/app/src/main/java/net/mynero/wallet/MainActivity.kt +++ b/app/src/main/java/net/mynero/wallet/MainActivity.kt @@ -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() } diff --git a/app/src/main/java/net/mynero/wallet/fragment/home/HomeFragment.kt b/app/src/main/java/net/mynero/wallet/fragment/home/HomeFragment.kt index 876aa97..ded6fce 100644 --- a/app/src/main/java/net/mynero/wallet/fragment/home/HomeFragment.kt +++ b/app/src/main/java/net/mynero/wallet/fragment/home/HomeFragment.kt @@ -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 diff --git a/app/src/main/java/net/mynero/wallet/fragment/onboarding/OnboardingFragment.kt b/app/src/main/java/net/mynero/wallet/fragment/onboarding/OnboardingFragment.kt index 15dbd46..48e46b7 100644 --- a/app/src/main/java/net/mynero/wallet/fragment/onboarding/OnboardingFragment.kt +++ b/app/src/main/java/net/mynero/wallet/fragment/onboarding/OnboardingFragment.kt @@ -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(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) + } } \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/fragment/onboarding/OnboardingViewModel.kt b/app/src/main/java/net/mynero/wallet/fragment/onboarding/OnboardingViewModel.kt index 28dc105..5650f6f 100644 --- a/app/src/main/java/net/mynero/wallet/fragment/onboarding/OnboardingViewModel.kt +++ b/app/src/main/java/net/mynero/wallet/fragment/onboarding/OnboardingViewModel.kt @@ -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 = _useProxy + private val _proxyAddress = MutableLiveData("") + private val _proxyPort = MutableLiveData("") + private val _useBundledTor = MutableLiveData(false) + val useBundledTor: LiveData = _useBundledTor + private val _passphrase = MutableLiveData("") + private val _confirmedPassphrase = MutableLiveData("") var showMoreOptions: LiveData = _showMoreOptions - var enableCreateButton: LiveData = _enableCreateButton var seedType: LiveData = _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) diff --git a/app/src/main/java/net/mynero/wallet/fragment/settings/SettingsFragment.kt b/app/src/main/java/net/mynero/wallet/fragment/settings/SettingsFragment.kt index 6a9e5ed..c5eeabc 100644 --- a/app/src/main/java/net/mynero/wallet/fragment/settings/SettingsFragment.kt +++ b/app/src/main/java/net/mynero/wallet/fragment/settings/SettingsFragment.kt @@ -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(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(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() diff --git a/app/src/main/java/net/mynero/wallet/livedata/LiveData.kt b/app/src/main/java/net/mynero/wallet/livedata/LiveData.kt new file mode 100644 index 0000000..99c1346 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/livedata/LiveData.kt @@ -0,0 +1,414 @@ +package net.mynero.wallet.livedata + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData + +fun combineLatest( + source1: LiveData, + source2: LiveData, + func: (T1?, T2?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 combineLatest( + source1: LiveData, + source2: LiveData, + source3: LiveData, + func: (T1?, T2?, T3?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 combineLatest( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + func: (T1?, T2?, T3?, T4?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 combineLatest( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + source5: LiveData, + func: (T1?, T2?, T3?, T4?, T5?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 combineLatestIgnoreNull( + source1: LiveData, + source2: LiveData, + func: (T1?, T2?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 combineLatestIgnoreNull( + source1: LiveData, + source2: LiveData, + source3: LiveData, + func: (T1?, T2?, T3?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 combineLatestIgnoreNull( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + func: (T1?, T2?, T3?, T4?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 combineLatestIgnoreNull( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + source5: LiveData, + func: (T1?, T2?, T3?, T4?, T5?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 combineLatestIgnoreNull( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + source5: LiveData, + source6: LiveData, + func: (T1?, T2?, T3?, T4?, T5?, T6?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 combineLatestIgnoreNull( + source1: LiveData, + source2: LiveData, + source3: LiveData, + source4: LiveData, + source5: LiveData, + source6: LiveData, + source7: LiveData, + source8: LiveData, + func: (T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?) -> S? +): LiveData { + val result = MediatorLiveData() + 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 +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/model/EnumTorState.kt b/app/src/main/java/net/mynero/wallet/model/EnumTorState.kt new file mode 100644 index 0000000..f5da735 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/model/EnumTorState.kt @@ -0,0 +1,8 @@ +package net.mynero.wallet.model + +enum class EnumTorState { + STARTING, + ON, + STOPPING, + OFF +} diff --git a/app/src/main/java/net/mynero/wallet/model/TorState.kt b/app/src/main/java/net/mynero/wallet/model/TorState.kt new file mode 100644 index 0000000..785d056 --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/model/TorState.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/model/Wallet.kt b/app/src/main/java/net/mynero/wallet/model/Wallet.kt index f1ad9c6..ffd28c5 100644 --- a/app/src/main/java/net/mynero/wallet/model/Wallet.kt +++ b/app/src/main/java/net/mynero/wallet/model/Wallet.kt @@ -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() diff --git a/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.kt b/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.kt index 64dec15..8c76a1f 100644 --- a/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.kt +++ b/app/src/main/java/net/mynero/wallet/service/MoneroHandlerThread.kt @@ -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 || diff --git a/app/src/main/java/net/mynero/wallet/service/PrefService.kt b/app/src/main/java/net/mynero/wallet/service/PrefService.kt index 3332b49..c7c45fb 100644 --- a/app/src/main/java/net/mynero/wallet/service/PrefService.kt +++ b/app/src/main/java/net/mynero/wallet/service/PrefService.kt @@ -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 diff --git a/app/src/main/java/net/mynero/wallet/service/ProxyService.kt b/app/src/main/java/net/mynero/wallet/service/ProxyService.kt index 8385829..815c1ab 100644 --- a/app/src/main/java/net/mynero/wallet/service/ProxyService.kt +++ b/app/src/main/java/net/mynero/wallet/service/ProxyService.kt @@ -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 = 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, "") diff --git a/app/src/main/java/net/mynero/wallet/service/SamouraiTorManager.kt b/app/src/main/java/net/mynero/wallet/service/SamouraiTorManager.kt new file mode 100644 index 0000000..d12ca2d --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/SamouraiTorManager.kt @@ -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 { + 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" + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mynero/wallet/service/TorKmpManager.kt b/app/src/main/java/net/mynero/wallet/service/TorKmpManager.kt new file mode 100644 index 0000000..3a0084c --- /dev/null +++ b/app/src/main/java/net/mynero/wallet/service/TorKmpManager.kt @@ -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 get() = listener.eventLines + + private val appScope by lazy { + CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) + } + + val torStateLiveData: MutableLiveData = 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 = MutableLiveData("") + val eventLines: LiveData = _eventLines + private val events: MutableList = 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) { + 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() + } + } + } + } +} diff --git a/app/src/main/java/net/mynero/wallet/util/Constants.kt b/app/src/main/java/net/mynero/wallet/util/Constants.kt index 25963c8..169726c 100644 --- a/app/src/main/java/net/mynero/wallet/util/Constants.kt +++ b/app/src/main/java/net/mynero/wallet/util/Constants.kt @@ -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" diff --git a/app/src/main/res/drawable/edittext_bg.xml b/app/src/main/res/drawable/edittext_bg.xml index 1fe35cf..72cfe66 100644 --- a/app/src/main/res/drawable/edittext_bg.xml +++ b/app/src/main/res/drawable/edittext_bg.xml @@ -1,11 +1,7 @@ - - - - - + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/edittext_bg_disabled.xml b/app/src/main/res/drawable/edittext_bg_disabled.xml new file mode 100644 index 0000000..f1a671f --- /dev/null +++ b/app/src/main/res/drawable/edittext_bg_disabled.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/edittext_bg_enabled.xml b/app/src/main/res/drawable/edittext_bg_enabled.xml new file mode 100644 index 0000000..1fe35cf --- /dev/null +++ b/app/src/main/res/drawable/edittext_bg_enabled.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_onboarding.xml b/app/src/main/res/layout/fragment_onboarding.xml index 2faca0a..e774ef8 100644 --- a/app/src/main/res/layout/fragment_onboarding.xml +++ b/app/src/main/res/layout/fragment_onboarding.xml @@ -242,7 +242,17 @@ android:minWidth="48dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + + app:layout_constraintTop_toBottomOf="@id/bundled_tor_checkbox" /> diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index c264dd9..200616b 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -237,11 +237,24 @@ app:layout_constraintTop_toBottomOf="@id/tor_switch" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> + + + app:layout_constraintTop_toBottomOf="@id/bundled_tor_checkbox" /> @color/oled_favouriteColor #ffffff #202020 + #0E0E0E #956E43 #060606 #0E0E0E diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 52d545d..84f7ff4 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -30,6 +30,7 @@ @color/oled_colorBackground #CCCCCC #454545 + #CCCCCC #B5895A #FBFBFB #E8E8E8 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f3c43e..9cd02b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -136,6 +136,7 @@ Block Height Use passphrase as seed offset Trusted daemon + Let Mysu start and manage a Tor daemon #%1$d: %2$s Previous addresses Donate to Mysu diff --git a/build.gradle b/build.gradle index 811b131..d807d57 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ allprojects { repositories { mavenCentral() google() + maven { url 'https://jitpack.io' } } }