chore: replace some fragments with activities

This commit is contained in:
- 2024-09-26 20:22:48 +02:00
parent d1645a43ec
commit 66e61d9064
39 changed files with 2107 additions and 3048 deletions

View file

@ -9,8 +9,8 @@ android {
minSdkVersion 22
targetSdkVersion 34
compileSdk 34
versionCode 50900
versionName "0.5.9 'Fluorine Fermi'"
versionCode 51000
versionName "0.5.10 'Fluorine Fermi'"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {

View file

@ -1,25 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Users\Test\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -19,14 +19,16 @@
android:supportsRtl="true"
android:theme="@style/MyMaterialThemeOled"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:name=".StartActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -36,6 +38,48 @@
<data android:scheme="monero" />
</intent-filter>
</activity>
<activity
android:name=".OnboardingActivity"
android:exported="true">
</activity>
<activity
android:name=".PasswordActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:name=".HomeActivity"
android:exported="true">
</activity>
<activity
android:name=".SendActivity"
android:exported="true">
</activity>
<activity
android:name=".ReceiveActivity"
android:exported="true">
</activity>
<activity
android:name=".SettingsActivity"
android:exported="true">
</activity>
<activity
android:name=".TransactionActivity"
android:exported="true">
</activity>
<activity
android:name=".UtxosActivity"
android:exported="true">
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"

View file

@ -1,27 +1,20 @@
package net.mynero.wallet.fragment.home
package net.mynero.wallet
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.mynero.wallet.MainActivity
import net.mynero.wallet.R
import net.mynero.wallet.adapter.TransactionInfoAdapter
import net.mynero.wallet.adapter.TransactionInfoAdapter.TxInfoAdapterListener
import net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.BalanceService
@ -32,47 +25,37 @@ import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.Constants
class HomeFragment : Fragment(), TxInfoAdapterListener {
class HomeActivity : AppCompatActivity() {
private var startHeight: Long = 0
private var mViewModel: HomeViewModel? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mainActivity = activity as MainActivity?
mViewModel = ViewModelProvider(this)[HomeViewModel::class.java]
bindObservers(view)
bindListeners(view)
mainActivity?.restartEvents?.observe(viewLifecycleOwner) {
bindObservers(view)
bindListeners(view)
}
}
val settingsImageView = findViewById<ImageView>(R.id.settings_imageview)
val sendButton = findViewById<Button>(R.id.send_button)
val receiveButton = findViewById<Button>(R.id.receive_button)
private fun bindListeners(view: View) {
val settingsImageView = view.findViewById<ImageView>(R.id.settings_imageview)
val sendButton = view.findViewById<Button>(R.id.send_button)
val receiveButton = view.findViewById<Button>(R.id.receive_button)
settingsImageView.setOnClickListener { navigate(HomeFragmentDirections.navToSettings()) }
sendButton.setOnClickListener { navigate(HomeFragmentDirections.navToSend()) }
receiveButton.setOnClickListener { navigate(HomeFragmentDirections.navToReceive()) }
}
private fun bindObservers(view: View) {
val txHistoryRecyclerView =
view.findViewById<RecyclerView>(R.id.transaction_history_recyclerview)
val unlockedBalanceTextView = view.findViewById<TextView>(R.id.balance_unlocked_textview)
val lockedBalanceTextView = view.findViewById<TextView>(R.id.balance_locked_textview)
findViewById<RecyclerView>(R.id.transaction_history_recyclerview)
val unlockedBalanceTextView = findViewById<TextView>(R.id.balance_unlocked_textview)
val lockedBalanceTextView = findViewById<TextView>(R.id.balance_locked_textview)
val balanceService = BalanceService.instance
val historyService = HistoryService.instance
val blockchainService = BlockchainService.instance
ProxyService.instance?.proxyChangeEvents?.observe(viewLifecycleOwner) { proxy ->
settingsImageView.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}
sendButton.setOnClickListener {
startActivity(Intent(this, SendActivity::class.java))
}
receiveButton.setOnClickListener {
startActivity(Intent(this, ReceiveActivity::class.java))
}
ProxyService.instance?.proxyChangeEvents?.observe(this) { proxy ->
lifecycleScope.launch(Dispatchers.IO) {
Log.d("HomeFragment", "Updating proxy, restarting wallet. proxy=$proxy")
val finalProxy =
@ -84,7 +67,7 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
}
}
DaemonService.instance?.daemonChangeEvents?.observe(viewLifecycleOwner) { daemon ->
DaemonService.instance?.daemonChangeEvents?.observe(this) { daemon ->
lifecycleScope.launch(Dispatchers.IO) {
Log.d("HomeFragment", "Updating daemon, restarting wallet. daemon=$daemon")
WalletManager.instance?.setDaemon(daemon)
@ -94,7 +77,7 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
}
}
balanceService?.balanceInfo?.observe(viewLifecycleOwner) { balanceInfo ->
balanceService?.balanceInfo?.observe(this) { balanceInfo ->
if (balanceInfo != null) {
unlockedBalanceTextView.text = balanceInfo.unlockedDisplay
if (balanceInfo.lockedDisplay == Constants.STREET_MODE_BALANCE || balanceInfo.isLockedBalanceZero) {
@ -108,9 +91,9 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
}
}
}
val progressBar = view.findViewById<ProgressBar>(R.id.sync_progress_bar)
val progressBarText = view.findViewById<TextView>(R.id.sync_progress_text)
blockchainService?.height?.observe(viewLifecycleOwner) { height: Long ->
val progressBar = findViewById<ProgressBar>(R.id.sync_progress_bar)
val progressBarText = findViewById<TextView>(R.id.sync_progress_text)
blockchainService?.height?.observe(this) { height: Long ->
val wallet = WalletManager.instance?.wallet
if (wallet?.isSynchronized == false) {
if (startHeight == 0L && height != 1L) {
@ -133,10 +116,17 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
progressBarText.text = "Synchronized at $height"
}
}
val adapter = TransactionInfoAdapter(this)
txHistoryRecyclerView.layoutManager = LinearLayoutManager(activity)
val activity = this
val adapter = TransactionInfoAdapter(object : TransactionInfoAdapter.TxInfoAdapterListener {
override fun onClickTransaction(txInfo: TransactionInfo?) {
val intent = Intent(activity, TransactionActivity::class.java)
intent.putExtra(Constants.NAV_ARG_TXINFO, txInfo)
startActivity(intent)
}
})
txHistoryRecyclerView.layoutManager = LinearLayoutManager(this)
txHistoryRecyclerView.adapter = adapter
historyService?.history?.observe(viewLifecycleOwner) { history: List<TransactionInfo> ->
historyService?.history?.observe(this) { history: List<TransactionInfo> ->
if (history.isEmpty()) {
// DISPLAYING EMPTY WALLET HISTORY
val wallet = WalletManager.instance?.wallet
@ -149,7 +139,7 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
R.drawable.xmrchan_loading2 // img for loading
}
txHistoryRecyclerView.visibility = View.GONE
displayEmptyHistory(true, view, textResId, botImgResId)
displayEmptyHistory(true, this, textResId, botImgResId)
} else {
// POPULATED WALLET HISTORY
val sortedHistory = history.sortedByDescending { it.timestamp }
@ -161,7 +151,7 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
txHistoryRecyclerView.visibility = View.VISIBLE
displayEmptyHistory(
false,
view,
this,
R.string.no_history_nget_some_monero_in_here,
R.drawable.xmrchan_loading2
)
@ -169,7 +159,7 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
samouraiTorManager?.getTorStateLiveData()?.observe(viewLifecycleOwner) {
samouraiTorManager?.getTorStateLiveData()?.observe(this) {
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if (socketAddress.toString().isEmpty()) return@let
if (ProxyService.instance?.usingProxy == true && ProxyService.instance?.useBundledTor == true) {
@ -182,6 +172,7 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
}
}
}
}
private fun refreshProxy(proxyAddress: String, proxyPort: String) {
@ -194,23 +185,9 @@ class HomeFragment : Fragment(), TxInfoAdapterListener {
}
}
override fun onClickTransaction(txInfo: TransactionInfo?) {
val directions: NavDirections = HomeFragmentDirections.navToTransaction(txInfo)
navigate(directions)
}
private fun navigate(destination: NavDirections) {
val activity = activity
if (activity != null) {
val fm = activity.supportFragmentManager
val navHostFragment = fm.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
navHostFragment?.navController?.navigate(destination)
}
}
private fun displayEmptyHistory(
display: Boolean,
view: View,
view: HomeActivity,
textResId: Int,
botImgResId: Int
) {

View file

@ -1,119 +0,0 @@
package net.mynero.wallet
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.NavHostFragment
import net.mynero.wallet.fragment.dialog.PasswordBottomSheetDialog
import net.mynero.wallet.fragment.dialog.PasswordBottomSheetDialog.PasswordListener
import net.mynero.wallet.fragment.dialog.SendBottomSheetDialog
import net.mynero.wallet.livedata.SingleLiveEvent
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.AddressService
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.MoneroHandlerThread
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.service.TxService
import net.mynero.wallet.service.UTXOService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.UriData
import java.io.File
class MainActivity : AppCompatActivity(), MoneroHandlerThread.Listener, PasswordListener {
val restartEvents: SingleLiveEvent<*> = SingleLiveEvent<Any?>()
var thread: MoneroHandlerThread? = null
private set
private var balanceService: BalanceService? = null
private var addressService: AddressService? = null
private var historyService: HistoryService? = null
private var daemonService: DaemonService? = null
private var blockchainService: BlockchainService? = null
private var utxoService: UTXOService? = null
private var proceedToSend = false
private var uriData: UriData? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val walletFile = File(applicationInfo.dataDir, Constants.WALLET_NAME)
val walletKeysFile = File(applicationInfo.dataDir, Constants.WALLET_NAME + ".keys")
if (walletKeysFile.exists()) {
val promptPassword =
PrefService.instance?.getBoolean(Constants.PREF_USES_PASSWORD, false) == true
if (!promptPassword) {
init(walletFile, "")
} else {
val passwordDialog = PasswordBottomSheetDialog()
passwordDialog.listener = this
passwordDialog.show(supportFragmentManager, "password_dialog")
}
val intent = intent
val uri = intent.data
if (uri != null) {
uriData = UriData.parse(uri.toString())
if (uriData != null) {
proceedToSend = true
}
}
} else {
navigate(R.id.onboarding_fragment)
}
}
private fun navigate(destination: Int) {
val activity: FragmentActivity = this
val fm = activity.supportFragmentManager
val navHostFragment = fm.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
navHostFragment?.navController?.navigate(destination)
}
fun init(walletFile: File, password: String?) {
val wallet = WalletManager.instance?.openWallet(walletFile.absolutePath, password ?: "")
thread = wallet?.let { MoneroHandlerThread("WalletService", this, it) }
thread?.let { thread ->
TxService(thread)
balanceService = BalanceService(thread)
addressService = AddressService(thread)
historyService = HistoryService(thread)
blockchainService = BlockchainService(thread)
daemonService = DaemonService(thread)
utxoService = UTXOService(thread)
thread.start()
}
}
override fun onRefresh(walletSynced: Boolean) {
if (walletSynced) utxoService?.refreshUtxos()
historyService?.refreshHistory()
balanceService?.refreshBalance()
blockchainService?.refreshBlockchain()
addressService?.refreshAddresses()
}
override fun onConnectionFail() {
runOnUiThread {
Toast.makeText(application, R.string.connection_failed, Toast.LENGTH_SHORT).show()
}
}
override fun onPasswordSuccess(password: String) {
val walletFile = File(applicationInfo.dataDir, Constants.WALLET_NAME)
init(walletFile, password)
restartEvents.call()
if (proceedToSend) {
val sendDialog = SendBottomSheetDialog()
sendDialog.uriData = uriData
sendDialog.show(supportFragmentManager, null)
}
}
override fun onPasswordFail() {
runOnUiThread {
Toast.makeText(application, R.string.bad_password, Toast.LENGTH_SHORT).show()
}
}
}

View file

@ -0,0 +1,608 @@
package net.mynero.wallet
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.Button
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.google.android.material.progressindicator.CircularProgressIndicator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.mynero.wallet.data.Node
import net.mynero.wallet.fragment.dialog.AddNodeBottomSheetDialog
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog
import net.mynero.wallet.livedata.combineLiveDatas
import net.mynero.wallet.model.EnumTorState
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.MoneroHandlerThread
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
import java.util.Calendar
class OnboardingActivity : AppCompatActivity() {
private lateinit var mViewModel: OnboardingViewModel
private lateinit var walletProxyAddressEditText: EditText
private lateinit var walletProxyPortEditText: EditText
private lateinit var walletPasswordEditText: EditText
private lateinit var walletPasswordConfirmEditText: EditText
private lateinit var walletSeedEditText: EditText
private lateinit var walletRestoreHeightEditText: EditText
private lateinit var createWalletButton: Button
private lateinit var moreOptionsDropdownTextView: TextView
private lateinit var torSwitch: SwitchCompat
private lateinit var advancedOptionsLayout: ConstraintLayout
private lateinit var moreOptionsChevronImageView: ImageView
private lateinit var seedOffsetCheckbox: CheckBox
private lateinit var selectNodeButton: Button
private lateinit var showXmrchanSwitch: SwitchCompat
private lateinit var xmrchanOnboardingImage: ImageView
private lateinit var seedTypeButton: Button
private lateinit var seedTypeDescTextView: TextView
private lateinit var useBundledTor: CheckBox
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
mViewModel = ViewModelProvider(this)[OnboardingViewModel::class.java]
selectNodeButton = findViewById(R.id.select_node_button)
walletPasswordEditText = findViewById(R.id.wallet_password_edittext)
walletPasswordConfirmEditText = findViewById(R.id.wallet_password_confirm_edittext)
walletSeedEditText = findViewById(R.id.wallet_seed_edittext)
walletRestoreHeightEditText = findViewById(R.id.wallet_restore_height_edittext)
createWalletButton = findViewById(R.id.create_wallet_button)
moreOptionsDropdownTextView = findViewById(R.id.advanced_settings_dropdown_textview)
moreOptionsChevronImageView = findViewById(R.id.advanced_settings_chevron_imageview)
torSwitch = findViewById(R.id.tor_onboarding_switch)
seedOffsetCheckbox = findViewById(R.id.seed_offset_checkbox)
walletProxyAddressEditText = findViewById(R.id.wallet_proxy_address_edittext)
walletProxyPortEditText = findViewById(R.id.wallet_proxy_port_edittext)
advancedOptionsLayout = findViewById(R.id.more_options_layout)
showXmrchanSwitch = findViewById(R.id.show_xmrchan_switch)
xmrchanOnboardingImage = findViewById(R.id.xmrchan_onboarding_imageview)
seedTypeButton = findViewById(R.id.seed_type_button)
seedTypeDescTextView = findViewById(R.id.seed_type_desc_textview)
useBundledTor = findViewById(R.id.bundled_tor_checkbox)
seedOffsetCheckbox.isChecked = mViewModel.useOffset
val usingProxy = ProxyService.instance?.usingProxy == true
val usingBundledTor = ProxyService.instance?.useBundledTor == true
torSwitch.isChecked = usingProxy
useBundledTor.isChecked = usingBundledTor
useBundledTor.isEnabled = usingProxy
walletProxyAddressEditText.isEnabled = usingProxy && !usingBundledTor
walletProxyPortEditText.isEnabled = usingProxy && !usingBundledTor
val node = PrefService.instance?.node // should be using default here
selectNodeButton.text = getString(R.string.node_button_text, node?.address)
bindListeners()
bindObservers()
}
private fun bindObservers() {
mViewModel.passphrase.observe(this) { text ->
if (text.isEmpty()) {
walletPasswordConfirmEditText.text = null
walletPasswordConfirmEditText.visibility = View.GONE
} else {
walletPasswordConfirmEditText.visibility = View.VISIBLE
}
}
mViewModel.showMoreOptions.observe(this) { show: Boolean ->
if (show) {
moreOptionsChevronImageView.setImageResource(R.drawable.ic_keyboard_arrow_up)
advancedOptionsLayout.visibility = View.VISIBLE
} else {
moreOptionsChevronImageView.setImageResource(R.drawable.ic_keyboard_arrow_down)
advancedOptionsLayout.visibility = View.GONE
}
}
mViewModel.enableButton.observe(this) { enable: Boolean ->
createWalletButton.isEnabled = enable
}
mViewModel.seedType.observe(this) { seedType: OnboardingViewModel.SeedType ->
seedTypeButton.text = seedType.toString()
seedTypeDescTextView.text = getText(seedType.descResId)
if (seedType == OnboardingViewModel.SeedType.LEGACY) {
seedOffsetCheckbox.visibility = View.VISIBLE
walletRestoreHeightEditText.visibility = View.VISIBLE
walletPasswordEditText.hint = getString(R.string.password_optional)
walletSeedEditText.hint = getString(R.string.recovery_phrase_optional_legacy)
} else {
seedOffsetCheckbox.visibility = View.GONE
walletRestoreHeightEditText.visibility = View.GONE
walletPasswordEditText.hint = getString(R.string.password_non_optional)
walletSeedEditText.hint = getString(R.string.recovery_phrase_optional_polyseed)
}
}
mViewModel.showMonerochan.observe(this) {
if (it) {
xmrchanOnboardingImage.visibility = View.VISIBLE
} else {
xmrchanOnboardingImage.visibility = View.GONE
}
}
mViewModel.useBundledTor.observe(this) { isChecked ->
walletProxyPortEditText.isEnabled = !isChecked && mViewModel.useProxy.value == true
walletProxyAddressEditText.isEnabled = !isChecked && mViewModel.useProxy.value == true
}
mViewModel.useProxy.observe(this) { useProxy ->
useBundledTor.isEnabled = useProxy
walletProxyAddressEditText.isEnabled = useProxy && mViewModel.useBundledTor.value == false
walletProxyPortEditText.isEnabled = useProxy && mViewModel.useBundledTor.value == false
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
val indicatorCircle = findViewById<CircularProgressIndicator>(R.id.onboarding_tor_loading_progressindicator)
val torIcon = findViewById<ImageView>(R.id.onboarding_tor_icon)
samouraiTorManager?.getTorStateLiveData()?.observe(this) { state ->
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if (socketAddress.toString().isEmpty()) return@let
if (mViewModel.useProxy.value == true && mViewModel.useBundledTor.value == true) {
torIcon?.visibility = View.VISIBLE
indicatorCircle?.visibility = View.INVISIBLE
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
updateProxy(address, port)
}
}
indicatorCircle.isIndeterminate = state.progressIndicator == 0
indicatorCircle.progress = state.progressIndicator
when (state.state) {
EnumTorState.OFF -> {
torIcon.visibility = View.INVISIBLE
indicatorCircle.visibility = View.INVISIBLE
}
EnumTorState.STARTING, EnumTorState.STOPPING -> {
torIcon.visibility = View.INVISIBLE
indicatorCircle.visibility = View.VISIBLE
}
else -> {}
}
}
}
private fun updateProxy(address: String, port: String) {
walletProxyPortEditText.setText(port)
walletProxyAddressEditText.setText(address)
}
private fun bindListeners() {
// Disable onBack click
val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
moreOptionsDropdownTextView.setOnClickListener { mViewModel.onMoreOptionsClicked() }
moreOptionsChevronImageView.setOnClickListener { mViewModel.onMoreOptionsClicked() }
seedOffsetCheckbox.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
mViewModel.useOffset = b
}
createWalletButton.setOnClickListener {
onBackPressedCallback.isEnabled = false
createOrImportWallet(
walletSeedEditText.text.toString().trim { it <= ' ' },
walletRestoreHeightEditText.text.toString().trim { it <= ' ' }
)
}
walletPasswordEditText.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) {
mViewModel.setPassphrase(editable.toString())
}
})
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) {
mViewModel.setConfirmedPassphrase(editable.toString())
}
})
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) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
if (text.isEmpty()) {
createWalletButton.setText(R.string.create_wallet)
} else {
createWalletButton.setText(R.string.menu_restore)
}
}
})
seedTypeButton.setOnClickListener { toggleSeedType() }
torSwitch.setOnCheckedChangeListener { _, b: Boolean ->
mViewModel.setUseProxy(b)
}
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.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
mViewModel.setMonerochan(b)
}
selectNodeButton.setOnClickListener {
val activity = this
supportFragmentManager.let { fragmentManager ->
val dialog = NodeSelectionBottomSheetDialog()
dialog.listener = object : NodeSelectionBottomSheetDialog.NodeSelectionDialogListener {
override fun onNodeSelected() {
val node = PrefService.instance?.node
selectNodeButton.text = getString(R.string.node_button_text, node?.address)
Toast.makeText(
activity,
getString(R.string.node_selected, node?.name ?: node?.host),
Toast.LENGTH_SHORT
).show()
}
override fun onClickedEditNode(node: Node?) {}
override fun onClickedAddNode() {}
}
dialog.show(fragmentManager, "node_selection_dialog")
}
}
useBundledTor.setOnCheckedChangeListener { _, isChecked ->
mViewModel.setUseBundledTor(isChecked)
}
}
private fun toggleSeedType() {
val seedType = mViewModel.seedType.value ?: return
var newSeedType = OnboardingViewModel.SeedType.UNKNOWN
if (seedType == OnboardingViewModel.SeedType.POLYSEED) {
newSeedType = OnboardingViewModel.SeedType.LEGACY
} else if (seedType == OnboardingViewModel.SeedType.LEGACY) {
newSeedType = OnboardingViewModel.SeedType.POLYSEED
}
mViewModel.setSeedType(newSeedType)
}
private fun createOrImportWallet(
walletSeed: String,
restoreHeightText: String
) {
this.let { act ->
lifecycleScope.launch(Dispatchers.IO) {
mViewModel.createOrImportWallet(
act,
walletSeed,
restoreHeightText,
mViewModel.useOffset,
applicationContext
)
}
}
}
}
internal class OnboardingViewModel : ViewModel() {
var useOffset = true
private val _showMoreOptions = MutableLiveData(false)
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("")
val passphrase: LiveData<String> = _passphrase
private val _confirmedPassphrase = MutableLiveData("")
private val _showMonerochan = MutableLiveData(Constants.DEFAULT_PREF_MONEROCHAN)
val showMonerochan: LiveData<Boolean> = _showMonerochan
var showMoreOptions: LiveData<Boolean> = _showMoreOptions
var seedType: LiveData<SeedType> = _seedType
init {
_useProxy.value = ProxyService.instance?.usingProxy
_useBundledTor.value = ProxyService.instance?.useBundledTor
}
val enableButton = combineLiveDatas(
seedType,
_useProxy,
_proxyAddress,
_proxyPort,
_useBundledTor,
_passphrase,
_confirmedPassphrase,
_creatingWallet,
ProxyService.instance?.samouraiTorManager?.getTorStateLiveData()
) { seedType, useProxy, proxyAddress, proxyPort, useBundledTor, passphrase, confirmedPassphrase, creatingWallet, torState ->
if (seedType == null || useProxy == null || proxyAddress == null || proxyPort == null || useBundledTor == null || passphrase == null || confirmedPassphrase == null || creatingWallet == null) return@combineLiveDatas false
if ((passphrase.isNotEmpty() || confirmedPassphrase.isNotEmpty()) && passphrase != confirmedPassphrase) return@combineLiveDatas false
if (creatingWallet) return@combineLiveDatas false
if (seedType == SeedType.POLYSEED && (passphrase.isEmpty() || confirmedPassphrase.isEmpty())) return@combineLiveDatas false
if (useProxy && (proxyAddress.isEmpty() || proxyPort.isEmpty()) && !useBundledTor) return@combineLiveDatas false
val progress = torState?.progressIndicator ?: 0
if (useBundledTor && progress < 100 && useProxy) return@combineLiveDatas false
return@combineLiveDatas true
}
fun onMoreOptionsClicked() {
val currentValue = showMoreOptions.value ?: false
val newValue = !currentValue
_showMoreOptions.value = newValue
}
fun setSeedType(seedType: SeedType?) {
_seedType.value = seedType
}
fun createOrImportWallet(
mainActivity: Activity,
walletSeed: String,
restoreHeightText: String,
useOffset: Boolean,
context: Context
) {
val passphrase = _passphrase.value ?: return
val confirmedPassphrase = _confirmedPassphrase.value ?: return
val application = mainActivity.application as MoneroApplication
_creatingWallet.postValue(true)
val offset = if (useOffset) confirmedPassphrase else ""
if (passphrase.isNotEmpty()) {
if (passphrase != confirmedPassphrase) {
_creatingWallet.postValue(false)
mainActivity.runOnUiThread {
Toast.makeText(
mainActivity,
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) {
println("KEK")
MoneroHandlerThread.init(walletFile, passphrase, context)
val intent = Intent(mainActivity, HomeActivity::class.java)
mainActivity.startActivity(intent)
mainActivity.finish()
} else {
mainActivity.runOnUiThread {
_creatingWallet.postValue(false)
Toast.makeText(
mainActivity,
application.getString(
R.string.create_wallet_failed,
walletStatus?.errorString
),
Toast.LENGTH_SHORT
).show()
}
}
}
private val newRestoreHeight: Long
get() {
val restoreDate = Calendar.getInstance()
restoreDate.add(Calendar.DAY_OF_MONTH, 0)
return RestoreHeight.instance?.getHeight(restoreDate.time) ?: 0
}
private fun getMnemonicType(seed: String): SeedType {
val words = seed.split("\\s".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val seedTypeValue = seedType.value ?: return SeedType.LEGACY
return if (words.size == 16 && seedTypeValue == SeedType.POLYSEED) {
SeedType.POLYSEED
} else if (words.size == 25 && seedTypeValue == SeedType.LEGACY) {
SeedType.LEGACY
} else {
SeedType.UNKNOWN
}
}
private fun createTempWallet(tmpWalletFile: File): Wallet? {
return WalletManager.instance?.createWallet(
tmpWalletFile,
"",
Constants.MNEMONIC_LANGUAGE,
0
)
}
fun setProxyAddress(address: String) {
_proxyAddress.value = address
if (address.isEmpty()) PrefService.instance?.deleteProxy()
val port = _proxyPort.value ?: return
ProxyService.instance?.updateProxy(address, port)
}
fun setProxyPort(port: String) {
_proxyPort.value = port
if (port.isEmpty()) PrefService.instance?.deleteProxy()
val address = _proxyAddress.value ?: return
ProxyService.instance?.updateProxy(address, port)
}
fun setUseBundledTor(useBundled: Boolean) {
_useBundledTor.value = useBundled
ProxyService.instance?.useBundledTor = useBundled
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (useBundled && ProxyService.instance?.usingProxy == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
fun setUseProxy(useProxy: Boolean) {
_useProxy.value = useProxy
ProxyService.instance?.usingProxy = useProxy
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (useProxy && ProxyService.instance?.useBundledTor == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
fun setPassphrase(passphrase: String) {
_passphrase.value = passphrase
}
fun setConfirmedPassphrase(confirmedPassphrase: String) {
_confirmedPassphrase.value = confirmedPassphrase
}
fun setMonerochan(b: Boolean) {
_showMonerochan.value = b
PrefService.instance?.edit()?.putBoolean(Constants.PREF_MONEROCHAN, b)?.apply()
}
enum class SeedType(val descResId: Int) {
LEGACY(R.string.seed_desc_legacy), POLYSEED(R.string.seed_desc_polyseed), UNKNOWN(0)
}
}

View file

@ -0,0 +1,64 @@
package net.mynero.wallet
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Constants
import java.io.File
// Shows a password prompt
// Finishes and returns the wallet's name and password in extra when user enters valid password
class PasswordActivity : AppCompatActivity() {
private var preventGoingBack = false
private lateinit var passwordEdittext: EditText
private lateinit var unlockButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_password)
preventGoingBack = intent.getBooleanExtra(Constants.EXTRA_PREVENT_GOING_BACK, false)
passwordEdittext = findViewById(R.id.wallet_password_edittext)
unlockButton = findViewById(R.id.unlock_wallet_button)
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (!preventGoingBack) {
finish()
}
}
})
val walletFile = File(applicationInfo.dataDir, Constants.WALLET_NAME)
unlockButton.setOnClickListener {
onUnlockClicked(walletFile, passwordEdittext.text.toString())
}
}
private fun onUnlockClicked(walletFile: File, password: String) {
if (checkPassword(walletFile, password)) {
val intent = Intent()
intent.putExtra(Constants.EXTRA_WALLET_NAME, walletFile.name)
intent.putExtra(Constants.EXTRA_WALLET_PASSWORD, password)
setResult(RESULT_OK, intent)
finish()
} else {
Toast.makeText(application, R.string.bad_password, Toast.LENGTH_SHORT).show()
}
}
private fun checkPassword(walletFile: File, password: String): Boolean {
return WalletManager.instance?.verifyWalletPasswordOnly(
walletFile.absolutePath + ".keys",
password
) == true
}
}

View file

@ -1,16 +1,16 @@
package net.mynero.wallet.fragment.receive
package net.mynero.wallet
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -19,51 +19,49 @@ import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.journeyapps.barcodescanner.BarcodeEncoder
import net.mynero.wallet.R
import net.mynero.wallet.adapter.SubaddressAdapter
import net.mynero.wallet.adapter.SubaddressAdapter.SubaddressAdapterListener
import net.mynero.wallet.data.Subaddress
import net.mynero.wallet.fragment.dialog.EditAddressLabelBottomSheetDialog
import net.mynero.wallet.fragment.dialog.EditAddressLabelBottomSheetDialog.LabelListener
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.AddressService
import net.mynero.wallet.util.Helper.clipBoardCopy
import java.nio.charset.StandardCharsets
import java.util.Collections
import java.util.EnumMap
class ReceiveFragment : Fragment() {
private var addressTextView: TextView? = null
private var addressLabelTextView: TextView? = null
private var addressImageView: ImageView? = null
private var copyAddressImageButton: ImageButton? = null
private var mViewModel: ReceiveViewModel? = null
class ReceiveActivity : AppCompatActivity() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_receive, container, false)
}
private lateinit var mViewModel: ReceiveViewModel
private lateinit var addressTextView: TextView
private lateinit var addressLabelTextView: TextView
private lateinit var addressImageView: ImageView
private lateinit var copyAddressImageButton: ImageButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_receive)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel = ViewModelProvider(this)[ReceiveViewModel::class.java]
addressImageView = view.findViewById(R.id.monero_qr_imageview)
addressTextView = view.findViewById(R.id.address_textview)
addressLabelTextView = view.findViewById(R.id.address_label_textview)
copyAddressImageButton = view.findViewById(R.id.copy_address_imagebutton)
bindListeners(view)
bindObservers(view)
mViewModel?.init()
addressImageView = findViewById(R.id.monero_qr_imageview)
addressTextView = findViewById(R.id.address_textview)
addressLabelTextView = findViewById(R.id.address_label_textview)
copyAddressImageButton = findViewById(R.id.copy_address_imagebutton)
bindListeners()
bindObservers()
mViewModel.init()
}
private fun bindListeners(view: View) {
val freshAddressImageView = view.findViewById<ImageButton>(R.id.fresh_address_imageview)
freshAddressImageView.setOnClickListener { mViewModel?.freshSubaddress }
private fun bindListeners() {
val freshAddressImageView = findViewById<ImageButton>(R.id.fresh_address_imageview)
freshAddressImageView.setOnClickListener { mViewModel.freshSubaddress }
}
private fun bindObservers(view: View) {
private fun bindObservers() {
val subaddressAdapterListener = object : SubaddressAdapterListener {
override fun onSubaddressSelected(subaddress: Subaddress?) {
mViewModel?.selectAddress(subaddress)
mViewModel.selectAddress(subaddress)
}
override fun onSubaddressEditLabel(subaddress: Subaddress?) {
@ -71,14 +69,14 @@ class ReceiveFragment : Fragment() {
}
}
val adapter = SubaddressAdapter(emptyList(), null, subaddressAdapterListener)
val recyclerView = view.findViewById<RecyclerView>(R.id.address_list_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(activity)
val recyclerView = findViewById<RecyclerView>(R.id.address_list_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
mViewModel?.address?.observe(viewLifecycleOwner) { address: Subaddress? ->
mViewModel.address.observe(this) { address: Subaddress? ->
setAddress(address)
adapter.submitSelectedAddress(address)
}
mViewModel?.addresses?.observe(viewLifecycleOwner) { addresses: List<Subaddress> ->
mViewModel.addresses.observe(this) { addresses: List<Subaddress> ->
// We want newer addresses addresses to be shown first
adapter.submitAddresses(addresses.reversed())
}
@ -89,31 +87,31 @@ class ReceiveFragment : Fragment() {
dialog.addressIndex = subaddress?.addressIndex ?: return
dialog.listener = object : LabelListener {
override fun onDismiss() {
mViewModel?.init()
mViewModel.init()
}
}
dialog.show(parentFragmentManager, "edit_address_dialog")
dialog.show(supportFragmentManager, "edit_address_dialog")
}
private fun setAddress(subaddress: Subaddress?) {
val label = subaddress?.displayLabel
val address = context?.getString(
val address = getString(
R.string.subbaddress_info_subtitle,
subaddress?.addressIndex, subaddress?.squashedAddress
)
addressLabelTextView?.text = if (label?.isEmpty() == true) address else label
addressTextView?.text = subaddress?.address
addressImageView?.setImageBitmap(subaddress?.address?.let { generate(it, 256, 256) })
copyAddressImageButton?.setOnClickListener {
addressLabelTextView.text = if (label?.isEmpty() == true) address else label
addressTextView.text = subaddress?.address
addressImageView.setImageBitmap(subaddress?.address?.let { generate(it, 256, 256) })
copyAddressImageButton.setOnClickListener {
clipBoardCopy(
context, "address", subaddress?.address
this, "address", subaddress?.address
)
}
addressLabelTextView?.setOnLongClickListener {
addressLabelTextView.setOnLongClickListener {
editAddressLabel(subaddress)
true
}
addressTextView?.setOnLongClickListener {
addressTextView.setOnLongClickListener {
editAddressLabel(subaddress)
true
}
@ -135,10 +133,7 @@ class ReceiveFragment : Fragment() {
if (bitMatrix[j, i]) {
pixels[i * width + j] = -0x1
} else {
context?.let { ctx ->
pixels[i * height + j] =
ContextCompat.getColor(ctx, R.color.oled_colorBackground)
}
pixels[i * height + j] = ContextCompat.getColor(this, R.color.oled_colorBackground)
}
}
}
@ -149,3 +144,36 @@ class ReceiveFragment : Fragment() {
return null
}
}
class ReceiveViewModel : ViewModel() {
private val _address = MutableLiveData<Subaddress?>()
private val _addresses = MutableLiveData<List<Subaddress>>()
val address: LiveData<Subaddress?> = _address
val addresses: LiveData<List<Subaddress>> = _addresses
fun init() {
_addresses.value = subaddresses
_address.value = addresses.value?.lastOrNull()
}
private val subaddresses: List<Subaddress>
get() {
val wallet = WalletManager.instance?.wallet
val subaddresses = ArrayList<Subaddress>()
val numAddresses = AddressService.instance?.numAddresses ?: 1
for (i in 0 until numAddresses) {
wallet?.getSubaddressObject(i)?.let { subaddresses.add(it) }
}
return Collections.unmodifiableList(subaddresses)
}
val freshSubaddress: Unit
get() {
_address.value = AddressService.instance?.freshSubaddress()
_addresses.value = subaddresses
}
fun selectAddress(subaddress: Subaddress?) {
_address.value = subaddress
}
}

View file

@ -0,0 +1,596 @@
package net.mynero.wallet
import android.app.Activity
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.RadioGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.google.zxing.client.android.Intents
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanIntentResult
import com.journeyapps.barcodescanner.ScanOptions
import com.ncorti.slidetoact.SlideToActView
import com.ncorti.slidetoact.SlideToActView.OnSlideCompleteListener
import net.mynero.wallet.model.PendingTransaction
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.service.BalanceService
import net.mynero.wallet.service.TxService
import net.mynero.wallet.service.UTXOService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Helper
import net.mynero.wallet.util.UriData
class SendActivity : AppCompatActivity() {
var priority: PendingTransaction.Priority = PendingTransaction.Priority.Priority_Low
private lateinit var mViewModel: SendViewModel
private lateinit var sendMaxButton: Button
private lateinit var addOutputImageView: ImageButton
private lateinit var destList: LinearLayout
private lateinit var createButton: Button
private lateinit var sendTxSlider: SlideToActView
private lateinit var feeRadioGroup: RadioGroup
private lateinit var feeRadioGroupLabelTextView: TextView
private lateinit var feeTextView: TextView
private lateinit var addressTextView: TextView
private lateinit var amountTextView: TextView
private lateinit var selectedUtxosValueTextView: TextView
private var currentEntryIndex = -1
private val qrCodeLauncher =
registerForActivityResult(ScanContract()) { result: ScanIntentResult ->
if (result.contents != null) {
if (currentEntryIndex != -1) {
pasteAddress(getDestView(currentEntryIndex), result.contents, false)
currentEntryIndex = -1
}
}
}
private val cameraPermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted: Boolean ->
if (granted) {
onScan(currentEntryIndex)
} else {
Toast.makeText(this, getString(R.string.no_camera_permission), Toast.LENGTH_SHORT)
.show()
currentEntryIndex = -1
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_send)
mViewModel = ViewModelProvider(this)[SendViewModel::class.java]
sendMaxButton = findViewById(R.id.send_max_button)
addOutputImageView = findViewById(R.id.add_output_button)
destList = findViewById(R.id.transaction_destination_list)
createButton = findViewById(R.id.create_tx_button)
feeRadioGroup = findViewById(R.id.tx_fee_radiogroup)
feeTextView = findViewById(R.id.fee_textview)
sendTxSlider = findViewById(R.id.send_tx_slider)
addressTextView = findViewById(R.id.address_pending_textview)
amountTextView = findViewById(R.id.amount_pending_textview)
feeRadioGroup = findViewById(R.id.tx_fee_radiogroup)
feeRadioGroupLabelTextView = findViewById(R.id.tx_fee_radiogroup_label_textview)
selectedUtxosValueTextView = findViewById(R.id.selected_utxos_value_textview)
bindListeners()
bindObservers()
init()
val selectedUtxos = mViewModel.utxos.value
if (selectedUtxos?.isNotEmpty() == true) {
var selectedValue: Long = 0
val utxos = UTXOService.instance?.getUtxos() ?: return
for (coinsInfo in utxos) {
if (selectedUtxos.contains(coinsInfo.keyImage)) {
selectedValue += coinsInfo.amount
}
}
val valueString = Wallet.getDisplayAmount(selectedValue)
selectedUtxosValueTextView.visibility = View.VISIBLE
selectedUtxosValueTextView.text = resources.getString(
R.string.selected_utxos_value,
valueString
)
} else {
selectedUtxosValueTextView.visibility = View.GONE
}
}
private fun init() {
val address = intent.getStringExtra(Constants.EXTRA_SEND_ADDRESS)
val amount = intent.getStringExtra(Constants.EXTRA_SEND_AMOUNT)
val max = intent.getBooleanExtra(Constants.EXTRA_SEND_MAX, false)
val utxos = intent.getStringArrayListExtra(Constants.EXTRA_SEND_UTXOS) ?: ArrayList()
addOutput(true, address, amount)
mViewModel.setSendingMax(max)
mViewModel.setUtxos(utxos)
}
private fun bindListeners() {
feeRadioGroup.check(R.id.low_fee_radiobutton)
priority = PendingTransaction.Priority.Priority_Low
feeRadioGroup.setOnCheckedChangeListener { _: RadioGroup?, i: Int ->
when (i) {
R.id.low_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_Low
R.id.med_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_Medium
R.id.high_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_High
}
}
addOutputImageView.setOnClickListener {
sendMaxButton.visibility = View.GONE
val outputCount = destCount
if (outputCount < 8) {
addOutput(false)
} else {
Toast.makeText(
this,
getString(R.string.max_outputs_allowed),
Toast.LENGTH_SHORT
).show()
}
}
sendMaxButton.setOnClickListener { mViewModel.setSendingMax(!isSendAll) }
createButton.setOnClickListener {
val outputsValid = checkDestsValidity(isSendAll)
if (outputsValid) {
Toast.makeText(this, getString(R.string.creating_tx), Toast.LENGTH_SHORT).show()
createButton.isEnabled = false
sendMaxButton.isEnabled = false
createTx(rawDests, isSendAll, priority, mViewModel.utxos.value ?: ArrayList())
} else {
Toast.makeText(
this,
getString(R.string.creating_tx_failed_invalid_outputs),
Toast.LENGTH_SHORT
).show()
}
}
sendTxSlider.onSlideCompleteListener =
object : OnSlideCompleteListener {
override fun onSlideComplete(view: SlideToActView) {
confirmSlider()
}
}
sendTxSlider.let { slideToActView ->
ViewCompat.addAccessibilityAction(
slideToActView,
getString(R.string.approve_the_transaction)
) { _, _ ->
confirmSlider()
return@addAccessibilityAction true
}
}
}
private fun confirmSlider() {
val pendingTx = mViewModel.pendingTransaction.value ?: return
Toast.makeText(this, getString(R.string.sending_tx), Toast.LENGTH_SHORT)
.show()
sendTx(pendingTx)
}
private fun checkDestsValidity(sendAll: Boolean): Boolean {
val dests = rawDests
for (dest in dests) {
val address = dest.component1()
val amount = dest.component2()
if (!sendAll) {
if (amount.isEmpty()) {
Toast.makeText(
this,
getString(R.string.send_amount_empty),
Toast.LENGTH_SHORT
).show()
return false
}
val amountRaw = Wallet.getAmountFromString(amount)
val balance = BalanceService.instance?.unlockedBalanceRaw ?: 0
if (amountRaw >= balance || amountRaw <= 0) {
Toast.makeText(
this,
getString(R.string.send_amount_invalid),
Toast.LENGTH_SHORT
).show()
return false
}
} else if (dests.size > 1) {
Toast.makeText(
this,
getString(R.string.send_amount_invalid_sendall_paytomany),
Toast.LENGTH_SHORT
).show()
return false
}
val uriData = UriData.parse(address)
val isValidAddress = uriData != null
if (!isValidAddress) {
Toast.makeText(
this,
getString(R.string.send_address_invalid),
Toast.LENGTH_SHORT
).show()
return false
}
if (dests.size > 1 && uriData?.hasPaymentId() == true) {
Toast.makeText(
this,
getString(R.string.paymentid_paytomany),
Toast.LENGTH_SHORT
).show()
return false
}
}
return true
}
private fun destsHasPaymentId(): Boolean {
val dests = rawDests
for (dest in dests) {
val address = dest.component1()
val uriData = UriData.parse(address) ?: return false
if (uriData.hasPaymentId()) return true
}
return false
}
private fun bindObservers() {
mViewModel.sendingMax.observe(this) { sendingMax: Boolean? ->
if (mViewModel.pendingTransaction.value == null) {
if (sendingMax == true) {
prepareOutputsForMaxSend()
sendMaxButton.text = getText(R.string.undo)
} else {
unprepareMaxSend()
sendMaxButton.text = getText(R.string.send_max)
}
}
}
mViewModel.showAddOutputButton.observe(this) { show: Boolean? ->
setAddOutputButtonVisibility(
if (show == true && !destsHasPaymentId()) View.VISIBLE else View.INVISIBLE
)
}
mViewModel.pendingTransaction.observe(this) { pendingTx: PendingTransaction? ->
showConfirmationLayout(pendingTx != null)
if (pendingTx != null) {
val address = if (destCount == 1) getAddressField(0).text.toString() else "Multiple"
addressTextView.text = getString(R.string.tx_address_text, address)
amountTextView.text =
getString(
R.string.tx_amount_text,
Helper.getDisplayAmount(pendingTx.getAmount())
)
feeTextView.text =
getString(R.string.tx_fee_text, Helper.getDisplayAmount(pendingTx.getFee()))
}
}
}
private fun addOutput(initial: Boolean, address: String? = null, amount: String? = null) {
val index = destCount
val entryView =
layoutInflater.inflate(R.layout.transaction_output_item, null) as ConstraintLayout
val removeOutputImageButton =
entryView.findViewById<ImageButton>(R.id.remove_output_imagebutton)
val addressField = entryView.findViewById<EditText>(R.id.address_edittext)
val amountField = entryView.findViewById<EditText>(R.id.amount_edittext)
val donateTextView = entryView.findViewById<TextView>(R.id.donate_label)
val donatingTextView = entryView.findViewById<TextView>(R.id.donating_label)
donateTextView.setOnClickListener {
addressField.setText(
Constants.DONATE_ADDRESS
)
}
donatingTextView.setOnClickListener {
addressField.setText("")
}
address?.let { addressField.setText(it) }
amount?.let { amountField.setText(it) }
val activity = this
addressField.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
val currentOutputs: Int = destCount
val uriData = UriData.parse(s.toString())
if (uriData != null) {
// we have valid address
val hasPaymentId = uriData.hasPaymentId()
if (currentOutputs > 1 && hasPaymentId) {
// multiple outputs when pasting/editing in integrated address. this is not allowed
Toast.makeText(
activity,
getString(R.string.paymentid_paytomany),
Toast.LENGTH_SHORT
).show()
addressField.text = null
} else if (currentOutputs == 1 && hasPaymentId) {
// show add output button: we are sending to integrated address
mViewModel.setShowAddOutputButton(false)
}
} else if (currentOutputs == 1 && !isSendAll) {
// when send-all is false and this is our only dest and address is invalid, then show add output button
mViewModel.setShowAddOutputButton(true)
}
if (s.toString() == Constants.DONATE_ADDRESS) {
donateTextView.visibility = View.INVISIBLE
donatingTextView.visibility = View.VISIBLE
} else {
donateTextView.visibility = View.VISIBLE
donatingTextView.visibility = View.INVISIBLE
}
}
})
entryView.findViewById<View>(R.id.paste_amount_imagebutton)
.setOnClickListener {
val clipboard = Helper.getClipBoardText(this)
if (clipboard != null) {
pasteAddress(entryView, clipboard, true)
}
}
entryView.findViewById<View>(R.id.paste_address_imagebutton)
.setOnClickListener {
val clipboard = Helper.getClipBoardText(this)
if (clipboard != null) {
pasteAddress(entryView, clipboard, false)
}
}
entryView.findViewById<View>(R.id.scan_address_imagebutton)
.setOnClickListener { onScan(index) }
if (initial) {
removeOutputImageButton.visibility = View.INVISIBLE
} else {
removeOutputImageButton.setOnClickListener {
val currentCount = destCount
if (currentCount > 1) {
if (currentCount == 2) {
sendMaxButton.visibility = View.VISIBLE
}
destList.removeView(entryView)
}
}
}
destList.addView(entryView)
}
private val destCount: Int
get() = destList.childCount ?: -1
private val rawDests: List<Pair<String, String>>
get() {
val dests = ArrayList<Pair<String, String>>()
for (i in 0 until destCount) {
val entryView = getDestView(i)
val amountField = entryView.findViewById<EditText>(R.id.amount_edittext)
val addressField = entryView.findViewById<EditText>(R.id.address_edittext)
val amount = amountField.text.toString().trim { it <= ' ' }
val address = addressField.text.toString().trim { it <= ' ' }
dests.add(Pair(address, amount))
}
return dests
}
private val isSendAll: Boolean
get() = mViewModel.sendingMax.value ?: false
private fun getDestView(pos: Int): ConstraintLayout {
return destList.getChildAt(pos) as ConstraintLayout
}
private fun getAddressField(pos: Int): EditText {
return getDestView(pos).findViewById<View>(R.id.address_edittext) as EditText
}
private fun unprepareMaxSend() {
val entryView = getDestView(0)
entryView.findViewById<View>(R.id.sending_all_textview).visibility = View.INVISIBLE
entryView.findViewById<View>(R.id.amount_edittext).visibility =
View.VISIBLE
}
private fun prepareOutputsForMaxSend() {
val entryView = getDestView(0)
entryView.findViewById<View>(R.id.sending_all_textview).visibility = View.VISIBLE
entryView.findViewById<View>(R.id.amount_edittext).visibility =
View.INVISIBLE
}
private fun showConfirmationLayout(show: Boolean) {
if (show) {
destList.visibility = View.GONE
setAddOutputButtonVisibility(View.GONE)
sendMaxButton.visibility = View.GONE
createButton.visibility = View.GONE
feeRadioGroup.visibility = View.GONE
feeRadioGroupLabelTextView.visibility = View.GONE
sendTxSlider.visibility = View.VISIBLE
feeTextView.visibility = View.VISIBLE
addressTextView.visibility = View.VISIBLE
amountTextView.visibility = View.VISIBLE
} else {
destList.visibility = View.VISIBLE
setAddOutputButtonVisibility(View.VISIBLE)
sendMaxButton.visibility = View.VISIBLE
createButton.visibility = View.VISIBLE
feeRadioGroup.visibility = View.VISIBLE
feeRadioGroupLabelTextView.visibility = View.VISIBLE
sendTxSlider.visibility = View.GONE
feeTextView.visibility = View.GONE
addressTextView.visibility = View.GONE
amountTextView.visibility = View.GONE
}
}
private fun onScan(index: Int) {
currentEntryIndex = index
if (Helper.getCameraPermission(this, cameraPermissionsLauncher)) {
val options = ScanOptions()
options.setBeepEnabled(false)
options.setOrientationLocked(true)
options.setDesiredBarcodeFormats(listOf(Intents.Scan.QR_CODE_MODE))
options.addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN)
qrCodeLauncher.launch(options)
}
}
private fun pasteAddress(
entryView: ConstraintLayout,
clipboard: String,
pastingAmount: Boolean
) {
if (pastingAmount) {
try {
clipboard.toDouble()
setAmount(entryView, clipboard)
} catch (e: Exception) {
Toast.makeText(
this,
getString(R.string.send_amount_invalid),
Toast.LENGTH_SHORT
).show()
return
}
}
val uriData = UriData.parse(clipboard)
if (uriData != null) {
val currentOutputs = destCount
if (currentOutputs > 1 && uriData.hasPaymentId()) {
Toast.makeText(
this,
getString(R.string.paymentid_paytomany),
Toast.LENGTH_SHORT
).show()
return
} else if (currentOutputs == 1 && uriData.hasPaymentId()) {
mViewModel.setShowAddOutputButton(false)
}
val addressField = entryView.findViewById<EditText>(R.id.address_edittext)
addressField.setText(uriData.address)
if (uriData.hasAmount()) {
setAmount(entryView, uriData.amount)
}
} else {
Toast.makeText(this, getString(R.string.send_address_invalid), Toast.LENGTH_SHORT)
.show()
}
}
private fun setAmount(entryView: ConstraintLayout, amount: String?) {
sendMaxButton.isEnabled = false
val amountField = entryView.findViewById<EditText>(R.id.amount_edittext)
amountField.setText(amount)
}
private fun createTx(
dests: List<Pair<String, String>>,
sendAll: Boolean,
feePriority: PendingTransaction.Priority,
utxos: ArrayList<String> = ArrayList()
) {
(application as MoneroApplication).executor?.execute {
try {
val pendingTx =
TxService.instance?.createTx(dests, sendAll, feePriority, utxos)
if (pendingTx != null && pendingTx.status === PendingTransaction.Status.Status_Ok) {
mViewModel.setPendingTransaction(pendingTx)
} else {
val activity: Activity = this
if (pendingTx != null) {
activity.runOnUiThread {
createButton.isEnabled = true
sendMaxButton.isEnabled = true
if (pendingTx.getErrorString() != null) Toast.makeText(
activity,
getString(R.string.error_creating_tx, pendingTx.getErrorString()),
Toast.LENGTH_SHORT
).show()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
val activity: Activity = this
activity.runOnUiThread {
createButton.isEnabled = true
sendMaxButton.isEnabled = true
Toast.makeText(
activity,
getString(R.string.error_creating_tx, e.message),
Toast.LENGTH_SHORT
).show()
}
}
}
}
private fun sendTx(pendingTx: PendingTransaction) {
(application as MoneroApplication).executor?.execute {
val success = TxService.instance?.sendTx(pendingTx)
val activity: Activity = this
activity.runOnUiThread {
if (success == true) {
Toast.makeText(this, getString(R.string.sent_tx), Toast.LENGTH_SHORT)
.show()
activity.onBackPressed()
} else {
sendTxSlider.resetSlider()
Toast.makeText(
this,
getString(R.string.error_sending_tx),
Toast.LENGTH_SHORT
)
.show()
}
}
}
}
private fun setAddOutputButtonVisibility(visibility: Int) {
addOutputImageView.visibility = visibility
}
}
class SendViewModel : ViewModel() {
private val _sendingMax = MutableLiveData(false)
private val _showAddOutputButton = MutableLiveData(true)
private val _utxos = MutableLiveData<ArrayList<String>>(ArrayList())
private val _pendingTransaction = MutableLiveData<PendingTransaction?>(null)
var sendingMax: LiveData<Boolean?> = _sendingMax
var showAddOutputButton: LiveData<Boolean?> = _showAddOutputButton
var utxos: LiveData<ArrayList<String>> = _utxos
var pendingTransaction: LiveData<PendingTransaction?> = _pendingTransaction
fun setSendingMax(value: Boolean) {
_sendingMax.value = value
setShowAddOutputButton(!value)
}
fun setShowAddOutputButton(value: Boolean) {
_showAddOutputButton.value = value
}
fun setUtxos(value: ArrayList<String>) {
_utxos.value = value
}
fun setPendingTransaction(pendingTx: PendingTransaction?) {
_pendingTransaction.postValue(pendingTx)
}
}

View file

@ -0,0 +1,331 @@
package net.mynero.wallet
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.progressindicator.CircularProgressIndicator
import net.mynero.wallet.data.Node
import net.mynero.wallet.data.Node.Companion.fromJson
import net.mynero.wallet.fragment.dialog.AddNodeBottomSheetDialog
import net.mynero.wallet.fragment.dialog.EditNodeBottomSheetDialog
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog
import net.mynero.wallet.fragment.dialog.WalletKeysBottomSheetDialog
import net.mynero.wallet.model.EnumTorState
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.BalanceService
import net.mynero.wallet.service.HistoryService
import net.mynero.wallet.service.MoneroHandlerThread
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.Constants
import org.json.JSONArray
import java.io.File
class SettingsActivity : AppCompatActivity() {
private lateinit var mViewModel: SettingsViewModel
private lateinit var walletProxyAddressEditText: EditText
private lateinit var walletProxyPortEditText: EditText
private lateinit var selectNodeButton: Button
private lateinit var streetModeSwitch: SwitchCompat
private lateinit var monerochanSwitch: SwitchCompat
private lateinit var useBundledTor: CheckBox
private lateinit var displaySeedButton: Button
private lateinit var displayUtxosButton: Button
private lateinit var torSwitch: SwitchCompat
private val askForWalletPasswordAndDisplayWalletKeys = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val password = result.data?.extras?.getString(Constants.EXTRA_WALLET_PASSWORD)
password?.let { displaySeedDialog(it) }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
mViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
displaySeedButton = findViewById(R.id.display_seed_button)
displayUtxosButton = findViewById(R.id.display_utxos_button)
selectNodeButton = findViewById(R.id.select_node_button)
streetModeSwitch = findViewById(R.id.street_mode_switch)
monerochanSwitch = findViewById(R.id.monerochan_switch)
torSwitch = findViewById(R.id.tor_switch)
val proxySettingsLayout = findViewById<ConstraintLayout>(R.id.wallet_proxy_settings_layout)
walletProxyAddressEditText = findViewById(R.id.wallet_proxy_address_edittext)
walletProxyPortEditText = findViewById(R.id.wallet_proxy_port_edittext)
useBundledTor = findViewById(R.id.bundled_tor_checkbox)
val cachedProxyAddress = ProxyService.instance?.proxyAddress ?: return
val cachedProxyPort = ProxyService.instance?.proxyPort ?: return
val cachedUsingProxy = ProxyService.instance?.usingProxy == true
val cachedUsingBundledTor = ProxyService.instance?.useBundledTor == true
walletProxyPortEditText.isEnabled = !cachedUsingBundledTor && cachedUsingProxy
walletProxyAddressEditText.isEnabled = !cachedUsingBundledTor && cachedUsingProxy
proxySettingsLayout.visibility = View.VISIBLE
streetModeSwitch.isChecked =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
monerochanSwitch.isChecked =
PrefService.instance?.getBoolean(Constants.PREF_MONEROCHAN, Constants.DEFAULT_PREF_MONEROCHAN) == true
useBundledTor.isChecked = cachedUsingBundledTor
torSwitch.isChecked = cachedUsingProxy
updateProxy(cachedProxyAddress, cachedProxyPort)
val node = PrefService.instance?.node // shouldn't use default value here
selectNodeButton.text = getString(R.string.node_button_text, node?.address)
bindListeners()
bindObservers()
}
private fun bindListeners() {
val activity = this
val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
refreshProxy()
finish()
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
streetModeSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PrefService.instance?.edit()?.putBoolean(Constants.PREF_STREET_MODE, b)?.apply()
BalanceService.instance?.refreshBalance()
}
monerochanSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PrefService.instance?.edit()?.putBoolean(Constants.PREF_MONEROCHAN, b)?.apply()
HistoryService.instance?.refreshHistory()
}
selectNodeButton.setOnClickListener {
supportFragmentManager.let { fragmentManager ->
val dialog = NodeSelectionBottomSheetDialog()
dialog.listener = object : NodeSelectionBottomSheetDialog.NodeSelectionDialogListener {
override fun onNodeSelected() {
val node = PrefService.instance?.node
selectNodeButton.text = getString(R.string.node_button_text, node?.address)
refreshProxy()
runOnUiThread {
Toast.makeText(
activity,
activity.getString(R.string.node_selected, node?.name ?: node?.host),
Toast.LENGTH_SHORT
).show()
}
}
override fun onClickedEditNode(node: Node?) {
val editNodeDialog = EditNodeBottomSheetDialog()
editNodeDialog.listener = object : EditNodeBottomSheetDialog.EditNodeListener {
override fun onNodeDeleted(node: Node?) {
try {
val nodesArray = PrefService.instance?.getString(Constants.PREF_CUSTOM_NODES, "[]")
val jsonArray = JSONArray(nodesArray)
for (i in 0 until jsonArray.length()) {
val nodeJsonObject = jsonArray.getJSONObject(i)
val savedNode = fromJson(nodeJsonObject)
if (savedNode?.toNodeString() == node?.toNodeString()) jsonArray.remove(i)
}
saveNodesAndReopen(jsonArray)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onNodeEdited(oldNode: Node?, newNode: Node?) {
try {
val nodesArray = PrefService.instance?.getString(Constants.PREF_CUSTOM_NODES, "[]")
val jsonArray = JSONArray(nodesArray)
for (i in 0 until jsonArray.length()) {
val nodeJsonObject = jsonArray.getJSONObject(i)
val savedNode = fromJson(nodeJsonObject)
if (savedNode?.toNodeString() == oldNode?.toNodeString()) jsonArray.put(
i,
newNode?.toJson()
)
}
saveNodesAndReopen(jsonArray)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
editNodeDialog.node = node
editNodeDialog.show(fragmentManager, "edit_node_dialog")
}
override fun onClickedAddNode() {
activity.supportFragmentManager.let { fragmentManager ->
val addNodeDialog = AddNodeBottomSheetDialog()
addNodeDialog.listener = object : AddNodeBottomSheetDialog.AddNodeListener {
override fun onNodeAdded() {}
}
addNodeDialog.show(fragmentManager, "add_node_dialog")
}
}
}
dialog.show(fragmentManager, "node_selection_dialog")
}
}
useBundledTor.setOnCheckedChangeListener { _, isChecked ->
mViewModel.setUseBundledTor(isChecked)
}
displaySeedButton.setOnClickListener {
val usesPassword =
PrefService.instance?.getBoolean(Constants.PREF_USES_PASSWORD, false) == true
if (usesPassword) {
val intent = Intent(this, PasswordActivity::class.java)
askForWalletPasswordAndDisplayWalletKeys.launch(intent)
} else {
displaySeedDialog("")
}
}
displayUtxosButton.setOnClickListener {
startActivity(Intent(this, UtxosActivity::class.java))
}
torSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
mViewModel.setUseProxy(b)
}
}
private fun bindObservers() {
mViewModel.useProxy.observe(this) { useProxy ->
useBundledTor.isEnabled = useProxy
walletProxyPortEditText.isEnabled = useProxy && mViewModel.useBundledTor.value == false
walletProxyAddressEditText.isEnabled = useProxy && mViewModel.useBundledTor.value == false
refreshProxy()
}
mViewModel.useBundledTor.observe(this) { isChecked ->
walletProxyPortEditText.isEnabled = !isChecked && mViewModel.useProxy.value == true
walletProxyAddressEditText.isEnabled = !isChecked && mViewModel.useProxy.value == true
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
val indicatorCircle =
findViewById<CircularProgressIndicator>(R.id.settings_tor_loading_progressindicator)
val torIcon = findViewById<ImageView>(R.id.settings_tor_icon)
samouraiTorManager?.getTorStateLiveData()?.observe(this) { state ->
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if (socketAddress.toString().isEmpty()) return@let
if (mViewModel.useProxy.value == true && mViewModel.useBundledTor.value == true) {
torIcon?.visibility = View.VISIBLE
indicatorCircle?.visibility = View.INVISIBLE
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
updateProxy(address, port)
}
}
indicatorCircle?.isIndeterminate = state.progressIndicator == 0
indicatorCircle?.progress = state.progressIndicator
when (state.state) {
EnumTorState.OFF -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.INVISIBLE
}
EnumTorState.STARTING, EnumTorState.STOPPING -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.VISIBLE
}
else -> {}
}
}
}
private fun updateProxy(address: String, port: String) {
walletProxyPortEditText.setText(port)
walletProxyAddressEditText.setText(address)
refreshProxy()
}
private fun refreshProxy() {
val proxyAddress = walletProxyAddressEditText.text.toString()
val proxyPort = walletProxyPortEditText.text.toString()
val savedProxyAddress = ProxyService.instance?.proxyAddress
val savedProxyPort = ProxyService.instance?.proxyPort
val currentWalletProxy = WalletManager.instance?.proxy
val newProxy = "$proxyAddress:$proxyPort"
if (proxyAddress != savedProxyAddress || proxyPort != savedProxyPort || (newProxy != currentWalletProxy && newProxy != ":"))
ProxyService.instance?.updateProxy(proxyAddress, proxyPort)
}
private fun displaySeedDialog(password: String) {
val informationDialog = WalletKeysBottomSheetDialog()
informationDialog.password = password
informationDialog.show(supportFragmentManager, "information_seed_dialog")
}
private fun saveNodesAndReopen(jsonArray: JSONArray) {
PrefService.instance?.edit()?.putString(Constants.PREF_CUSTOM_NODES, jsonArray.toString())
?.apply()
}
}
class SettingsViewModel : ViewModel() {
private val _useProxy = MutableLiveData(false)
val useProxy: LiveData<Boolean> = _useProxy
private val _useBundledTor = MutableLiveData(false)
val useBundledTor: LiveData<Boolean> = _useBundledTor
init {
_useProxy.value = ProxyService.instance?.usingProxy
_useBundledTor.value = ProxyService.instance?.useBundledTor
}
fun setUseProxy(use: Boolean) {
_useProxy.value = use
ProxyService.instance?.usingProxy = use
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (use && ProxyService.instance?.useBundledTor == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
fun setUseBundledTor(use: Boolean) {
_useBundledTor.value = use
ProxyService.instance?.useBundledTor = use
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (use && ProxyService.instance?.usingProxy == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
}

View file

@ -0,0 +1,75 @@
package net.mynero.wallet
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import net.mynero.wallet.service.MoneroHandlerThread
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.UriData
import java.io.File
class StartActivity : AppCompatActivity() {
private var uriData: UriData? = null
private val startPasswordActivityForOpeningWallet = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val walletName = result.data?.extras?.getString(Constants.EXTRA_WALLET_NAME)
val walletPassword = result.data?.extras?.getString(Constants.EXTRA_WALLET_PASSWORD)
if (walletName != null && walletPassword != null) {
val walletFile = File(applicationInfo.dataDir, walletName)
openWallet(walletFile, walletPassword)
} else {
// if we ever get here, it's a bug ¯\_(ツ)_/¯ so let's just recreate the activity
recreate()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent.data?.let { uriData = UriData.parse(it.toString()) }
val walletFile = File(applicationInfo.dataDir, Constants.WALLET_NAME)
val walletKeysFile = File(applicationInfo.dataDir, Constants.WALLET_NAME + ".keys")
if (walletKeysFile.exists()) {
val usesPassword = PrefService.instance?.getBoolean(Constants.PREF_USES_PASSWORD, false) == true
if (!usesPassword) {
openWallet(walletFile, "")
return
} else {
val intent = Intent(this, PasswordActivity::class.java)
intent.putExtra(Constants.EXTRA_PREVENT_GOING_BACK, true)
startPasswordActivityForOpeningWallet.launch(intent)
return
}
} else {
startActivity(Intent(this, OnboardingActivity::class.java))
finish()
return
}
}
private fun openWallet(walletFile: File, password: String) {
MoneroHandlerThread.init(walletFile, password, applicationContext)
if (uriData == null) {
// the app was NOT started with a monero uri payment data, proceed to the home activity
startActivity(Intent(this, HomeActivity::class.java))
} else {
// the app was started with a monero uri payment data, we proceed to the send activity but launch the home activity as well
// so that when users press back button they go to home activity instead of closing the app
val homeIntent = Intent(this, HomeActivity::class.java)
homeIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
startActivity(homeIntent)
val sendIntent = Intent(this, SendActivity::class.java)
sendIntent.putExtra(Constants.EXTRA_SEND_ADDRESS, uriData!!.address)
uriData!!.amount?.let { sendIntent.putExtra(Constants.EXTRA_SEND_AMOUNT, it) }
startActivity(sendIntent)
}
finish()
}
}

View file

@ -1,4 +1,4 @@
package net.mynero.wallet.fragment.transaction
package net.mynero.wallet
import android.os.Bundle
import android.view.LayoutInflater
@ -6,10 +6,9 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import net.mynero.wallet.R
import net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.HistoryService
@ -21,62 +20,55 @@ import java.util.Calendar
import java.util.Date
import java.util.Objects
class TransactionFragment : Fragment() {
private var mViewModel: TransactionViewModel? = null
class TransactionActivity : AppCompatActivity() {
private var transactionInfo: TransactionInfo? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_transaction, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_transaction)
val cal = Calendar.getInstance()
val tz = cal.timeZone //get the local time zone.
DateHelper.DATETIME_FORMATTER.timeZone = tz
mViewModel = ViewModelProvider(this)[TransactionViewModel::class.java]
val args = arguments
if (args != null) {
transactionInfo = arguments?.getParcelable(Constants.NAV_ARG_TXINFO)
}
bindObservers(view)
bindListeners(view)
transactionInfo = intent.extras?.getParcelable(Constants.NAV_ARG_TXINFO)
bindObservers()
bindListeners()
}
private fun bindListeners(view: View) {
val copyTxHashImageButton = view.findViewById<ImageButton>(R.id.copy_txhash_imagebutton)
private fun bindListeners() {
val copyTxHashImageButton = findViewById<ImageButton>(R.id.copy_txhash_imagebutton)
copyTxHashImageButton.setOnClickListener {
val txInfo = transactionInfo
if (txInfo != null) {
Helper.clipBoardCopy(context, "transaction_hash", txInfo.hash)
Helper.clipBoardCopy(this, "transaction_hash", txInfo.hash)
}
}
val copyTxAddressImageButton =
view.findViewById<ImageButton>(R.id.copy_txaddress_imagebutton)
val addressTextView = view.findViewById<TextView>(R.id.transaction_address_textview)
findViewById<ImageButton>(R.id.copy_txaddress_imagebutton)
val addressTextView = findViewById<TextView>(R.id.transaction_address_textview)
copyTxAddressImageButton.setOnClickListener {
val txInfo = transactionInfo
if (txInfo != null) {
val destination = addressTextView.text.toString()
Helper.clipBoardCopy(context, "transaction_address", destination)
Helper.clipBoardCopy(this, "transaction_address", destination)
}
}
}
private fun bindObservers(view: View) {
val txActionTextView = view.findViewById<TextView>(R.id.transaction_action_textview)
val confLabel2 = view.findViewById<TextView>(R.id.transaction_conf_label2_textview)
val txHashTextView = view.findViewById<TextView>(R.id.transaction_hash_textview)
val txConfTextView = view.findViewById<TextView>(R.id.transaction_conf_textview)
val txAddressTextView = view.findViewById<TextView>(R.id.transaction_address_textview)
private fun bindObservers() {
val txActionTextView = findViewById<TextView>(R.id.transaction_action_textview)
val confLabel2 = findViewById<TextView>(R.id.transaction_conf_label2_textview)
val txHashTextView = findViewById<TextView>(R.id.transaction_hash_textview)
val txConfTextView = findViewById<TextView>(R.id.transaction_conf_textview)
val txAddressTextView = findViewById<TextView>(R.id.transaction_address_textview)
val copyTxAddressImageButton =
view.findViewById<ImageButton>(R.id.copy_txaddress_imagebutton)
val txDateTextView = view.findViewById<TextView>(R.id.transaction_date_textview)
val txAmountTextView = view.findViewById<TextView>(R.id.transaction_amount_textview)
val blockHeightTextView = view.findViewById<TextView>(R.id.tx_block_height_textview)
HistoryService.instance?.history?.observe(viewLifecycleOwner) { transactionInfos: List<TransactionInfo> ->
findViewById<ImageButton>(R.id.copy_txaddress_imagebutton)
val txDateTextView = findViewById<TextView>(R.id.transaction_date_textview)
val txAmountTextView = findViewById<TextView>(R.id.transaction_amount_textview)
val blockHeightTextView = findViewById<TextView>(R.id.tx_block_height_textview)
HistoryService.instance?.history?.observe(this) { transactionInfos: List<TransactionInfo> ->
val newTransactionInfo = findNewestVersionOfTransaction(
transactionInfo, transactionInfos
)
@ -96,34 +88,31 @@ class TransactionFragment : Fragment() {
blockHeightTextView.visibility = View.GONE
confLabel2.text = getString(R.string.transaction_conf_desc2_unconfirmed)
}
val ctx = context
if (ctx != null) {
val streetModeEnabled =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
val balanceString =
if (streetModeEnabled) Constants.STREET_MODE_BALANCE else Helper.getDisplayAmount(
newTransactionInfo.amount,
12
val streetModeEnabled =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
val balanceString =
if (streetModeEnabled) Constants.STREET_MODE_BALANCE else Helper.getDisplayAmount(
newTransactionInfo.amount,
12
)
if (newTransactionInfo.direction === TransactionInfo.Direction.Direction_In) {
txActionTextView.text = getString(R.string.transaction_action_recv)
txAmountTextView.setTextColor(
ContextCompat.getColor(
this,
R.color.oled_positiveColor
)
if (newTransactionInfo.direction === TransactionInfo.Direction.Direction_In) {
txActionTextView.text = getString(R.string.transaction_action_recv)
txAmountTextView.setTextColor(
ContextCompat.getColor(
ctx,
R.color.oled_positiveColor
)
)
} else {
txActionTextView.text = getString(R.string.transaction_action_sent)
txAmountTextView.setTextColor(
ContextCompat.getColor(
this,
R.color.oled_negativeColor
)
} else {
txActionTextView.text = getString(R.string.transaction_action_sent)
txAmountTextView.setTextColor(
ContextCompat.getColor(
ctx,
R.color.oled_negativeColor
)
)
}
txAmountTextView.text = balanceString
)
}
txAmountTextView.text = balanceString
var destination: String? = "-"
val wallet = WalletManager.instance?.wallet
if (newTransactionInfo.txKey == null) {

View file

@ -0,0 +1,145 @@
package net.mynero.wallet
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.adapter.CoinsInfoAdapter
import net.mynero.wallet.model.CoinsInfo
import net.mynero.wallet.service.AddressService
import net.mynero.wallet.service.UTXOService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.MoneroThreadPoolExecutor
class UtxosActivity : AppCompatActivity() {
private lateinit var sendUtxosButton: Button
private lateinit var churnUtxosButton: Button
private lateinit var freezeUtxosButton: Button
private lateinit var adapter: CoinsInfoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_utxos)
freezeUtxosButton = findViewById(R.id.freeze_utxos_button)
sendUtxosButton = findViewById(R.id.send_utxos_button)
churnUtxosButton = findViewById(R.id.churn_utxos_button)
adapter = CoinsInfoAdapter(object : CoinsInfoAdapter.CoinsInfoAdapterListener {
override fun onUtxoSelected(coinsInfo: CoinsInfo) {
val selected = adapter.contains(coinsInfo)
if (selected) {
adapter.deselectUtxo(coinsInfo)
} else {
adapter.selectUtxo(coinsInfo)
}
var frozenExists = false
var unfrozenExists = false
for (selectedUtxo in adapter.selectedUtxos.values) {
if (selectedUtxo.isFrozen || UTXOService.instance?.isCoinFrozen(selectedUtxo) == true)
frozenExists = true
else {
unfrozenExists = true
}
}
val bothExist: Boolean = frozenExists && unfrozenExists
if (adapter.selectedUtxos.isEmpty()) {
sendUtxosButton.visibility = View.GONE
churnUtxosButton.visibility = View.GONE
freezeUtxosButton.visibility = View.GONE
freezeUtxosButton.setBackgroundResource(R.drawable.button_bg_left)
} else {
if (frozenExists) {
freezeUtxosButton.setBackgroundResource(R.drawable.button_bg)
sendUtxosButton.visibility = View.GONE
churnUtxosButton.visibility = View.GONE
} else {
freezeUtxosButton.setBackgroundResource(R.drawable.button_bg_left)
sendUtxosButton.visibility = View.VISIBLE
churnUtxosButton.visibility = View.VISIBLE
}
freezeUtxosButton.visibility = View.VISIBLE
}
if (bothExist) {
freezeUtxosButton.setText(R.string.toggle_freeze)
} else if (frozenExists) {
freezeUtxosButton.setText(R.string.unfreeze)
} else if (unfrozenExists) {
freezeUtxosButton.setText(R.string.freeze)
}
}
})
bindListeners()
bindObservers()
}
private fun bindListeners() {
sendUtxosButton.visibility = View.GONE
churnUtxosButton.visibility = View.GONE
freezeUtxosButton.visibility = View.GONE
freezeUtxosButton.setOnClickListener {
Toast.makeText(this, "Toggling freeze status, please wait.", Toast.LENGTH_SHORT)
.show()
MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR?.execute {
UTXOService.instance?.toggleFrozen(adapter.selectedUtxos)
runOnUiThread {
adapter.clear()
sendUtxosButton.visibility = View.GONE
churnUtxosButton.visibility = View.GONE
freezeUtxosButton.visibility = View.GONE
}
}
}
sendUtxosButton.setOnClickListener {
val selectedKeyImages = ArrayList<String>()
for (coinsInfo in adapter.selectedUtxos.values) {
coinsInfo.keyImage?.let { keyImage -> selectedKeyImages.add(keyImage) }
}
supportFragmentManager.let { fragmentManager ->
val intent = Intent(this, SendActivity::class.java)
intent.putStringArrayListExtra(Constants.EXTRA_SEND_UTXOS, selectedKeyImages)
startActivity(intent)
}
}
churnUtxosButton.setOnClickListener {
val selectedKeyImages = ArrayList<String>()
for (coinsInfo in adapter.selectedUtxos.values) {
coinsInfo.keyImage?.let { keyImage -> selectedKeyImages.add(keyImage) }
}
val intent = Intent(this, SendActivity::class.java)
intent.putExtra(Constants.EXTRA_SEND_ADDRESS, AddressService.instance?.currentSubaddress()?.address)
intent.putExtra(Constants.EXTRA_SEND_MAX, true)
intent.putStringArrayListExtra(Constants.EXTRA_SEND_UTXOS, selectedKeyImages)
startActivity(intent)
}
}
private fun bindObservers() {
val utxosRecyclerView =
findViewById<RecyclerView>(R.id.transaction_history_recyclerview)
val utxoService = UTXOService.instance
utxosRecyclerView.layoutManager = LinearLayoutManager(this)
utxosRecyclerView.adapter = adapter
utxoService?.utxos?.observe(this) { utxos: List<CoinsInfo> ->
val filteredUtxos = HashMap<String?, CoinsInfo>()
for (coinsInfo in utxos) {
if (!coinsInfo.isSpent) {
filteredUtxos[coinsInfo.pubKey] = coinsInfo
}
}
if (filteredUtxos.isEmpty()) {
utxosRecyclerView.visibility = View.GONE
} else {
adapter.submitList(filteredUtxos)
utxosRecyclerView.visibility = View.VISIBLE
}
}
}
}

View file

@ -1,65 +0,0 @@
package net.mynero.wallet.fragment.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.mynero.wallet.R
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Helper.getClipBoardText
import java.io.File
class PasswordBottomSheetDialog : BottomSheetDialogFragment() {
var listener: PasswordListener? = null
var canCancel = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.password_bottom_sheet_dialog, null)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isCancelable = canCancel
val walletFile = File(activity?.applicationInfo?.dataDir, Constants.WALLET_NAME)
val pastePasswordImageButton =
view.findViewById<ImageButton>(R.id.paste_password_imagebutton)
val passwordEditText = view.findViewById<EditText>(R.id.wallet_password_edittext)
val unlockWalletButton = view.findViewById<Button>(R.id.unlock_wallet_button)
pastePasswordImageButton.setOnClickListener {
passwordEditText.setText(
getClipBoardText(view.context)
)
}
unlockWalletButton.setOnClickListener {
val password = passwordEditText.text.toString()
val success = checkPassword(walletFile, password)
if (success) {
listener?.onPasswordSuccess(password)
dismiss()
} else {
listener?.onPasswordFail()
}
}
}
private fun checkPassword(walletFile: File, password: String): Boolean {
return WalletManager.instance?.verifyWalletPasswordOnly(
walletFile.absolutePath + ".keys",
password
) == true
}
interface PasswordListener {
fun onPasswordSuccess(password: String)
fun onPasswordFail()
}
}

View file

@ -1,407 +0,0 @@
package net.mynero.wallet.fragment.dialog
import android.app.Activity
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import android.widget.RadioGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.zxing.client.android.Intents
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanIntentResult
import com.journeyapps.barcodescanner.ScanOptions
import net.mynero.wallet.MoneroApplication
import net.mynero.wallet.R
import net.mynero.wallet.model.PendingTransaction
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.Wallet.Companion.getAmountFromString
import net.mynero.wallet.model.Wallet.Companion.isAddressValid
import net.mynero.wallet.service.BalanceService
import net.mynero.wallet.service.TxService
import net.mynero.wallet.service.UTXOService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Helper
import net.mynero.wallet.util.Helper.getCameraPermission
import net.mynero.wallet.util.Helper.getClipBoardText
import net.mynero.wallet.util.UriData
import net.mynero.wallet.util.UriData.Companion.parse
class SendBottomSheetDialog : BottomSheetDialogFragment() {
private val _sendingMax = MutableLiveData(false)
private val _pendingTransaction = MutableLiveData<PendingTransaction?>(null)
var selectedUtxos = ArrayList<String>()
private var sendingMax: LiveData<Boolean?> = _sendingMax
private var pendingTransaction: LiveData<PendingTransaction?> = _pendingTransaction
var uriData: UriData? = null
private val cameraPermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted: Boolean ->
if (granted) {
onScan()
} else {
Toast.makeText(activity, getString(R.string.no_camera_permission), Toast.LENGTH_SHORT)
.show()
}
}
var isChurning = false
var listener: Listener? = null
var priority: PendingTransaction.Priority = PendingTransaction.Priority.Priority_Low
private var addressEditText: EditText? = null
private var amountEditText: EditText? = null
private val barcodeLauncher = registerForActivityResult(
ScanContract()
) { result: ScanIntentResult ->
if (result.contents != null) {
pasteAddress(result.contents)
}
}
private var sendAllTextView: TextView? = null
private var feeTextView: TextView? = null
private var addressTextView: TextView? = null
private var amountTextView: TextView? = null
private var feeRadioGroupLabelTextView: TextView? = null
private var selectedUtxosValueTextView: TextView? = null
private var createButton: Button? = null
private var sendButton: Button? = null
private var sendMaxButton: Button? = null
private var pasteAddressImageButton: ImageButton? = null
private var scanAddressImageButton: ImageButton? = null
private var feeRadioGroup: RadioGroup? = null
private var donateTextView: TextView? = null
private var donatingTextView: TextView? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.send_bottom_sheet_dialog, null)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pasteAddressImageButton = view.findViewById(R.id.paste_address_imagebutton)
scanAddressImageButton = view.findViewById(R.id.scan_address_imagebutton)
sendMaxButton = view.findViewById(R.id.send_max_button)
addressEditText = view.findViewById(R.id.address_edittext)
amountEditText = view.findViewById(R.id.amount_edittext)
sendButton = view.findViewById(R.id.send_tx_button)
createButton = view.findViewById(R.id.create_tx_button)
sendAllTextView = view.findViewById(R.id.sending_all_textview)
feeTextView = view.findViewById(R.id.fee_textview)
addressTextView = view.findViewById(R.id.address_pending_textview)
amountTextView = view.findViewById(R.id.amount_pending_textview)
feeRadioGroup = view.findViewById(R.id.tx_fee_radiogroup)
feeRadioGroupLabelTextView = view.findViewById(R.id.tx_fee_radiogroup_label_textview)
selectedUtxosValueTextView = view.findViewById(R.id.selected_utxos_value_textview)
donateTextView = view.findViewById(R.id.donate_label_textview)
donatingTextView = view.findViewById(R.id.donating_label_textview)
donateTextView?.setOnClickListener {
addressEditText?.setText(
Constants.DONATE_ADDRESS
)
}
donatingTextView?.setOnClickListener {
addressEditText?.setText("")
}
addressEditText?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
if (s.toString() == Constants.DONATE_ADDRESS) {
donatingTextView?.visibility = View.VISIBLE
donateTextView?.visibility = View.INVISIBLE
} else {
donatingTextView?.visibility = View.INVISIBLE
donateTextView?.visibility = View.VISIBLE
}
}
})
if (uriData != null) {
addressEditText?.setText(uriData?.address)
if (uriData?.hasAmount() == true) {
amountEditText?.setText(uriData?.amount)
}
}
if (selectedUtxos.isNotEmpty()) {
var selectedValue: Long = 0
val utxos = UTXOService.instance?.getUtxos() ?: return
for (coinsInfo in utxos) {
if (selectedUtxos.contains(coinsInfo.keyImage)) {
selectedValue += coinsInfo.amount
}
}
val valueString = Wallet.getDisplayAmount(selectedValue)
selectedUtxosValueTextView?.visibility = View.VISIBLE
if (isChurning) {
_sendingMax.postValue(true)
sendMaxButton?.isEnabled = false
selectedUtxosValueTextView?.text = resources.getString(
R.string.selected_utxos_value_churning,
valueString
)
} else {
selectedUtxosValueTextView?.text = resources.getString(
R.string.selected_utxos_value,
valueString
)
}
} else {
selectedUtxosValueTextView?.visibility = View.GONE
}
bindObservers()
bindListeners()
}
private fun bindObservers() {
BalanceService.instance?.balanceInfo?.observe(viewLifecycleOwner) { balanceInfo ->
createButton?.isEnabled = balanceInfo?.rawUnlocked != 0L
if (!isChurning) {
sendMaxButton?.isEnabled = balanceInfo?.rawUnlocked != 0L
}
}
sendingMax.observe(viewLifecycleOwner) { sendingMax: Boolean? ->
if (pendingTransaction.value == null) {
if (sendingMax == true) {
amountEditText?.visibility = View.INVISIBLE
sendAllTextView?.visibility = View.VISIBLE
sendMaxButton?.text = getText(R.string.undo)
} else {
amountEditText?.visibility = View.VISIBLE
sendAllTextView?.visibility = View.GONE
sendMaxButton?.text = getText(R.string.send_max)
}
}
}
pendingTransaction.observe(viewLifecycleOwner) { pendingTx: PendingTransaction? ->
showConfirmationLayout(pendingTx != null)
if (pendingTx != null) {
val address = addressEditText?.text.toString()
addressTextView?.text = getString(R.string.tx_address_text, address)
amountTextView?.text = getString(
R.string.tx_amount_text,
Helper.getDisplayAmount(pendingTx.getAmount())
)
feeTextView?.text =
getString(R.string.tx_fee_text, Helper.getDisplayAmount(pendingTx.getFee()))
}
}
}
private fun bindListeners() {
feeRadioGroup?.check(R.id.low_fee_radiobutton)
feeRadioGroup?.setOnCheckedChangeListener { _: RadioGroup?, i: Int ->
when (i) {
R.id.low_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_Low
R.id.med_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_Medium
R.id.high_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_High
}
}
pasteAddressImageButton?.setOnClickListener {
val ctx = context
if (ctx != null) {
val clipboard = getClipBoardText(ctx)
clipboard?.let { pasteAddress(it) }
}
}
scanAddressImageButton?.setOnClickListener { onScan() }
sendMaxButton?.setOnClickListener {
val currentValue = if (sendingMax.value != null) sendingMax.value else false
_sendingMax.postValue(currentValue == false)
}
createButton?.setOnClickListener {
val activity = activity
if (activity != null) {
val sendAll = sendingMax.value ?: false
val address = addressEditText?.text.toString().trim { it <= ' ' }
val amount = amountEditText?.text.toString().trim { it <= ' ' }
val validAddress = isAddressValid(address)
if (validAddress && (amount.isNotEmpty() || sendAll)) {
val amountRaw = getAmountFromString(amount)
val balance = BalanceService.instance?.unlockedBalanceRaw ?: 0
if ((amountRaw >= balance || amountRaw <= 0) && !sendAll) {
Toast.makeText(
activity,
getString(R.string.send_amount_invalid),
Toast.LENGTH_SHORT
).show()
return@setOnClickListener
}
Toast.makeText(activity, getString(R.string.creating_tx), Toast.LENGTH_SHORT)
.show()
createButton?.isEnabled = false
createTx(address, amount, sendAll, priority)
} else if (!validAddress) {
Toast.makeText(
activity,
getString(R.string.send_address_invalid),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
activity,
getString(R.string.send_amount_empty),
Toast.LENGTH_SHORT
).show()
}
}
}
sendButton?.setOnClickListener {
val pendingTx = pendingTransaction.value
if (pendingTx != null) {
Toast.makeText(activity, getString(R.string.sending_tx), Toast.LENGTH_SHORT).show()
sendButton?.isEnabled = false
sendTx(pendingTx)
}
}
}
private fun onScan() {
if (activity?.let { getCameraPermission(it, cameraPermissionsLauncher) } == true) {
val options = ScanOptions()
options.setBeepEnabled(false)
options.setOrientationLocked(true)
options.setDesiredBarcodeFormats(listOf(Intents.Scan.QR_CODE_MODE))
options.addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN)
barcodeLauncher.launch(options)
}
}
private fun sendTx(pendingTx: PendingTransaction) {
val activity: Activity? = activity
if (activity != null) {
(activity.application as MoneroApplication).executor?.execute {
val success = TxService.instance?.sendTx(pendingTx)
activity.runOnUiThread(Runnable {
if (success == true) {
Toast.makeText(activity, getString(R.string.sent_tx), Toast.LENGTH_SHORT)
.show()
if (listener != null) {
listener?.onSentTransaction()
}
dismiss()
} else {
sendButton?.isEnabled = true
Toast.makeText(
activity,
getString(R.string.error_sending_tx),
Toast.LENGTH_SHORT
).show()
}
})
}
}
}
private fun createTx(
address: String,
amount: String,
sendAll: Boolean,
feePriority: PendingTransaction.Priority
) {
val activity: Activity? = activity
if (activity != null) {
(activity.application as MoneroApplication).executor?.execute {
try {
val pendingTx = TxService.instance?.createTx(
address,
amount,
sendAll,
feePriority,
selectedUtxos
)
if (pendingTx != null && pendingTx.status === PendingTransaction.Status.Status_Ok) {
_pendingTransaction.postValue(pendingTx)
} else {
activity.runOnUiThread(Runnable {
createButton?.isEnabled = true
if (pendingTx != null) {
Toast.makeText(
activity,
getString(
R.string.error_creating_tx,
pendingTx.getErrorString()
),
Toast.LENGTH_SHORT
).show()
}
})
}
} catch (e: Exception) {
activity.runOnUiThread(Runnable {
createButton?.isEnabled = true
Toast.makeText(activity, e.message, Toast.LENGTH_SHORT).show()
})
}
}
}
}
private fun showConfirmationLayout(show: Boolean) {
if (show) {
sendButton?.visibility = View.VISIBLE
addressEditText?.visibility = View.GONE
amountEditText?.visibility = View.GONE
sendAllTextView?.visibility = View.GONE
createButton?.visibility = View.GONE
sendMaxButton?.visibility = View.GONE
pasteAddressImageButton?.visibility = View.GONE
scanAddressImageButton?.visibility = View.GONE
feeTextView?.visibility = View.VISIBLE
addressTextView?.visibility = View.VISIBLE
amountTextView?.visibility = View.VISIBLE
selectedUtxosValueTextView?.visibility = View.GONE
feeRadioGroup?.visibility = View.GONE
feeRadioGroupLabelTextView?.visibility = View.GONE
donateTextView?.visibility = View.GONE
} else {
sendButton?.visibility = View.GONE
addressEditText?.visibility = View.VISIBLE
amountEditText?.visibility =
if (java.lang.Boolean.TRUE == sendingMax.value) View.GONE else View.VISIBLE
sendAllTextView?.visibility =
if (java.lang.Boolean.TRUE == sendingMax.value) View.VISIBLE else View.GONE
createButton?.visibility = View.VISIBLE
sendMaxButton?.visibility = View.VISIBLE
pasteAddressImageButton?.visibility = View.VISIBLE
scanAddressImageButton?.visibility = View.VISIBLE
feeTextView?.visibility = View.GONE
addressTextView?.visibility = View.GONE
amountTextView?.visibility = View.GONE
if (selectedUtxos.isNotEmpty()) {
selectedUtxosValueTextView?.visibility = View.VISIBLE
}
feeRadioGroup?.visibility = View.VISIBLE
feeRadioGroupLabelTextView?.visibility = View.VISIBLE
donateTextView?.visibility = View.VISIBLE
}
}
private fun pasteAddress(address: String) {
val uriData = parse(address)
if (uriData != null) {
addressEditText?.setText(uriData.address)
if (uriData.hasAmount()) {
amountEditText?.setText(uriData.amount)
}
} else {
Toast.makeText(activity, getString(R.string.send_address_invalid), Toast.LENGTH_SHORT)
.show()
}
}
interface Listener {
fun onSentTransaction()
}
}

View file

@ -1,5 +0,0 @@
package net.mynero.wallet.fragment.home
import androidx.lifecycle.ViewModel
class HomeViewModel : ViewModel()

View file

@ -1,350 +0,0 @@
package net.mynero.wallet.fragment.onboarding
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
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.ImageView
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 com.google.android.material.progressindicator.CircularProgressIndicator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.mynero.wallet.R
import net.mynero.wallet.data.Node
import net.mynero.wallet.fragment.dialog.AddNodeBottomSheetDialog
import net.mynero.wallet.fragment.dialog.AddNodeBottomSheetDialog.AddNodeListener
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog.NodeSelectionDialogListener
import net.mynero.wallet.fragment.onboarding.OnboardingViewModel.SeedType
import net.mynero.wallet.model.EnumTorState
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
class OnboardingFragment : Fragment(), NodeSelectionDialogListener, AddNodeListener {
private var useOffset = true
private var mViewModel: OnboardingViewModel? = null
private var walletProxyAddressEditText: EditText? = null
private var walletProxyPortEditText: EditText? = null
private var walletPasswordEditText: EditText? = null
private var walletPasswordConfirmEditText: EditText? = null
private var walletSeedEditText: EditText? = null
private var walletRestoreHeightEditText: EditText? = null
private var createWalletButton: Button? = null
private var moreOptionsDropdownTextView: TextView? = null
private var torSwitch: SwitchCompat? = null
private var advancedOptionsLayout: ConstraintLayout? = null
private var moreOptionsChevronImageView: ImageView? = null
private var seedOffsetCheckbox: CheckBox? = null
private var selectNodeButton: Button? = null
private var showXmrchanSwitch: SwitchCompat? = null
private var xmrchanOnboardingImage: ImageView? = null
private var seedTypeButton: Button? = null
private var seedTypeDescTextView: TextView? = null
private var useBundledTor: CheckBox? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_onboarding, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel = ViewModelProvider(this)[OnboardingViewModel::class.java]
selectNodeButton = view.findViewById(R.id.select_node_button)
walletPasswordEditText = view.findViewById(R.id.wallet_password_edittext)
walletPasswordConfirmEditText = view.findViewById(R.id.wallet_password_confirm_edittext)
walletSeedEditText = view.findViewById(R.id.wallet_seed_edittext)
walletRestoreHeightEditText = view.findViewById(R.id.wallet_restore_height_edittext)
createWalletButton = view.findViewById(R.id.create_wallet_button)
moreOptionsDropdownTextView = view.findViewById(R.id.advanced_settings_dropdown_textview)
moreOptionsChevronImageView = view.findViewById(R.id.advanced_settings_chevron_imageview)
torSwitch = view.findViewById(R.id.tor_onboarding_switch)
seedOffsetCheckbox = view.findViewById(R.id.seed_offset_checkbox)
walletProxyAddressEditText = view.findViewById(R.id.wallet_proxy_address_edittext)
walletProxyPortEditText = view.findViewById(R.id.wallet_proxy_port_edittext)
advancedOptionsLayout = view.findViewById(R.id.more_options_layout)
showXmrchanSwitch = view.findViewById(R.id.show_xmrchan_switch)
xmrchanOnboardingImage = view.findViewById(R.id.xmrchan_onboarding_imageview)
seedTypeButton = view.findViewById(R.id.seed_type_button)
seedTypeDescTextView = view.findViewById(R.id.seed_type_desc_textview)
useBundledTor = view.findViewById(R.id.bundled_tor_checkbox)
seedOffsetCheckbox?.isChecked = useOffset
val usingProxy = ProxyService.instance?.usingProxy == true
val usingBundledTor = ProxyService.instance?.useBundledTor == true
torSwitch?.isChecked = usingProxy
useBundledTor?.isChecked = usingBundledTor
useBundledTor?.isEnabled = usingProxy
walletProxyAddressEditText?.isEnabled = usingProxy && !usingBundledTor
walletProxyPortEditText?.isEnabled = usingProxy && !usingBundledTor
val node = PrefService.instance?.node // should be using default here
selectNodeButton?.text = getString(R.string.node_button_text, node?.address)
bindListeners()
bindObservers()
}
private fun bindObservers() {
mViewModel?.passphrase?.observe(viewLifecycleOwner) { text ->
if (text.isEmpty()) {
walletPasswordConfirmEditText?.text = null
walletPasswordConfirmEditText?.visibility = View.GONE
} else {
walletPasswordConfirmEditText?.visibility = View.VISIBLE
}
}
mViewModel?.showMoreOptions?.observe(viewLifecycleOwner) { show: Boolean ->
if (show) {
moreOptionsChevronImageView?.setImageResource(R.drawable.ic_keyboard_arrow_up)
advancedOptionsLayout?.visibility = View.VISIBLE
} else {
moreOptionsChevronImageView?.setImageResource(R.drawable.ic_keyboard_arrow_down)
advancedOptionsLayout?.visibility = View.GONE
}
}
mViewModel?.enableButton?.observe(viewLifecycleOwner) { enable: Boolean ->
createWalletButton?.isEnabled = enable
}
mViewModel?.seedType?.observe(viewLifecycleOwner) { seedType: SeedType ->
seedTypeButton?.text = seedType.toString()
seedTypeDescTextView?.text = getText(seedType.descResId)
if (seedType == SeedType.LEGACY) {
seedOffsetCheckbox?.visibility = View.VISIBLE
walletRestoreHeightEditText?.visibility = View.VISIBLE
walletPasswordEditText?.hint = getString(R.string.password_optional)
walletSeedEditText?.hint = getString(R.string.recovery_phrase_optional_legacy)
} else {
seedOffsetCheckbox?.visibility = View.GONE
walletRestoreHeightEditText?.visibility = View.GONE
walletPasswordEditText?.hint = getString(R.string.password_non_optional)
walletSeedEditText?.hint = getString(R.string.recovery_phrase_optional_polyseed)
}
}
mViewModel?.showMonerochan?.observe(viewLifecycleOwner) {
if (it) {
xmrchanOnboardingImage?.visibility = View.VISIBLE
} else {
xmrchanOnboardingImage?.visibility = View.GONE
}
}
mViewModel?.useBundledTor?.observe(viewLifecycleOwner) { isChecked ->
walletProxyPortEditText?.isEnabled = !isChecked && mViewModel?.useProxy?.value == true
walletProxyAddressEditText?.isEnabled = !isChecked && mViewModel?.useProxy?.value == true
}
mViewModel?.useProxy?.observe(viewLifecycleOwner) { useProxy ->
useBundledTor?.isEnabled = useProxy
walletProxyAddressEditText?.isEnabled = useProxy && mViewModel?.useBundledTor?.value == false
walletProxyPortEditText?.isEnabled = useProxy && mViewModel?.useBundledTor?.value == false
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
val indicatorCircle =
view?.findViewById<CircularProgressIndicator>(R.id.onboarding_tor_loading_progressindicator)
val torIcon = view?.findViewById<ImageView>(R.id.onboarding_tor_icon)
samouraiTorManager?.getTorStateLiveData()?.observe(viewLifecycleOwner) { state ->
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if (socketAddress.toString().isEmpty()) return@let
if (mViewModel?.useProxy?.value == true && mViewModel?.useBundledTor?.value == true) {
torIcon?.visibility = View.VISIBLE
indicatorCircle?.visibility = View.INVISIBLE
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
updateProxy(address, port)
}
}
indicatorCircle?.isIndeterminate = state.progressIndicator == 0
indicatorCircle?.progress = state.progressIndicator
when (state.state) {
EnumTorState.OFF -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.INVISIBLE
}
EnumTorState.STARTING, EnumTorState.STOPPING -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.VISIBLE
}
else -> {}
}
}
}
private fun updateProxy(address: String, port: String) {
walletProxyPortEditText?.setText(port)
walletProxyAddressEditText?.setText(address)
}
private fun bindListeners() {
// Disable onBack click
val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {}
}
activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner, onBackPressedCallback)
moreOptionsDropdownTextView?.setOnClickListener { mViewModel?.onMoreOptionsClicked() }
moreOptionsChevronImageView?.setOnClickListener { mViewModel?.onMoreOptionsClicked() }
seedOffsetCheckbox?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
useOffset = b
}
createWalletButton?.setOnClickListener {
onBackPressedCallback.isEnabled = false
createOrImportWallet(
walletSeedEditText?.text.toString().trim { it <= ' ' },
walletRestoreHeightEditText?.text.toString().trim { it <= ' ' }
)
}
walletPasswordEditText?.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) {
mViewModel?.setPassphrase(editable.toString())
}
})
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) {
mViewModel?.setConfirmedPassphrase(editable.toString())
}
})
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) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
if (text.isEmpty()) {
createWalletButton?.setText(R.string.create_wallet)
} else {
createWalletButton?.setText(R.string.menu_restore)
}
}
})
seedTypeButton?.setOnClickListener { toggleSeedType() }
torSwitch?.setOnCheckedChangeListener { _, b: Boolean ->
mViewModel?.setUseProxy(b)
}
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?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
mViewModel?.setMonerochan(b)
}
selectNodeButton?.setOnClickListener {
activity?.supportFragmentManager?.let { fragmentManager ->
val dialog = NodeSelectionBottomSheetDialog()
dialog.listener = this
dialog.show(fragmentManager, "node_selection_dialog")
}
}
useBundledTor?.setOnCheckedChangeListener { _, isChecked ->
mViewModel?.setUseBundledTor(isChecked)
}
}
private fun toggleSeedType() {
val seedType = mViewModel?.seedType?.value ?: return
var newSeedType = SeedType.UNKNOWN
if (seedType == SeedType.POLYSEED) {
newSeedType = SeedType.LEGACY
} else if (seedType == SeedType.LEGACY) {
newSeedType = SeedType.POLYSEED
}
mViewModel?.setSeedType(newSeedType)
}
private fun createOrImportWallet(
walletSeed: String,
restoreHeightText: String
) {
activity?.let { act ->
lifecycleScope.launch(Dispatchers.IO) {
mViewModel?.createOrImportWallet(
act,
walletSeed,
restoreHeightText,
useOffset
)
}
}
}
override fun onNodeSelected() {
val node = PrefService.instance?.node
selectNodeButton?.text = getString(R.string.node_button_text, node?.address)
Toast.makeText(
activity,
getString(R.string.node_selected, node?.name ?: node?.host),
Toast.LENGTH_SHORT
).show()
}
override fun onClickedEditNode(node: Node?) {}
override fun onClickedAddNode() {
activity?.supportFragmentManager?.let { fragmentManager ->
val addNodeDialog = AddNodeBottomSheetDialog()
addNodeDialog.listener = this
addNodeDialog.show(fragmentManager, "add_node_dialog")
}
}
override fun onNodeAdded() {
activity?.supportFragmentManager?.let { fragmentManager ->
val dialog = NodeSelectionBottomSheetDialog()
dialog.listener = this
dialog.show(fragmentManager, "node_selection_dialog")
}
}
}

