mirror of
https://codeberg.org/anoncontributorxmr/mysu.git
synced 2024-11-12 22:23:32 +01:00
chore: replace some fragments with activities
This commit is contained in:
parent
d1645a43ec
commit
66e61d9064
@ -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 {
|
||||
|
25
app/proguard-rules.pro
vendored
25
app/proguard-rules.pro
vendored
@ -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
|
@ -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"
|
||||
|
@ -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
|
||||
) {
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
608
app/src/main/java/net/mynero/wallet/OnboardingActivity.kt
Normal file
608
app/src/main/java/net/mynero/wallet/OnboardingActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
64
app/src/main/java/net/mynero/wallet/PasswordActivity.kt
Normal file
64
app/src/main/java/net/mynero/wallet/PasswordActivity.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -148,4 +143,37 @@ 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
|
||||
}
|
||||
}
|
596
app/src/main/java/net/mynero/wallet/SendActivity.kt
Normal file
596
app/src/main/java/net/mynero/wallet/SendActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
331
app/src/main/java/net/mynero/wallet/SettingsActivity.kt
Normal file
331
app/src/main/java/net/mynero/wallet/SettingsActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
75
app/src/main/java/net/mynero/wallet/StartActivity.kt
Normal file
75
app/src/main/java/net/mynero/wallet/StartActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -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) {
|
145
app/src/main/java/net/mynero/wallet/UtxosActivity.kt
Normal file
145
app/src/main/java/net/mynero/wallet/UtxosActivity.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package net.mynero.wallet.fragment.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class HomeViewModel : ViewModel()
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package net.mynero.wallet.fragment.transaction
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class TransactionViewModel : ViewModel()
|
@ -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
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package net.mynero.wallet.fragment.utxos
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class UtxosViewModel : ViewModel()
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue
Block a user