View file

@ -1,283 +0,0 @@
package net.mynero.wallet.fragment.onboarding
import android.app.Activity
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import net.mynero.wallet.MainActivity
import net.mynero.wallet.MoneroApplication
import net.mynero.wallet.R
import net.mynero.wallet.livedata.combineLiveDatas
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
import java.util.Calendar
class OnboardingViewModel : ViewModel() {
private val _showMoreOptions = MutableLiveData(false)
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("")
val passphrase: LiveData<String> = _passphrase
private val _confirmedPassphrase = MutableLiveData("")
private val _showMonerochan = MutableLiveData(Constants.DEFAULT_PREF_MONEROCHAN)
val showMonerochan: LiveData<Boolean> = _showMonerochan
var showMoreOptions: LiveData<Boolean> = _showMoreOptions
var seedType: LiveData<SeedType> = _seedType
init {
_useProxy.value = ProxyService.instance?.usingProxy
_useBundledTor.value = ProxyService.instance?.useBundledTor
}
val enableButton = combineLiveDatas(
seedType,
_useProxy,
_proxyAddress,
_proxyPort,
_useBundledTor,
_passphrase,
_confirmedPassphrase,
_creatingWallet,
ProxyService.instance?.samouraiTorManager?.getTorStateLiveData()
) { seedType, useProxy, proxyAddress, proxyPort, useBundledTor, passphrase, confirmedPassphrase, creatingWallet, torState ->
if (seedType == null || useProxy == null || proxyAddress == null || proxyPort == null || useBundledTor == null || passphrase == null || confirmedPassphrase == null || creatingWallet == null) return@combineLiveDatas false
if ((passphrase.isNotEmpty() || confirmedPassphrase.isNotEmpty()) && passphrase != confirmedPassphrase) return@combineLiveDatas false
if (creatingWallet) return@combineLiveDatas false
if (seedType == SeedType.POLYSEED && (passphrase.isEmpty() || confirmedPassphrase.isEmpty())) return@combineLiveDatas false
if (useProxy && (proxyAddress.isEmpty() || proxyPort.isEmpty()) && !useBundledTor) return@combineLiveDatas false
val progress = torState?.progressIndicator ?: 0
if (useBundledTor && progress < 100 && useProxy) return@combineLiveDatas false
return@combineLiveDatas true
}
fun onMoreOptionsClicked() {
val currentValue = showMoreOptions.value ?: false
val newValue = !currentValue
_showMoreOptions.value = newValue
}
fun setSeedType(seedType: SeedType?) {
_seedType.value = seedType
}
fun createOrImportWallet(
mainActivity: Activity,
walletSeed: String,
restoreHeightText: String,
useOffset: Boolean
) {
val passphrase = _passphrase.value ?: return
val confirmedPassphrase = _confirmedPassphrase.value ?: return
val application = mainActivity.application as MoneroApplication
_creatingWallet.postValue(true)
val offset = if (useOffset) confirmedPassphrase else ""
if (passphrase.isNotEmpty()) {
if (passphrase != confirmedPassphrase) {
_creatingWallet.postValue(false)
mainActivity.runOnUiThread {
Toast.makeText(
mainActivity,
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) {
(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()
}
}
}
private val newRestoreHeight: Long
get() {
val restoreDate = Calendar.getInstance()
restoreDate.add(Calendar.DAY_OF_MONTH, 0)
return RestoreHeight.instance?.getHeight(restoreDate.time) ?: 0
}
private fun getMnemonicType(seed: String): SeedType {
val words = seed.split("\\s".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val seedTypeValue = seedType.value ?: return SeedType.LEGACY
return if (words.size == 16 && seedTypeValue == SeedType.POLYSEED) {
SeedType.POLYSEED
} else if (words.size == 25 && seedTypeValue == SeedType.LEGACY) {
SeedType.LEGACY
} else {
SeedType.UNKNOWN
}
}
private fun createTempWallet(tmpWalletFile: File): Wallet? {
return WalletManager.instance?.createWallet(
tmpWalletFile,
"",
Constants.MNEMONIC_LANGUAGE,
0
)
}
fun setProxyAddress(address: String) {
_proxyAddress.value = address
if (address.isEmpty()) PrefService.instance?.deleteProxy()
val port = _proxyPort.value ?: return
ProxyService.instance?.updateProxy(address, port)
}
fun setProxyPort(port: String) {
_proxyPort.value = port
if (port.isEmpty()) PrefService.instance?.deleteProxy()
val address = _proxyAddress.value ?: return
ProxyService.instance?.updateProxy(address, port)
}
fun setUseBundledTor(useBundled: Boolean) {
_useBundledTor.value = useBundled
ProxyService.instance?.useBundledTor = useBundled
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (useBundled && ProxyService.instance?.usingProxy == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
fun setUseProxy(useProxy: Boolean) {
_useProxy.value = useProxy
ProxyService.instance?.usingProxy = useProxy
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (useProxy && ProxyService.instance?.useBundledTor == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
fun setPassphrase(passphrase: String) {
_passphrase.value = passphrase
}
fun setConfirmedPassphrase(confirmedPassphrase: String) {
_confirmedPassphrase.value = confirmedPassphrase
}
fun setMonerochan(b: Boolean) {
_showMonerochan.value = b
PrefService.instance?.edit()?.putBoolean(Constants.PREF_MONEROCHAN, b)?.apply()
}
enum class SeedType(val descResId: Int) {
LEGACY(R.string.seed_desc_legacy), POLYSEED(R.string.seed_desc_polyseed), UNKNOWN(0)
}
}

View file

@ -1,42 +0,0 @@
package net.mynero.wallet.fragment.receive
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import net.mynero.wallet.data.Subaddress
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.AddressService
import java.util.Collections
class ReceiveViewModel : ViewModel() {
private val _address = MutableLiveData<Subaddress?>()
private val _addresses = MutableLiveData<List<Subaddress>>()
val address: LiveData<Subaddress?> = _address
val addresses: LiveData<List<Subaddress>> = _addresses
fun init() {
_addresses.value = subaddresses
_address.value = addresses.value?.lastOrNull()
}
private val subaddresses: List<Subaddress>
get() {
val wallet = WalletManager.instance?.wallet
val subaddresses = ArrayList<Subaddress>()
val numAddresses = AddressService.instance?.numAddresses ?: 1
for (i in 0 until numAddresses) {
wallet?.getSubaddressObject(i)?.let { subaddresses.add(it) }
}
return Collections.unmodifiableList(subaddresses)
}
val freshSubaddress: Unit
get() {
_address.value = AddressService.instance?.freshSubaddress()
_addresses.value = subaddresses
}
fun selectAddress(subaddress: Subaddress?) {
_address.value = subaddress
}
}

View file

@ -1,550 +0,0 @@
package net.mynero.wallet.fragment.send
import android.app.Activity
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.RadioGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.zxing.client.android.Intents
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanIntentResult
import com.journeyapps.barcodescanner.ScanOptions
import com.ncorti.slidetoact.SlideToActView
import com.ncorti.slidetoact.SlideToActView.OnSlideCompleteListener
import net.mynero.wallet.MoneroApplication
import net.mynero.wallet.R
import net.mynero.wallet.model.PendingTransaction
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.service.BalanceService
import net.mynero.wallet.service.TxService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Helper
import net.mynero.wallet.util.UriData
class SendFragment : Fragment() {
var priority: PendingTransaction.Priority = PendingTransaction.Priority.Priority_Low
private var mViewModel: SendViewModel? = null
private var sendMaxButton: Button? = null
private var addOutputImageView: ImageButton? = null
private var destList: LinearLayout? = null
private var inflater: LayoutInflater? = null
private var createButton: Button? = null
private var sendTxSlider: SlideToActView? = null
private var feeRadioGroup: RadioGroup? = null
private var feeRadioGroupLabelTextView: TextView? = null
private var feeTextView: TextView? = null
private var addressTextView: TextView? = null
private var amountTextView: TextView? = null
private var currentEntryIndex = -1
private val qrCodeLauncher =
registerForActivityResult(ScanContract()) { result: ScanIntentResult ->
if (result.contents != null) {
if (currentEntryIndex != -1) {
pasteAddress(getDestView(currentEntryIndex), result.contents, false)
currentEntryIndex = -1
}
}
}
private val cameraPermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted: Boolean ->
if (granted) {
onScan(currentEntryIndex)
} else {
Toast.makeText(activity, getString(R.string.no_camera_permission), Toast.LENGTH_SHORT)
.show()
currentEntryIndex = -1
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_send, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel = ViewModelProvider(this)[SendViewModel::class.java]
sendMaxButton = view.findViewById(R.id.send_max_button)
addOutputImageView = view.findViewById(R.id.add_output_button)
destList = view.findViewById(R.id.transaction_destination_list)
createButton = view.findViewById(R.id.create_tx_button)
feeRadioGroup = view.findViewById(R.id.tx_fee_radiogroup)
feeTextView = view.findViewById(R.id.fee_textview)
sendTxSlider = view.findViewById(R.id.send_tx_slider)
addressTextView = view.findViewById(R.id.address_pending_textview)
amountTextView = view.findViewById(R.id.amount_pending_textview)
feeRadioGroup = view.findViewById(R.id.tx_fee_radiogroup)
feeRadioGroupLabelTextView = view.findViewById(R.id.tx_fee_radiogroup_label_textview)
inflater = activity?.layoutInflater
bindListeners()
bindObservers()
init()
}
private fun init() {
addOutput(true)
}
private fun bindListeners() {
feeRadioGroup?.check(R.id.low_fee_radiobutton)
priority = PendingTransaction.Priority.Priority_Low
feeRadioGroup?.setOnCheckedChangeListener { _: RadioGroup?, i: Int ->
when (i) {
R.id.low_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_Low
R.id.med_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_Medium
R.id.high_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_High
}
}
addOutputImageView?.setOnClickListener {
sendMaxButton?.visibility = View.GONE
val outputCount = destCount
if (outputCount < 8) {
addOutput(false)
} else {
Toast.makeText(
activity,
getString(R.string.max_outputs_allowed),
Toast.LENGTH_SHORT
).show()
}
}
sendMaxButton?.setOnClickListener { mViewModel?.setSendingMax(!isSendAll) }
createButton?.setOnClickListener {
val outputsValid = checkDestsValidity(isSendAll)
if (outputsValid) {
Toast.makeText(activity, getString(R.string.creating_tx), Toast.LENGTH_SHORT).show()
createButton?.isEnabled = false
sendMaxButton?.isEnabled = false
createTx(rawDests, isSendAll, priority)
} else {
Toast.makeText(
activity,
getString(R.string.creating_tx_failed_invalid_outputs),
Toast.LENGTH_SHORT
).show()
}
}
sendTxSlider?.onSlideCompleteListener =
object : OnSlideCompleteListener {
override fun onSlideComplete(view: SlideToActView) {
confirmSlider()
}
}
sendTxSlider?.let { slideToActView ->
ViewCompat.addAccessibilityAction(
slideToActView,
getString(R.string.approve_the_transaction)
) { _, _ ->
confirmSlider()
return@addAccessibilityAction true
}
}
}
private fun confirmSlider() {
val pendingTx = mViewModel?.pendingTransaction?.value ?: return
Toast.makeText(activity, getString(R.string.sending_tx), Toast.LENGTH_SHORT)
.show()
sendTx(pendingTx)
}
private fun checkDestsValidity(sendAll: Boolean): Boolean {
val dests = rawDests
for (dest in dests) {
val address = dest.component1()
val amount = dest.component2()
if (!sendAll) {
if (amount.isEmpty()) {
Toast.makeText(
activity,
getString(R.string.send_amount_empty),
Toast.LENGTH_SHORT
).show()
return false
}
val amountRaw = Wallet.getAmountFromString(amount)
val balance = BalanceService.instance?.unlockedBalanceRaw ?: 0
if (amountRaw >= balance || amountRaw <= 0) {
Toast.makeText(
activity,
getString(R.string.send_amount_invalid),
Toast.LENGTH_SHORT
).show()
return false
}
} else if (dests.size > 1) {
Toast.makeText(
activity,
getString(R.string.send_amount_invalid_sendall_paytomany),
Toast.LENGTH_SHORT
).show()
return false
}
val uriData = UriData.parse(address)
val isValidAddress = uriData != null
if (!isValidAddress) {
Toast.makeText(
activity,
getString(R.string.send_address_invalid),
Toast.LENGTH_SHORT
).show()
return false
}
if (dests.size > 1 && uriData?.hasPaymentId() == true) {
Toast.makeText(
activity,
getString(R.string.paymentid_paytomany),
Toast.LENGTH_SHORT
).show()
return false
}
}
return true
}
private fun destsHasPaymentId(): Boolean {
val dests = rawDests
for (dest in dests) {
val address = dest.component1()
val uriData = UriData.parse(address) ?: return false
if (uriData.hasPaymentId()) return true
}
return false
}
private fun bindObservers() {
mViewModel?.sendingMax?.observe(viewLifecycleOwner) { sendingMax: Boolean? ->
if (mViewModel?.pendingTransaction?.value == null) {
if (sendingMax == true) {
prepareOutputsForMaxSend()
sendMaxButton?.text = getText(R.string.undo)
} else {
unprepareMaxSend()
sendMaxButton?.text = getText(R.string.send_max)
}
}
}
mViewModel?.showAddOutputButton?.observe(viewLifecycleOwner) { show: Boolean? ->
setAddOutputButtonVisibility(
if (show == true && !destsHasPaymentId()) View.VISIBLE else View.INVISIBLE
)
}
mViewModel?.pendingTransaction?.observe(viewLifecycleOwner) { pendingTx: PendingTransaction? ->
showConfirmationLayout(pendingTx != null)
if (pendingTx != null) {
val address = if (destCount == 1) getAddressField(0).text.toString() else "Multiple"
addressTextView?.text = getString(R.string.tx_address_text, address)
amountTextView?.text =
getString(
R.string.tx_amount_text,
Helper.getDisplayAmount(pendingTx.getAmount())
)
feeTextView?.text =
getString(R.string.tx_fee_text, Helper.getDisplayAmount(pendingTx.getFee()))
}
}
}
private fun addOutput(initial: Boolean) {
if (inflater != null) {
val index = destCount
val entryView =
inflater?.inflate(R.layout.transaction_output_item, null) as ConstraintLayout
val removeOutputImageButton =
entryView.findViewById<ImageButton>(R.id.remove_output_imagebutton)
val addressField = entryView.findViewById<EditText>(R.id.address_edittext)
val donateTextView = entryView.findViewById<TextView>(R.id.donate_label)
val donatingTextView = entryView.findViewById<TextView>(R.id.donating_label)
donateTextView.setOnClickListener {
addressField.setText(
Constants.DONATE_ADDRESS
)
}
donatingTextView.setOnClickListener {
addressField.setText("")
}
addressField.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
val currentOutputs: Int = destCount
val uriData = UriData.parse(s.toString())
if (uriData != null) {
// we have valid address
val hasPaymentId = uriData.hasPaymentId()
if (currentOutputs > 1 && hasPaymentId) {
// multiple outputs when pasting/editing in integrated address. this is not allowed
Toast.makeText(
activity,
getString(R.string.paymentid_paytomany),
Toast.LENGTH_SHORT
).show()
addressField.text = null
} else if (currentOutputs == 1 && hasPaymentId) {
// show add output button: we are sending to integrated address
mViewModel?.setShowAddOutputButton(false)
}
} else if (currentOutputs == 1 && !isSendAll) {
// when send-all is false and this is our only dest and address is invalid, then show add output button
mViewModel?.setShowAddOutputButton(true)
}
if (s.toString() == Constants.DONATE_ADDRESS) {
donateTextView.visibility = View.INVISIBLE
donatingTextView.visibility = View.VISIBLE
} else {
donateTextView.visibility = View.VISIBLE
donatingTextView.visibility = View.INVISIBLE
}
}
})
entryView.findViewById<View>(R.id.paste_amount_imagebutton)
.setOnClickListener {
val ctx = context
if (ctx != null) {
val clipboard = Helper.getClipBoardText(ctx)
if (clipboard != null) {
pasteAddress(entryView, clipboard, true)
}
}
}
entryView.findViewById<View>(R.id.paste_address_imagebutton)
.setOnClickListener {
val ctx = context
if (ctx != null) {
val clipboard = Helper.getClipBoardText(ctx)
if (clipboard != null) {
pasteAddress(entryView, clipboard, false)
}
}
}
entryView.findViewById<View>(R.id.scan_address_imagebutton)
.setOnClickListener { onScan(index) }
if (initial) {
removeOutputImageButton.visibility = View.INVISIBLE
} else {
removeOutputImageButton.setOnClickListener {
val currentCount = destCount
if (currentCount > 1) {
if (currentCount == 2) {
sendMaxButton?.visibility = View.VISIBLE
}
destList?.removeView(entryView)
}
}
}
destList?.addView(entryView)
}
}
private val destCount: Int
get() = destList?.childCount ?: -1
private val rawDests: List<Pair<String, String>>
get() {
val dests = ArrayList<Pair<String, String>>()
for (i in 0 until destCount) {
val entryView = getDestView(i)
val amountField = entryView.findViewById<EditText>(R.id.amount_edittext)
val addressField = entryView.findViewById<EditText>(R.id.address_edittext)
val amount = amountField.text.toString().trim { it <= ' ' }
val address = addressField.text.toString().trim { it <= ' ' }
dests.add(Pair(address, amount))
}
return dests
}
private val isSendAll: Boolean
get() = mViewModel?.sendingMax?.value ?: false
private fun getDestView(pos: Int): ConstraintLayout {
return destList?.getChildAt(pos) as ConstraintLayout
}
private fun getAddressField(pos: Int): EditText {
return getDestView(pos).findViewById<View>(R.id.address_edittext) as EditText
}
private fun unprepareMaxSend() {
val entryView = getDestView(0)
entryView.findViewById<View>(R.id.sending_all_textview).visibility = View.INVISIBLE
entryView.findViewById<View>(R.id.amount_edittext).visibility =
View.VISIBLE
}
private fun prepareOutputsForMaxSend() {
val entryView = getDestView(0)
entryView.findViewById<View>(R.id.sending_all_textview).visibility = View.VISIBLE
entryView.findViewById<View>(R.id.amount_edittext).visibility =
View.INVISIBLE
}
private fun showConfirmationLayout(show: Boolean) {
if (show) {
destList?.visibility = View.GONE
setAddOutputButtonVisibility(View.GONE)
sendMaxButton?.visibility = View.GONE
createButton?.visibility = View.GONE
feeRadioGroup?.visibility = View.GONE
feeRadioGroupLabelTextView?.visibility = View.GONE
sendTxSlider?.visibility = View.VISIBLE
feeTextView?.visibility = View.VISIBLE
addressTextView?.visibility = View.VISIBLE
amountTextView?.visibility = View.VISIBLE
} else {
destList?.visibility = View.VISIBLE
setAddOutputButtonVisibility(View.VISIBLE)
sendMaxButton?.visibility = View.VISIBLE
createButton?.visibility = View.VISIBLE
feeRadioGroup?.visibility = View.VISIBLE
feeRadioGroupLabelTextView?.visibility = View.VISIBLE
sendTxSlider?.visibility = View.GONE
feeTextView?.visibility = View.GONE
addressTextView?.visibility = View.GONE
amountTextView?.visibility = View.GONE
}
}
private fun onScan(index: Int) {
currentEntryIndex = index
if (activity?.let { Helper.getCameraPermission(it, cameraPermissionsLauncher) } == true) {
val options = ScanOptions()
options.setBeepEnabled(false)
options.setOrientationLocked(true)
options.setDesiredBarcodeFormats(listOf(Intents.Scan.QR_CODE_MODE))
options.addExtra(Intents.Scan.SCAN_TYPE, Intents.Scan.MIXED_SCAN)
qrCodeLauncher.launch(options)
}
}
private fun pasteAddress(
entryView: ConstraintLayout,
clipboard: String,
pastingAmount: Boolean
) {
if (pastingAmount) {
try {
clipboard.toDouble()
setAmount(entryView, clipboard)
} catch (e: Exception) {
Toast.makeText(
activity,
getString(R.string.send_amount_invalid),
Toast.LENGTH_SHORT
).show()
return
}
}
val uriData = UriData.parse(clipboard)
if (uriData != null) {
val currentOutputs = destCount
if (currentOutputs > 1 && uriData.hasPaymentId()) {
Toast.makeText(
activity,
getString(R.string.paymentid_paytomany),
Toast.LENGTH_SHORT
).show()
return
} else if (currentOutputs == 1 && uriData.hasPaymentId()) {
mViewModel?.setShowAddOutputButton(false)
}
val addressField = entryView.findViewById<EditText>(R.id.address_edittext)
addressField.setText(uriData.address)
if (uriData.hasAmount()) {
setAmount(entryView, uriData.amount)
}
} else {
Toast.makeText(activity, getString(R.string.send_address_invalid), Toast.LENGTH_SHORT)
.show()
}
}
private fun setAmount(entryView: ConstraintLayout, amount: String?) {
sendMaxButton?.isEnabled = false
val amountField = entryView.findViewById<EditText>(R.id.amount_edittext)
amountField.setText(amount)
}
private fun createTx(
dests: List<Pair<String, String>>,
sendAll: Boolean,
feePriority: PendingTransaction.Priority
) {
(activity?.application as MoneroApplication).executor?.execute {
try {
val pendingTx =
TxService.instance?.createTx(dests, sendAll, feePriority, ArrayList())
if (pendingTx != null && pendingTx.status === PendingTransaction.Status.Status_Ok) {
mViewModel?.setPendingTransaction(pendingTx)
} else {
val activity: Activity? = activity
if (activity != null && pendingTx != null) {
activity.runOnUiThread(Runnable {
createButton?.isEnabled = true
sendMaxButton?.isEnabled = true
if (pendingTx.getErrorString() != null) Toast.makeText(
activity,
getString(R.string.error_creating_tx, pendingTx.getErrorString()),
Toast.LENGTH_SHORT
).show()
})
}
}
} catch (e: Exception) {
e.printStackTrace()
val activity: Activity? = activity
activity?.runOnUiThread {
createButton?.isEnabled = true
sendMaxButton?.isEnabled = true
Toast.makeText(
activity,
getString(R.string.error_creating_tx, e.message),
Toast.LENGTH_SHORT
).show()
}
}
}
}
private fun sendTx(pendingTx: PendingTransaction) {
(activity?.application as MoneroApplication).executor?.execute {
val success = TxService.instance?.sendTx(pendingTx)
val activity: Activity? = activity
activity?.runOnUiThread {
if (success == true) {
Toast.makeText(getActivity(), getString(R.string.sent_tx), Toast.LENGTH_SHORT)
.show()
activity.onBackPressed()
} else {
sendTxSlider?.resetSlider()
Toast.makeText(
getActivity(),
getString(R.string.error_sending_tx),
Toast.LENGTH_SHORT
)
.show()
}
}
}
}
private fun setAddOutputButtonVisibility(visibility: Int) {
addOutputImageView?.visibility = visibility
}
}

View file

@ -1,27 +0,0 @@
package net.mynero.wallet.fragment.send
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import net.mynero.wallet.model.PendingTransaction
class SendViewModel : ViewModel() {
private val _sendingMax = MutableLiveData(false)
private val _showAddOutputButton = MutableLiveData(true)
private val _pendingTransaction = MutableLiveData<PendingTransaction?>(null)
var sendingMax: LiveData<Boolean?> = _sendingMax
var showAddOutputButton: LiveData<Boolean?> = _showAddOutputButton
var pendingTransaction: LiveData<PendingTransaction?> = _pendingTransaction
fun setSendingMax(value: Boolean) {
_sendingMax.value = value
setShowAddOutputButton(!value)
}
fun setShowAddOutputButton(value: Boolean) {
_showAddOutputButton.value = value
}
fun setPendingTransaction(pendingTx: PendingTransaction?) {
_pendingTransaction.postValue(pendingTx)
}
}

View file

@ -1,324 +0,0 @@
package net.mynero.wallet.fragment.settings
import android.os.Bundle
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.ImageView
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.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.progressindicator.CircularProgressIndicator
import net.mynero.wallet.R
import net.mynero.wallet.data.Node
import net.mynero.wallet.data.Node.Companion.fromJson
import net.mynero.wallet.fragment.dialog.AddNodeBottomSheetDialog
import net.mynero.wallet.fragment.dialog.AddNodeBottomSheetDialog.AddNodeListener
import net.mynero.wallet.fragment.dialog.EditNodeBottomSheetDialog
import net.mynero.wallet.fragment.dialog.EditNodeBottomSheetDialog.EditNodeListener
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog.NodeSelectionDialogListener
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.EnumTorState
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.BalanceService
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
class SettingsFragment : Fragment(), PasswordListener, NodeSelectionDialogListener, AddNodeListener,
EditNodeListener {
private var mViewModel: SettingsViewModel? = null
private var walletProxyAddressEditText: EditText? = null
private var walletProxyPortEditText: EditText? = null
private var selectNodeButton: Button? = null
private var streetModeSwitch: SwitchCompat? = null
private var monerochanSwitch: SwitchCompat? = null
private var useBundledTor: CheckBox? = null
private var displaySeedButton: Button? = null
private var displayUtxosButton: Button? = null
private var torSwitch: SwitchCompat? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_settings, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
displaySeedButton = view.findViewById(R.id.display_seed_button)
displayUtxosButton = view.findViewById(R.id.display_utxos_button)
selectNodeButton = view.findViewById(R.id.select_node_button)
streetModeSwitch = view.findViewById(R.id.street_mode_switch)
monerochanSwitch = view.findViewById(R.id.monerochan_switch)
torSwitch = view.findViewById(R.id.tor_switch)
val proxySettingsLayout = 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)
useBundledTor = view.findViewById(R.id.bundled_tor_checkbox)
val cachedProxyAddress = ProxyService.instance?.proxyAddress ?: return
val cachedProxyPort = ProxyService.instance?.proxyPort ?: return
val cachedUsingProxy = ProxyService.instance?.usingProxy == true
val cachedUsingBundledTor = ProxyService.instance?.useBundledTor == true
walletProxyPortEditText?.isEnabled = !cachedUsingBundledTor && cachedUsingProxy
walletProxyAddressEditText?.isEnabled = !cachedUsingBundledTor && cachedUsingProxy
proxySettingsLayout.visibility = View.VISIBLE
streetModeSwitch?.isChecked =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
monerochanSwitch?.isChecked =
PrefService.instance?.getBoolean(Constants.PREF_MONEROCHAN, Constants.DEFAULT_PREF_MONEROCHAN) == true
useBundledTor?.isChecked = cachedUsingBundledTor
torSwitch?.isChecked = cachedUsingProxy
updateProxy(cachedProxyAddress, cachedProxyPort)
val node = PrefService.instance?.node // shouldn't use default value here
selectNodeButton?.text = getString(R.string.node_button_text, node?.address)
bindListeners()
bindObservers()
}
private fun bindListeners() {
val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
refreshProxy()
findNavController().popBackStack()
}
}
activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner, onBackPressedCallback)
streetModeSwitch?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PrefService.instance?.edit()?.putBoolean(Constants.PREF_STREET_MODE, b)?.apply()
BalanceService.instance?.refreshBalance()
}
monerochanSwitch?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PrefService.instance?.edit()?.putBoolean(Constants.PREF_MONEROCHAN, b)?.apply()
HistoryService.instance?.refreshHistory()
}
selectNodeButton?.setOnClickListener {
activity?.supportFragmentManager?.let { fragmentManager ->
val dialog = NodeSelectionBottomSheetDialog()
dialog.listener = this
dialog.show(fragmentManager, "node_selection_dialog")
}
}
useBundledTor?.setOnCheckedChangeListener { _, isChecked ->
mViewModel?.setUseBundledTor(isChecked)
}
displaySeedButton?.setOnClickListener {
val usesPassword =
PrefService.instance?.getBoolean(Constants.PREF_USES_PASSWORD, false) == true
if (usesPassword) {
activity?.supportFragmentManager?.let { fragmentManager ->
val passwordDialog = PasswordBottomSheetDialog()
passwordDialog.canCancel = true
passwordDialog.listener = this
passwordDialog.show(fragmentManager, "password_dialog")
}
} else {
displaySeedDialog("")
}
}
displayUtxosButton?.setOnClickListener { navigate(R.id.nav_to_utxos) }
torSwitch?.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
mViewModel?.setUseProxy(b)
}
}
private fun bindObservers() {
mViewModel?.useProxy?.observe(viewLifecycleOwner) { useProxy ->
useBundledTor?.isEnabled = useProxy
walletProxyPortEditText?.isEnabled = useProxy && mViewModel?.useBundledTor?.value == false
walletProxyAddressEditText?.isEnabled = useProxy && mViewModel?.useBundledTor?.value == false
refreshProxy()
}
mViewModel?.useBundledTor?.observe(viewLifecycleOwner) { isChecked ->
walletProxyPortEditText?.isEnabled = !isChecked && mViewModel?.useProxy?.value == true
walletProxyAddressEditText?.isEnabled = !isChecked && mViewModel?.useProxy?.value == true
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
val indicatorCircle =
view?.findViewById<CircularProgressIndicator>(R.id.settings_tor_loading_progressindicator)
val torIcon = view?.findViewById<ImageView>(R.id.settings_tor_icon)
samouraiTorManager?.getTorStateLiveData()?.observe(viewLifecycleOwner) { state ->
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if (socketAddress.toString().isEmpty()) return@let
if (mViewModel?.useProxy?.value == true && mViewModel?.useBundledTor?.value == true) {
torIcon?.visibility = View.VISIBLE
indicatorCircle?.visibility = View.INVISIBLE
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
updateProxy(address, port)
}
}
indicatorCircle?.isIndeterminate = state.progressIndicator == 0
indicatorCircle?.progress = state.progressIndicator
when (state.state) {
EnumTorState.OFF -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.INVISIBLE
}
EnumTorState.STARTING, EnumTorState.STOPPING -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.VISIBLE
}
else -> {}
}
}
}
private fun updateProxy(address: String, port: String) {
walletProxyPortEditText?.setText(port)
walletProxyAddressEditText?.setText(address)
refreshProxy()
}
private fun refreshProxy() {
val proxyAddress = walletProxyAddressEditText?.text.toString()
val proxyPort = walletProxyPortEditText?.text.toString()
val savedProxyAddress = ProxyService.instance?.proxyAddress
val savedProxyPort = ProxyService.instance?.proxyPort
val currentWalletProxy = WalletManager.instance?.proxy
val newProxy = "$proxyAddress:$proxyPort"
if (proxyAddress != savedProxyAddress || proxyPort != savedProxyPort || (newProxy != currentWalletProxy && newProxy != ":"))
ProxyService.instance?.updateProxy(proxyAddress, proxyPort)
}
private fun displaySeedDialog(password: String) {
activity?.supportFragmentManager?.let { fragmentManager ->
val informationDialog = WalletKeysBottomSheetDialog()
informationDialog.password = password
informationDialog.show(fragmentManager, "information_seed_dialog")
}
}
override fun onPasswordSuccess(password: String) {
displaySeedDialog(password)
}
override fun onPasswordFail() {
Toast.makeText(context, R.string.bad_password, Toast.LENGTH_SHORT).show()
}
override fun onNodeSelected() {
val node = PrefService.instance?.node
selectNodeButton?.text = getString(R.string.node_button_text, node?.address)
refreshProxy()
activity?.runOnUiThread {
Toast.makeText(
activity,
activity?.getString(R.string.node_selected, node?.name ?: node?.host),
Toast.LENGTH_SHORT
).show()
}
}
override fun onClickedAddNode() {
activity?.supportFragmentManager?.let { fragmentManager ->
val addNodeDialog = AddNodeBottomSheetDialog()
addNodeDialog.listener = this
addNodeDialog.show(fragmentManager, "add_node_dialog")
}
}
override fun onClickedEditNode(node: Node?) {
activity?.supportFragmentManager?.let { fragmentManager ->
val editNodeDialog = EditNodeBottomSheetDialog()
editNodeDialog.listener = this
editNodeDialog.node = node
editNodeDialog.show(fragmentManager, "edit_node_dialog")
}
}
override fun onNodeAdded() {
activity?.supportFragmentManager?.let { fragmentManager ->
val dialog = NodeSelectionBottomSheetDialog()
dialog.listener = this
dialog.show(fragmentManager, "node_selection_dialog")
}
}
private fun navigate(destination: Int) {
val activity = activity
if (activity != null) {
val fm = activity.supportFragmentManager
val navHostFragment = fm.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
navHostFragment?.navController?.navigate(destination)
}
}
override fun onNodeDeleted(node: Node?) {
try {
val nodesArray = PrefService.instance?.getString(Constants.PREF_CUSTOM_NODES, "[]")
val jsonArray = JSONArray(nodesArray)
for (i in 0 until jsonArray.length()) {
val nodeJsonObject = jsonArray.getJSONObject(i)
val savedNode = fromJson(nodeJsonObject)
if (savedNode?.toNodeString() == node?.toNodeString()) jsonArray.remove(i)
}
saveNodesAndReopen(jsonArray)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onNodeEdited(oldNode: Node?, newNode: Node?) {
try {
val nodesArray = PrefService.instance?.getString(Constants.PREF_CUSTOM_NODES, "[]")
val jsonArray = JSONArray(nodesArray)
for (i in 0 until jsonArray.length()) {
val nodeJsonObject = jsonArray.getJSONObject(i)
val savedNode = fromJson(nodeJsonObject)
if (savedNode?.toNodeString() == oldNode?.toNodeString()) jsonArray.put(
i,
newNode?.toJson()
)
}
saveNodesAndReopen(jsonArray)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun saveNodesAndReopen(jsonArray: JSONArray) {
PrefService.instance?.edit()?.putString(Constants.PREF_CUSTOM_NODES, jsonArray.toString())
?.apply()
onNodeAdded()
}
}

View file

@ -1,44 +0,0 @@
package net.mynero.wallet.fragment.settings
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.Constants
class SettingsViewModel : ViewModel() {
private val _useProxy = MutableLiveData(false)
val useProxy: LiveData<Boolean> = _useProxy
private val _useBundledTor = MutableLiveData(false)
val useBundledTor: LiveData<Boolean> = _useBundledTor
init {
_useProxy.value = ProxyService.instance?.usingProxy
_useBundledTor.value = ProxyService.instance?.useBundledTor
}
fun setUseProxy(use: Boolean) {
_useProxy.value = use
ProxyService.instance?.usingProxy = use
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (use && ProxyService.instance?.useBundledTor == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
fun setUseBundledTor(use: Boolean) {
_useBundledTor.value = use
ProxyService.instance?.useBundledTor = use
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (use && ProxyService.instance?.usingProxy == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
}

View file

@ -1,5 +0,0 @@
package net.mynero.wallet.fragment.transaction
import androidx.lifecycle.ViewModel
class TransactionViewModel : ViewModel()

View file

@ -1,166 +0,0 @@
package net.mynero.wallet.fragment.utxos
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.R
import net.mynero.wallet.adapter.CoinsInfoAdapter
import net.mynero.wallet.adapter.CoinsInfoAdapter.CoinsInfoAdapterListener
import net.mynero.wallet.fragment.dialog.SendBottomSheetDialog
import net.mynero.wallet.model.CoinsInfo
import net.mynero.wallet.service.AddressService
import net.mynero.wallet.service.UTXOService
import net.mynero.wallet.util.MoneroThreadPoolExecutor
import net.mynero.wallet.util.UriData
class UtxosFragment : Fragment(), CoinsInfoAdapterListener, SendBottomSheetDialog.Listener {
private val adapter = CoinsInfoAdapter(this)
private var mViewModel: UtxosViewModel? = null
private var sendUtxosButton: Button? = null
private var churnUtxosButton: Button? = null
private var freezeUtxosButton: Button? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_utxos, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel = ViewModelProvider(this)[UtxosViewModel::class.java]
bindListeners(view)
bindObservers(view)
}
private fun bindListeners(view: View) {
freezeUtxosButton = view.findViewById(R.id.freeze_utxos_button)
sendUtxosButton = view.findViewById(R.id.send_utxos_button)
churnUtxosButton = view.findViewById(R.id.churn_utxos_button)
sendUtxosButton?.visibility = View.GONE
churnUtxosButton?.visibility = View.GONE
freezeUtxosButton?.visibility = View.GONE
freezeUtxosButton?.setOnClickListener {
Toast.makeText(context, "Toggling freeze status, please wait.", Toast.LENGTH_SHORT)
.show()
MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR?.execute {
UTXOService.instance?.toggleFrozen(adapter.selectedUtxos)
activity?.runOnUiThread {
adapter.clear()
sendUtxosButton?.visibility = View.GONE
churnUtxosButton?.visibility = View.GONE
freezeUtxosButton?.visibility = View.GONE
}
}
}
sendUtxosButton?.setOnClickListener {
val selectedKeyImages = ArrayList<String>()
for (coinsInfo in adapter.selectedUtxos.values) {
coinsInfo.keyImage?.let { keyImage -> selectedKeyImages.add(keyImage) }
}
activity?.supportFragmentManager?.let { fragmentManager ->
val sendDialog = SendBottomSheetDialog()
sendDialog.listener = this
sendDialog.selectedUtxos = selectedKeyImages
sendDialog.show(fragmentManager, null)
}
}
churnUtxosButton?.setOnClickListener {
val selectedKeyImages = ArrayList<String>()
for (coinsInfo in adapter.selectedUtxos.values) {
coinsInfo.keyImage?.let { keyImage -> selectedKeyImages.add(keyImage) }
}
activity?.supportFragmentManager?.let { fragmentManager ->
val sendDialog = SendBottomSheetDialog()
sendDialog.listener = this
sendDialog.isChurning = true
sendDialog.uriData =
AddressService.instance?.currentSubaddress()?.address?.let { address ->
UriData.parse(address)
}
sendDialog.selectedUtxos = selectedKeyImages
sendDialog.show(fragmentManager, null)
}
}
}
private fun bindObservers(view: View) {
val utxosRecyclerView =
view.findViewById<RecyclerView>(R.id.transaction_history_recyclerview)
val utxoService = UTXOService.instance
utxosRecyclerView.layoutManager = LinearLayoutManager(activity)
utxosRecyclerView.adapter = adapter
utxoService?.utxos?.observe(viewLifecycleOwner) { utxos: List<CoinsInfo> ->
val filteredUtxos = HashMap<String?, CoinsInfo>()
for (coinsInfo in utxos) {
if (!coinsInfo.isSpent) {
filteredUtxos[coinsInfo.pubKey] = coinsInfo
}
}
if (filteredUtxos.isEmpty()) {
utxosRecyclerView.visibility = View.GONE
} else {
adapter.submitList(filteredUtxos)
utxosRecyclerView.visibility = View.VISIBLE
}
}
}
override fun onUtxoSelected(coinsInfo: CoinsInfo) {
val selected = adapter.contains(coinsInfo)
if (selected) {
adapter.deselectUtxo(coinsInfo)
} else {
adapter.selectUtxo(coinsInfo)
}
var frozenExists = false
var unfrozenExists = false
for (selectedUtxo in adapter.selectedUtxos.values) {
if (selectedUtxo.isFrozen || UTXOService.instance?.isCoinFrozen(selectedUtxo) == true)
frozenExists = true
else {
unfrozenExists = true
}
}
val bothExist: Boolean = frozenExists && unfrozenExists
if (adapter.selectedUtxos.isEmpty()) {
sendUtxosButton?.visibility = View.GONE
churnUtxosButton?.visibility = View.GONE
freezeUtxosButton?.visibility = View.GONE
freezeUtxosButton?.setBackgroundResource(R.drawable.button_bg_left)
} else {
if (frozenExists) {
freezeUtxosButton?.setBackgroundResource(R.drawable.button_bg)
sendUtxosButton?.visibility = View.GONE
churnUtxosButton?.visibility = View.GONE
} else {
freezeUtxosButton?.setBackgroundResource(R.drawable.button_bg_left)
sendUtxosButton?.visibility = View.VISIBLE
churnUtxosButton?.visibility = View.VISIBLE
}
freezeUtxosButton?.visibility = View.VISIBLE
}
if (bothExist) {
freezeUtxosButton?.setText(R.string.toggle_freeze)
} else if (frozenExists) {
freezeUtxosButton?.setText(R.string.unfreeze)
} else if (unfrozenExists) {
freezeUtxosButton?.setText(R.string.freeze)
}
}
override fun onSentTransaction() {
adapter.clear()
churnUtxosButton?.visibility = View.GONE
sendUtxosButton?.visibility = View.GONE
freezeUtxosButton?.visibility = View.GONE
}
}

View file

@ -1,5 +0,0 @@
package net.mynero.wallet.fragment.utxos
import androidx.lifecycle.ViewModel
class UtxosViewModel : ViewModel()

View file

@ -16,7 +16,11 @@
*/
package net.mynero.wallet.service
import android.content.Context
import android.os.Handler
import android.util.Log
import android.widget.Toast
import net.mynero.wallet.R
import net.mynero.wallet.model.PendingTransaction
import net.mynero.wallet.model.TransactionOutput
import net.mynero.wallet.model.Wallet
@ -24,24 +28,42 @@ import net.mynero.wallet.model.Wallet.Companion.getAmountFromString
import net.mynero.wallet.model.Wallet.ConnectionStatus
import net.mynero.wallet.model.WalletListener
import net.mynero.wallet.model.WalletManager
import java.io.File
/**
* Handy class for starting a new thread that has a looper. The looper can then be
* used to create handler classes. Note that start() must still be called.
* The started Thread has a stck size of STACK_SIZE (=5MB)
*/
class MoneroHandlerThread(name: String, val listener: Listener?, wallet: Wallet) :
Thread(null, null, name, THREAD_STACK_SIZE), WalletListener {
private val wallet: Wallet
class MoneroHandlerThread(val wallet: Wallet, val context: Context) : Thread(null, null, "WalletService", THREAD_STACK_SIZE), WalletListener {
init {
this.wallet = wallet
private val handler = Handler(context.mainLooper)
companion object {
// from src/cryptonote_config.h
const val THREAD_STACK_SIZE = (5 * 1024 * 1024).toLong()
fun init(walletFile: File, password: String?, context: Context) {
val wallet = WalletManager.instance!!.openWallet(walletFile.absolutePath, password ?: "")
val thread = MoneroHandlerThread(wallet, context)
TxService(thread)
BalanceService(thread)
AddressService(thread)
HistoryService(thread)
BlockchainService(thread)
DaemonService(thread)
UTXOService(thread)
thread.start()
}
}
@Synchronized
override fun start() {
println("HERE1")
super.start()
listener?.onRefresh(false)
println("HERE2")
onRefresh(false)
}
override fun run() {
@ -101,7 +123,7 @@ class MoneroHandlerThread(name: String, val listener: Listener?, wallet: Wallet)
private fun tryRestartConnection() {
Log.d("MoneroHandlerThread.kt", "refreshed() Starting connection retry")
listener?.onConnectionFail()
onConnectionFail()
wallet.init(0)
wallet.startRefresh()
}
@ -111,7 +133,7 @@ class MoneroHandlerThread(name: String, val listener: Listener?, wallet: Wallet)
if (walletSynced) {
wallet.refreshCoins()
}
listener?.onRefresh(walletSynced)
onRefresh(walletSynced)
}
@Throws(Exception::class)
@ -179,13 +201,20 @@ class MoneroHandlerThread(name: String, val listener: Listener?, wallet: Wallet)
return pendingTx.commit("", true)
}
interface Listener {
fun onRefresh(walletSynced: Boolean)
fun onConnectionFail()
fun onRefresh(walletSynced: Boolean) {
println("onRefresh")
if (walletSynced) {
UTXOService.instance?.refreshUtxos()
}
HistoryService.instance?.refreshHistory()
BalanceService.instance?.refreshBalance()
BlockchainService.instance?.refreshBlockchain()
AddressService.instance?.refreshAddresses()
}
companion object {
// from src/cryptonote_config.h
const val THREAD_STACK_SIZE = (5 * 1024 * 1024).toLong()
fun onConnectionFail() {
handler.post {
Toast.makeText(context, R.string.connection_failed, Toast.LENGTH_SHORT).show()
}
}
}

View file

@ -20,6 +20,14 @@ object Constants {
const val NAV_ARG_TXINFO = "nav_arg_txinfo"
const val STREET_MODE_BALANCE = "#.############"
const val EXTRA_PREVENT_GOING_BACK = "prevent_going_back"
const val EXTRA_WALLET_NAME = "wallet_name"
const val EXTRA_WALLET_PASSWORD = "wallet_password"
const val EXTRA_SEND_ADDRESS = "send_address"
const val EXTRA_SEND_AMOUNT = "send_amount"
const val EXTRA_SEND_MAX = "send_max"
const val EXTRA_SEND_UTXOS = "send_utxos"
const val DEFAULT_PREF_MONEROCHAN = false
// Donation address is also specified in strings.xml, it is used as a tooltip in address fields

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="net.mynero.wallet.MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/main_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,44 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/oled_dialogBackgroundColor">
tools:context="net.mynero.wallet.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/enter_password_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="32dp"
android:layout_marginBottom="24dp"
android:text="@string/enter_password"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/wallet_password_edittext"
app:layout_constraintBottom_toTopOf="@+id/wallet_password_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="@+id/wallet_password_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="32dp"
android:layout_marginBottom="16dp"
android:background="@drawable/edittext_bg"
android:hint="@string/password"
android:inputType="textPassword"
app:layout_constraintBottom_toTopOf="@id/unlock_wallet_button"
app:layout_constraintBottom_toTopOf="@+id/unlock_wallet_button"
app:layout_constraintEnd_toStartOf="@id/paste_password_imagebutton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/enter_password_textview" />
app:layout_constraintStart_toStartOf="parent" />
<ImageButton
android:id="@+id/paste_password_imagebutton"
@ -66,9 +64,8 @@
android:background="@drawable/button_bg"
android:text="@string/unlock"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wallet_password_edittext" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,300 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/oled_dialogBackgroundColor"
android:fitsSystemWindows="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- CREATE LAYOUT -->
<TextView
android:id="@+id/send_monero_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:text="@string/send_monero"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/selected_utxos_value_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/selected_utxos_value_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:text="@string/selected_utxos_value"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/address_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/send_monero_textview" />
<EditText
android:id="@+id/address_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp"
android:background="@drawable/edittext_bg"
android:ellipsize="middle"
android:hint="@string/address"
android:singleLine="true"
app:layout_constraintBottom_toTopOf="@id/donate_label_textview"
app:layout_constraintEnd_toStartOf="@id/paste_address_imagebutton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/selected_utxos_value_textview"
tools:visibility="visible" />
<TextView
android:id="@+id/donate_label_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="16dp"
android:text="@string/donate_label"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/amount_edittext"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/address_edittext" />
<TextView
android:id="@+id/donating_label_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="16dp"
android:text="@string/donating_label"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/amount_edittext"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/address_edittext"
app:layout_constraintVertical_bias="1.0"
tools:visibility="visible" />
<ImageButton
android:id="@+id/paste_address_imagebutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@android:color/transparent"
android:minWidth="48dp"
android:minHeight="48dp"
android:src="@drawable/ic_content_paste_24dp"
app:layout_constraintBottom_toBottomOf="@id/address_edittext"
app:layout_constraintEnd_toStartOf="@id/scan_address_imagebutton"
app:layout_constraintStart_toEndOf="@id/address_edittext"
app:layout_constraintTop_toTopOf="@id/address_edittext"
tools:ignore="SpeakableTextPresentCheck"
tools:visibility="visible" />
<ImageButton
android:id="@+id/scan_address_imagebutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:background="@android:color/transparent"
android:minWidth="48dp"
android:minHeight="48dp"
android:src="@drawable/ic_scan"
app:layout_constraintBottom_toBottomOf="@id/address_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/paste_address_imagebutton"
app:layout_constraintTop_toTopOf="@id/address_edittext"
tools:ignore="SpeakableTextPresentCheck"
tools:visibility="visible" />
<EditText
android:id="@+id/amount_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:background="@drawable/edittext_bg"
android:hint="@string/amount"
android:inputType="numberDecimal"
app:layout_constraintBottom_toTopOf="@id/tx_fee_radiogroup"
app:layout_constraintEnd_toStartOf="@id/send_max_button"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/sending_all_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="12dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="@string/sending_all"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/amount_edittext"
app:layout_constraintEnd_toStartOf="@id/send_max_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/amount_edittext"
tools:visibility="visible" />
<Button
android:id="@+id/send_max_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:background="@drawable/button_bg"
android:text="@string/send_max"
app:layout_constraintBottom_toBottomOf="@id/amount_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/amount_edittext"
app:layout_constraintTop_toTopOf="@id/amount_edittext"
tools:visibility="visible" />
<TextView
android:id="@+id/tx_fee_radiogroup_label_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="@string/fee_priority"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@id/tx_fee_radiogroup"
app:layout_constraintEnd_toStartOf="@id/tx_fee_radiogroup"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tx_fee_radiogroup" />
<RadioGroup
android:id="@+id/tx_fee_radiogroup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="24dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/create_tx_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tx_fee_radiogroup_label_textview"
app:layout_constraintTop_toBottomOf="@id/send_max_button">
<RadioButton
android:id="@+id/low_fee_radiobutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/low"
android:textSize="16sp" />
<RadioButton
android:id="@+id/med_fee_radiobutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/medium"
android:textSize="16sp" />
<RadioButton
android:id="@+id/high_fee_radiobutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/high"
android:textSize="16sp" />
</RadioGroup>
<Button
android:id="@+id/create_tx_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:background="@drawable/button_bg"
android:text="@string/create"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tx_fee_radiogroup_label_textview"
tools:visibility="visible" />
<!-- SEND LAYOUT -->
<TextView
android:id="@+id/address_pending_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:ellipsize="middle"
android:singleLine="true"
android:text="@string/tx_address_text"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/amount_pending_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/send_monero_textview"
tools:visibility="gone" />
<TextView
android:id="@+id/amount_pending_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/tx_amount_text"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/fee_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/address_pending_textview"
tools:visibility="gone" />
<TextView
android:id="@+id/fee_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/tx_fee_text"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/send_tx_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/amount_pending_textview"
tools:visibility="gone" />
<Button
android:id="@+id/send_tx_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="16dp"
android:background="@drawable/button_bg"
android:text="@string/send"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/fee_textview"
tools:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -1,95 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_nav"
app:startDestination="@id/main_fragment">
<fragment
android:id="@+id/main_fragment"
android:name="net.mynero.wallet.fragment.home.HomeFragment"
android:label="fragment_main"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/nav_to_settings"
app:destination="@id/settings_fragment"
app:enterAnim="@anim/slide_in_from_right"
app:exitAnim="@anim/slide_out_to_left"
app:popEnterAnim="@anim/slide_in_from_left"
app:popExitAnim="@anim/slide_out_to_right"/>
<action
android:id="@+id/nav_to_receive"
app:destination="@id/receive_fragment"
app:enterAnim="@anim/slide_in_from_bottom"
app:exitAnim="@anim/slide_out_to_top"
app:popEnterAnim="@anim/slide_in_from_top"
app:popExitAnim="@anim/slide_out_to_bottom"/>
<action
android:id="@+id/nav_to_send"
app:destination="@id/send_fragment"
app:enterAnim="@anim/slide_in_from_bottom"
app:exitAnim="@anim/slide_out_to_top"
app:popEnterAnim="@anim/slide_in_from_top"
app:popExitAnim="@anim/slide_out_to_bottom"/>
<action
android:id="@+id/nav_to_onboarding"
app:destination="@id/onboarding_fragment" />
<action
android:id="@+id/nav_to_transaction"
app:destination="@id/transaction_fragment"
app:enterAnim="@anim/slide_in_from_left"
app:exitAnim="@anim/slide_out_to_right"
app:popEnterAnim="@anim/slide_in_from_right"
app:popExitAnim="@anim/slide_out_to_left">
<argument
android:name="nav_arg_txinfo"
app:argType="net.mynero.wallet.model.TransactionInfo"
app:nullable="true" />
</action>
</fragment>
<fragment
android:id="@+id/settings_fragment"
android:name="net.mynero.wallet.fragment.settings.SettingsFragment"
android:label="fragment_send_amount"
tools:layout="@layout/fragment_settings">
<action
android:id="@+id/nav_to_utxos"
app:destination="@id/utxos_fragment"
app:enterAnim="@anim/slide_in_from_right"
app:exitAnim="@anim/slide_out_to_left"
app:popEnterAnim="@anim/slide_in_from_left"
app:popExitAnim="@anim/slide_out_to_right"/>
</fragment>
<fragment
android:id="@+id/send_fragment"
android:name="net.mynero.wallet.fragment.send.SendFragment"
android:label="fragment_send_amount"
tools:layout="@layout/fragment_send" />
<fragment
android:id="@+id/receive_fragment"
android:name="net.mynero.wallet.fragment.receive.ReceiveFragment"
android:label="fragment_send_amount"
tools:layout="@layout/fragment_receive" />
<fragment
android:id="@+id/utxos_fragment"
android:name="net.mynero.wallet.fragment.utxos.UtxosFragment"
android:label="fragment_utxos"
tools:layout="@layout/fragment_utxos" />
<fragment
android:id="@+id/onboarding_fragment"
android:name="net.mynero.wallet.fragment.onboarding.OnboardingFragment"
android:label="fragment_onboarding"
tools:layout="@layout/fragment_onboarding">
<action
android:id="@+id/nav_to_home"
app:destination="@id/main_fragment"
app:enterAnim="@anim/slide_in_from_right"
app:exitAnim="@anim/slide_out_to_left"
app:popEnterAnim="@anim/slide_in_from_left"
app:popExitAnim="@anim/slide_out_to_right"/>
</fragment>
<fragment
android:id="@+id/transaction_fragment"
android:name="net.mynero.wallet.fragment.transaction.TransactionFragment"
android:label="fragment_onboarding"
tools:layout="@layout/fragment_transaction" />
</navigation>