feat: EXPERIMENTAL branch for 0.6.0 release. Contains lots of bugs and is generally unpolished, use at your own risk

This commit is contained in:
- 2024-12-22 12:56:33 +01:00
parent e82930ae29
commit 4ed7071872
72 changed files with 2357 additions and 2652 deletions

View File

@ -6,11 +6,11 @@ android {
ndkVersion '27.1.12297006'
defaultConfig {
applicationId "net.mynero.wallet"
minSdkVersion 22
minSdkVersion 26
targetSdkVersion 34
compileSdk 34
versionCode 51004
versionName "0.5.10.4 'Fluorine Fermi'"
versionName "0.6.0 'Fluorine Fermi'"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
@ -123,17 +123,19 @@ static def getId(name) {
dependencies {
// Android stuff
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.5'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5'
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.8.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7'
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.8.5'
// Logging
implementation 'com.jakewharton.timber:timber:5.0.1'
// Slide to Send tx sliders
implementation 'com.ncorti:slidetoact:0.9.0'

View File

@ -76,7 +76,7 @@
</activity>
<activity
android:name=".UtxosActivity"
android:name=".EnotesActivity"
android:exported="true">
</activity>
@ -86,6 +86,17 @@
android:stateNotNeeded="true"
tools:replace="android:screenOrientation" />
<service
android:name=".service.wallet.WalletService"
android:description="@string/service_description"
android:exported="false"
android:foregroundServiceType="specialUse"
android:label="Monero Wallet Service">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Keeps app in sync with the blockchain" />
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@ -145,6 +145,19 @@ struct MyWalletListener : Monero::WalletListener {
if (jlistener == nullptr) return;
LOGD("moneySpent %"
PRIu64, amount);
JNIEnv *jenv;
int envStat = attachJVM(&jenv);
if (envStat == JNI_ERR) return;
jmethodID listenerClass_moneySpent = jenv->GetMethodID(class_WalletListener, "moneySpent",
"(Ljava/lang/String;J)V");
jstring jTxId = jenv->NewStringUTF(txId.c_str());
jlong jAmount = static_cast<jlong>(amount);
jenv->CallVoidMethod(jlistener, listenerClass_moneySpent, jTxId, jAmount);
jenv->DeleteLocalRef(jTxId);
detachJVM(jenv, envStat);
}
/**
@ -157,6 +170,19 @@ struct MyWalletListener : Monero::WalletListener {
if (jlistener == nullptr) return;
LOGD("moneyReceived %"
PRIu64, amount);
JNIEnv *jenv;
int envStat = attachJVM(&jenv);
if (envStat == JNI_ERR) return;
jmethodID listenerClass_moneyReceived = jenv->GetMethodID(class_WalletListener, "moneyReceived",
"(Ljava/lang/String;J)V");
jstring jTxId = jenv->NewStringUTF(txId.c_str());
jlong jAmount = static_cast<jlong>(amount);
jenv->CallVoidMethod(jlistener, listenerClass_moneyReceived, jTxId, jAmount);
jenv->DeleteLocalRef(jTxId);
detachJVM(jenv, envStat);
}
/**
@ -169,6 +195,20 @@ struct MyWalletListener : Monero::WalletListener {
if (jlistener == nullptr) return;
LOGD("unconfirmedMoneyReceived %"
PRIu64, amount);
JNIEnv *jenv;
int envStat = attachJVM(&jenv);
if (envStat == JNI_ERR) return;
jmethodID listenerClass_unconfirmedMoneyReceived = jenv->GetMethodID(class_WalletListener, "unconfirmedMoneyReceived",
"(Ljava/lang/String;J)V");
jstring jTxId = jenv->NewStringUTF(txId.c_str());
jlong jAmount = static_cast<jlong>(amount);
jenv->CallVoidMethod(jlistener, listenerClass_unconfirmedMoneyReceived, jTxId, jAmount);
jenv->DeleteLocalRef(jTxId);
detachJVM(jenv, envStat);
}
/**
@ -1573,6 +1613,13 @@ Java_net_mynero_wallet_model_TransactionHistory_refreshJ(JNIEnv *env, jobject in
return cpp2java(env, history->getAll());
}
JNIEXPORT jobject JNICALL
Java_net_mynero_wallet_model_TransactionHistory_getAllJ(JNIEnv *env, jobject instance) {
Monero::TransactionHistory *history = getHandle<Monero::TransactionHistory>(env,
instance);
return cpp2java(env, history->getAll());
}
// TransactionInfo is implemented in Java - no need here
JNIEXPORT jint JNICALL

View File

@ -0,0 +1,147 @@
package net.mynero.wallet
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.adapter.EnotesAdapter
import net.mynero.wallet.model.Balance
import net.mynero.wallet.model.CoinsInfo
import net.mynero.wallet.service.wallet.WalletService
import net.mynero.wallet.service.wallet.WalletServiceObserver
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.PreferenceUtils
class EnotesActivity : AppCompatActivity(), WalletServiceObserver {
private val viewModel: EnotesViewModel by viewModels()
private var walletService: WalletService? = null
private lateinit var freezeUtxosButton: Button
private lateinit var sendUtxosButton: Button
private lateinit var unfreezeUtxosButton: Button
private lateinit var enotesRecyclerView: RecyclerView
private lateinit var adapter: EnotesAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_enotes)
freezeUtxosButton = findViewById(R.id.freeze_enotes_button)
sendUtxosButton = findViewById(R.id.send_enotes_button)
unfreezeUtxosButton = findViewById(R.id.unfreeze_enotes_button)
enotesRecyclerView = findViewById(R.id.transaction_history_recyclerview)
val streetMode = PreferenceUtils.isStreetMode(this)
adapter = EnotesAdapter(streetMode, object : EnotesAdapter.EnotesAdapterListener {
override fun onEnoteSelected(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)
frozenExists = true
else {
unfrozenExists = true
}
}
if (adapter.selectedUtxos.isEmpty()) {
sendUtxosButton.isEnabled = false
freezeUtxosButton.isEnabled = false
unfreezeUtxosButton.isEnabled = false
} else {
sendUtxosButton.isEnabled = unfrozenExists
freezeUtxosButton.isEnabled = unfrozenExists
unfreezeUtxosButton.isEnabled = frozenExists
}
}
})
bindListeners()
bindObservers()
bindService(Intent(applicationContext, WalletService::class.java), connection, 0)
}
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val walletService = (service as WalletService.WalletServiceBinder).service
this@EnotesActivity.walletService = walletService
walletService.addObserver(this@EnotesActivity)
walletService.refreshEnotes()
}
override fun onServiceDisconnected(className: ComponentName) {
walletService = null
}
}
private fun bindListeners() {
sendUtxosButton.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.putStringArrayListExtra(Constants.EXTRA_SEND_ENOTES, selectedKeyImages)
startActivity(intent)
}
freezeUtxosButton.setOnClickListener {
Toast.makeText(this, "Freezing enotes, please wait.", Toast.LENGTH_SHORT)
.show()
walletService?.freezeEnote(adapter.selectedUtxos.keys.filterNotNull().toList())
}
unfreezeUtxosButton.setOnClickListener {
Toast.makeText(this, "Thawing enotes, please wait.", Toast.LENGTH_SHORT)
.show()
walletService?.thawEnote(adapter.selectedUtxos.keys.filterNotNull().toList())
}
}
private fun bindObservers() {
enotesRecyclerView.layoutManager = LinearLayoutManager(this)
enotesRecyclerView.adapter = adapter
viewModel.enotes.observe(this) { enotes: List<CoinsInfo> ->
val filteredEnotes = enotes.filter { !it.isSpent }
if (filteredEnotes.isEmpty()) {
enotesRecyclerView.visibility = View.GONE
} else {
adapter.submitList(filteredEnotes)
adapter.clear()
enotesRecyclerView.visibility = View.VISIBLE
}
}
}
override fun onEnotesRefreshed(enotes: List<CoinsInfo>, balance: Balance) {
viewModel.updateEnotes(enotes)
}
}
internal class EnotesViewModel : ViewModel() {
private val _enotes: MutableLiveData<List<CoinsInfo>> = MutableLiveData()
val enotes: LiveData<List<CoinsInfo>> = _enotes
fun updateEnotes(enotes: List<CoinsInfo>) {
_enotes.postValue(enotes)
}
}

View File

@ -1,49 +1,89 @@
package net.mynero.wallet
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.util.Log
import android.os.IBinder
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.mynero.wallet.adapter.TransactionInfoAdapter
import net.mynero.wallet.data.DefaultNode
import net.mynero.wallet.model.Balance
import net.mynero.wallet.model.CoinsInfo
import net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.BalanceService
import net.mynero.wallet.service.BlockchainService
import net.mynero.wallet.service.DaemonService
import net.mynero.wallet.service.HistoryService
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.service.wallet.WalletServiceObserver
import net.mynero.wallet.service.wallet.WalletService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.PreferenceUtils
import timber.log.Timber
class HomeActivity : AppCompatActivity() {
class HomeActivity : AppCompatActivity(), WalletServiceObserver {
private val viewModel: HomeActivityViewModel by viewModels()
private var walletService: WalletService? = null
private lateinit var walletName: String
private lateinit var walletPassword: String
private lateinit var progressBar: ProgressBar
private lateinit var progressBarText: TextView
private lateinit var settingsImageView: ImageView
private lateinit var sendButton: Button
private lateinit var receiveButton: Button
private lateinit var txHistoryRecyclerView: RecyclerView
private lateinit var unlockedBalanceTextView: TextView
private lateinit var frozenBalanceTextView: TextView
private lateinit var lockedBalanceTextView: TextView
private lateinit var transactionInfoAdapter: TransactionInfoAdapter
private var startHeight: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
val settingsImageView = findViewById<ImageView>(R.id.settings_imageview)
val sendButton = findViewById<Button>(R.id.send_button)
val receiveButton = findViewById<Button>(R.id.receive_button)
walletName = intent.extras?.getString(Constants.EXTRA_WALLET_NAME)!!
walletPassword = intent.extras?.getString(Constants.EXTRA_WALLET_PASSWORD)!!
val txHistoryRecyclerView =
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
settingsImageView = findViewById(R.id.settings_imageview)
sendButton = findViewById(R.id.send_button)
receiveButton = findViewById(R.id.receive_button)
txHistoryRecyclerView = findViewById(R.id.transaction_history_recyclerview)
unlockedBalanceTextView = findViewById(R.id.balance_unlocked_textview)
frozenBalanceTextView = findViewById(R.id.balance_frozen_textview)
lockedBalanceTextView = findViewById(R.id.balance_locked_textview)
progressBar = findViewById(R.id.sync_progress_bar)
progressBarText = findViewById(R.id.sync_progress_text)
val streetMode = PreferenceUtils.isStreetMode(this)
transactionInfoAdapter = TransactionInfoAdapter(streetMode, object : TransactionInfoAdapter.TxInfoAdapterListener {
override fun onClickTransaction(txInfo: TransactionInfo) {
val intent = Intent(this@HomeActivity, TransactionActivity::class.java)
intent.putExtra(Constants.NAV_ARG_TXINFO, txInfo.hash)
startActivity(intent)
}
})
txHistoryRecyclerView.layoutManager = LinearLayoutManager(this)
txHistoryRecyclerView.adapter = transactionInfoAdapter
settingsImageView.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
@ -55,107 +95,12 @@ class HomeActivity : AppCompatActivity() {
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 =
if (proxy.isNotEmpty() && ProxyService.instance?.usingProxy == true) proxy else ""
WalletManager.instance?.setProxy(finalProxy)
WalletManager.instance?.wallet?.setProxy(finalProxy)
WalletManager.instance?.wallet?.init(0)
WalletManager.instance?.wallet?.startRefresh()
}
viewModel.balance.observe(this) { balance ->
updateBalances(balance)
}
DaemonService.instance?.daemonChangeEvents?.observe(this) { daemon ->
lifecycleScope.launch(Dispatchers.IO) {
Log.d("HomeFragment", "Updating daemon, restarting wallet. daemon=$daemon")
WalletManager.instance?.setDaemon(daemon)
WalletManager.instance?.wallet?.init(0)
WalletManager.instance?.wallet?.setTrustedDaemon(daemon.trusted)
WalletManager.instance?.wallet?.startRefresh()
}
}
balanceService?.balanceInfo?.observe(this) { balanceInfo ->
if (balanceInfo != null) {
unlockedBalanceTextView.text = balanceInfo.unlockedDisplay
if (balanceInfo.lockedDisplay == Constants.STREET_MODE_BALANCE || balanceInfo.isLockedBalanceZero) {
lockedBalanceTextView.visibility = View.INVISIBLE
} else {
lockedBalanceTextView.text = getString(
R.string.wallet_locked_balance_text,
balanceInfo.lockedDisplay
)
lockedBalanceTextView.visibility = View.VISIBLE
}
}
}
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) {
startHeight = height
}
val daemonHeight = blockchainService.daemonHeight
val n = daemonHeight - height
val x = Math.round(100 - (100f * n / (1f * daemonHeight - startHeight)))
progressBar.isIndeterminate = height <= 1 || daemonHeight <= 0
if (height > 1 && daemonHeight > 1) {
progressBar.progress = x
progressBarText.visibility = View.VISIBLE
progressBarText.text = "Syncing... $n blocks remaining"
} else {
progressBarText.visibility = View.GONE
}
} else {
progressBar.visibility = View.INVISIBLE
progressBarText.visibility = View.VISIBLE
progressBarText.text = "Synchronized at $height"
}
}
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(this) { history: List<TransactionInfo> ->
if (history.isEmpty()) {
// DISPLAYING EMPTY WALLET HISTORY
val wallet = WalletManager.instance?.wallet
val textResId: Int
val botImgResId = if (wallet != null && wallet.isSynchronized) {
textResId = R.string.no_history_nget_some_monero_in_here
R.drawable.xmrchan_empty2 // img for synchronized
} else {
textResId = R.string.no_history_loading
R.drawable.xmrchan_loading2 // img for loading
}
txHistoryRecyclerView.visibility = View.GONE
displayEmptyHistory(true, this, textResId, botImgResId)
} else {
// POPULATED WALLET HISTORY
val sortedHistory = history.sortedByDescending { it.timestamp }
if (sortedHistory.size > 100) {
adapter.submitList(sortedHistory.subList(0, 99))
} else {
adapter.submitList(sortedHistory)
}
txHistoryRecyclerView.visibility = View.VISIBLE
displayEmptyHistory(
false,
this,
R.string.no_history_nget_some_monero_in_here,
R.drawable.xmrchan_loading2
)
}
viewModel.transactions.observe(this) { history ->
updateTransactionHistory(history)
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
@ -167,21 +112,48 @@ class HomeActivity : AppCompatActivity() {
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
if (WalletManager.instance?.proxy != proxyString)
if (WalletManager.instance.proxy != proxyString)
refreshProxy(address, port)
}
}
}
connectWalletService()
}
override fun onResume() {
super.onResume()
updateBalances(viewModel.balance.value)
updateTransactionHistory(viewModel.transactions.value)
}
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val walletService = (service as WalletService.WalletServiceBinder).service
this@HomeActivity.walletService = walletService
walletService.addObserver(this@HomeActivity)
val node = PreferenceUtils.getOrSetDefaultNode(this@HomeActivity, DefaultNode.defaultNode())
walletService.openWallet(walletName, walletPassword, node.address, node.username ?: "", node.password ?: "", node.trusted, PreferenceUtils.getProxyIfEnabled(applicationContext) ?: "")
}
override fun onServiceDisconnected(className: ComponentName) {
walletService = null
}
}
private fun connectWalletService() {
val intent = Intent(applicationContext, WalletService::class.java)
startService(intent)
bindService(intent, connection, BIND_AUTO_CREATE)
}
private fun refreshProxy(proxyAddress: String, proxyPort: String) {
val cachedProxyAddress = ProxyService.instance?.proxyAddress
val cachedProxyPort = ProxyService.instance?.proxyPort
val currentWalletProxy = WalletManager.instance?.proxy
val currentWalletProxy = WalletManager.instance.proxy
val newProxy = "$proxyAddress:$proxyPort"
if ((proxyAddress != cachedProxyAddress) || (proxyPort != cachedProxyPort) || (newProxy != currentWalletProxy && newProxy != ":")) {
ProxyService.instance?.updateProxy(proxyAddress, proxyPort)
// ProxyService.instance?.updateProxy(proxyAddress, proxyPort)
}
}
@ -189,26 +161,142 @@ class HomeActivity : AppCompatActivity() {
display: Boolean,
view: HomeActivity,
textResId: Int,
botImgResId: Int
) {
val mnrjTextView = view.findViewById<TextView>(R.id.monerochan_empty_tx_textview)
val textView = view.findViewById<TextView>(R.id.empty_tx_textview)
val botImageView = view.findViewById<ImageView>(R.id.monerochan_imageview)
view.findViewById<View>(R.id.no_history_layout).visibility =
if (display) View.VISIBLE else View.GONE
val displayMonerochan =
PrefService.instance?.getBoolean(Constants.PREF_MONEROCHAN, Constants.DEFAULT_PREF_MONEROCHAN) == true
if (displayMonerochan) {
botImageView.visibility = View.VISIBLE
mnrjTextView.visibility = View.VISIBLE
textView.visibility = View.GONE
} else {
botImageView.visibility = View.GONE
mnrjTextView.visibility = View.GONE
textView.visibility = View.VISIBLE
}
botImageView.setImageResource(botImgResId)
mnrjTextView.setText(textResId)
textView.setText(textResId)
}
}
override fun onBlockchainHeightFetched(height: Long) {
updateSynchronizationProgress()
}
override fun onEnotesRefreshed(enotes: List<CoinsInfo>, balance: Balance) {
viewModel.updateBalance(balance)
}
override fun onWalletHistoryRefreshed(transactions: List<TransactionInfo>) {
viewModel.updateTransactions(transactions)
}
override fun onRefreshed() {
updateSynchronizationProgress()
}
override fun onNewBlock(height: Long) {
updateSynchronizationProgress()
}
private fun updateBalances(balance: Balance?) {
if (balance != null) {
val streetMode = PreferenceUtils.isStreetMode(applicationContext)
unlockedBalanceTextView.text = if (!streetMode) Wallet.getDisplayAmount(balance.unlocked) else Constants.STREET_MODE_BALANCE
if (balance.frozen != 0L) {
val textFrozenBalance = if (!streetMode) Wallet.getDisplayAmount(balance.frozen) else Constants.STREET_MODE_BALANCE
val formattedFrozenBalance = if (balance.frozen < 0) "- $textFrozenBalance" else "+ $textFrozenBalance"
frozenBalanceTextView.text = "$formattedFrozenBalance frozen"
frozenBalanceTextView.visibility = View.VISIBLE
} else {
frozenBalanceTextView.visibility = View.GONE
}
if (balance.pending != 0L) {
val textUnconfirmedBalance = if (!streetMode) Wallet.getDisplayAmount(balance.pending) else Constants.STREET_MODE_BALANCE
val formattedUnconfirmedBalance = if (balance.pending < 0) "- $textUnconfirmedBalance" else "+ $textUnconfirmedBalance"
lockedBalanceTextView.text = "$formattedUnconfirmedBalance unconfirmed"
lockedBalanceTextView.visibility = View.VISIBLE
} else {
lockedBalanceTextView.visibility = View.GONE
}
}
}
private fun updateTransactionHistory(history: List<TransactionInfo>?) {
if (history.isNullOrEmpty()) {
val wallet = walletService?.getWallet()
val textResId: Int = if (wallet != null && wallet.isSynchronized) {
R.string.no_history_nget_some_monero_in_here
} else {
R.string.no_history_loading
}
txHistoryRecyclerView.visibility = View.GONE
displayEmptyHistory(true, this, textResId)
} else {
// POPULATED WALLET HISTORY
val sortedHistory = history.sortedByDescending { it.timestamp }
if (sortedHistory.size > 100) {
transactionInfoAdapter.submitList(sortedHistory.subList(0, 99))
} else {
transactionInfoAdapter.submitList(sortedHistory)
}
transactionInfoAdapter.submitStreetMode(PreferenceUtils.isStreetMode(applicationContext))
txHistoryRecyclerView.visibility = View.VISIBLE
displayEmptyHistory(
false,
this,
R.string.no_history_nget_some_monero_in_here,
)
}
}
private fun updateSynchronizationProgress() {
walletService?.let { walletService ->
val walletBeginSyncHeight = walletService.getWalletBeginSyncHeight()
val walletHeight = walletService.getWalletOrThrow().getBlockChainHeightJ()
val daemonHeight = walletService.getDaemonHeight()
Timber.e("walletHeight = $walletHeight, daemonHeight = $daemonHeight")
val diff = daemonHeight - walletHeight
val synchronized = daemonHeight > 0 && diff < 2
runOnUiThread {
progressBarText.visibility = View.VISIBLE
if (synchronized) {
progressBar.visibility = View.INVISIBLE
progressBarText.text = "Synchronized at ${walletHeight}"
} else if (daemonHeight > 0) {
progressBar.isIndeterminate = false
// Google employs the very rare kind of engineers to work on Android - engineers with negative fucking IQ
// try switching the order of setting max and min here to see their brilliance and true ingenuity
// and after you do that, try to find an explanation (or even a mention) of this behaviour in the docs:
// https://developer.android.com/reference/android/widget/ProgressBar.html
progressBar.max = daemonHeight.toInt()
progressBar.min = walletBeginSyncHeight.toInt()
progressBar.setProgress(walletHeight.toInt(), true)
progressBar.visibility = View.VISIBLE
progressBarText.text = "Synchronizing! $walletHeight / $daemonHeight ($diff blocks remaining)"
} else {
progressBar.visibility = View.INVISIBLE
progressBar.isIndeterminate = true
progressBarText.text = "Connecting..."
}
}
}
}
}
internal class HomeActivityViewModel : ViewModel() {
val walletPassword: String = ""
private val _height: MutableLiveData<HeightInfo> = MutableLiveData()
val height: LiveData<HeightInfo> = _height
private val _transactions = MutableLiveData<List<TransactionInfo>>()
val transactions: LiveData<List<TransactionInfo>> = _transactions
private val _balance: MutableLiveData<Balance> = MutableLiveData()
val balance: LiveData<Balance> = _balance
fun updateHeight(newHeightInfo: HeightInfo) {
_height.postValue(newHeightInfo)
}
fun updateTransactions(newTransactions: List<TransactionInfo>) {
_transactions.postValue(newTransactions)
}
fun updateBalance(newBalance: Balance) {
_balance.postValue(newBalance)
}
}
internal data class HeightInfo(val daemon: Long, val wallet: Long)

View File

@ -4,18 +4,21 @@ import android.app.Application
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.NightmodeHelper
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import timber.log.Timber
class MoneroApplication : Application() {
var executor: ExecutorService? = null
private set
companion object {
init {
System.loadLibrary("monerujo")
}
}
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
PrefService(this)
ProxyService(this)
NightmodeHelper.preferredNightmode
executor = Executors.newFixedThreadPool(16)
}
}

View File

@ -15,34 +15,38 @@ import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
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.DefaultNode
import net.mynero.wallet.data.Node
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog
import net.mynero.wallet.listener.NodeSelectionDialogListenerAdapter
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.PreferenceUtils
import net.mynero.wallet.util.RestoreHeight
import timber.log.Timber
import java.io.File
import java.util.Calendar
class OnboardingActivity : AppCompatActivity() {
private lateinit var mViewModel: OnboardingViewModel
private val viewModel: OnboardingViewModel by viewModels()
private lateinit var walletProxyAddressEditText: EditText
private lateinit var walletProxyPortEditText: EditText
private lateinit var walletPasswordEditText: EditText
@ -57,7 +61,6 @@ class OnboardingActivity : AppCompatActivity() {
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
@ -66,7 +69,6 @@ class OnboardingActivity : AppCompatActivity() {
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)
@ -81,12 +83,11 @@ class OnboardingActivity : AppCompatActivity() {
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
seedOffsetCheckbox.isChecked = viewModel.useOffset
val usingProxy = ProxyService.instance?.usingProxy == true
val usingBundledTor = ProxyService.instance?.useBundledTor == true
@ -96,15 +97,15 @@ class OnboardingActivity : AppCompatActivity() {
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?.name)
val node = PreferenceUtils.getOrSetDefaultNode(this, DefaultNode.defaultNode())
selectNodeButton.text = getString(R.string.node_button_text, node.name)
bindListeners()
bindObservers()
}
private fun bindObservers() {
mViewModel.passphrase.observe(this) { text ->
viewModel.passphrase.observe(this) { text ->
if (text.isEmpty()) {
walletPasswordConfirmEditText.text = null
walletPasswordConfirmEditText.visibility = View.GONE
@ -113,7 +114,7 @@ class OnboardingActivity : AppCompatActivity() {
}
}
mViewModel.showMoreOptions.observe(this) { show: Boolean ->
viewModel.showMoreOptions.observe(this) { show: Boolean ->
if (show) {
moreOptionsChevronImageView.setImageResource(R.drawable.ic_keyboard_arrow_up)
advancedOptionsLayout.visibility = View.VISIBLE
@ -123,11 +124,11 @@ class OnboardingActivity : AppCompatActivity() {
}
}
mViewModel.enableButton.observe(this) { enable: Boolean ->
viewModel.enableButton.observe(this) { enable: Boolean ->
createWalletButton.isEnabled = enable
}
mViewModel.seedType.observe(this) { seedType: OnboardingViewModel.SeedType ->
viewModel.seedType.observe(this) { seedType: OnboardingViewModel.SeedType ->
seedTypeButton.text = seedType.toString()
seedTypeDescTextView.text = getText(seedType.descResId)
if (seedType == OnboardingViewModel.SeedType.LEGACY) {
@ -143,23 +144,15 @@ class OnboardingActivity : AppCompatActivity() {
}
}
mViewModel.showMonerochan.observe(this) {
if (it) {
xmrchanOnboardingImage.visibility = View.VISIBLE
} else {
xmrchanOnboardingImage.visibility = View.GONE
}
viewModel.useBundledTor.observe(this) { isChecked ->
walletProxyPortEditText.isEnabled = !isChecked && viewModel.useProxy.value == true
walletProxyAddressEditText.isEnabled = !isChecked && viewModel.useProxy.value == true
}
mViewModel.useBundledTor.observe(this) { isChecked ->
walletProxyPortEditText.isEnabled = !isChecked && mViewModel.useProxy.value == true
walletProxyAddressEditText.isEnabled = !isChecked && mViewModel.useProxy.value == true
}
mViewModel.useProxy.observe(this) { useProxy ->
viewModel.useProxy.observe(this) { useProxy ->
useBundledTor.isEnabled = useProxy
walletProxyAddressEditText.isEnabled = useProxy && mViewModel.useBundledTor.value == false
walletProxyPortEditText.isEnabled = useProxy && mViewModel.useBundledTor.value == false
walletProxyAddressEditText.isEnabled = useProxy && viewModel.useBundledTor.value == false
walletProxyPortEditText.isEnabled = useProxy && viewModel.useBundledTor.value == false
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
@ -169,7 +162,7 @@ class OnboardingActivity : AppCompatActivity() {
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) {
if (viewModel.useProxy.value == true && viewModel.useBundledTor.value == true) {
torIcon?.visibility = View.VISIBLE
indicatorCircle?.visibility = View.INVISIBLE
val proxyString = socketAddress.toString().substring(1)
@ -210,12 +203,12 @@ class OnboardingActivity : AppCompatActivity() {
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
moreOptionsDropdownTextView.setOnClickListener { mViewModel.onMoreOptionsClicked() }
moreOptionsDropdownTextView.setOnClickListener { viewModel.onMoreOptionsClicked() }
moreOptionsChevronImageView.setOnClickListener { mViewModel.onMoreOptionsClicked() }
moreOptionsChevronImageView.setOnClickListener { viewModel.onMoreOptionsClicked() }
seedOffsetCheckbox.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
mViewModel.useOffset = b
viewModel.useOffset = b
}
createWalletButton.setOnClickListener {
@ -230,7 +223,7 @@ class OnboardingActivity : AppCompatActivity() {
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())
viewModel.setPassphrase(editable.toString())
}
})
@ -238,7 +231,7 @@ class OnboardingActivity : AppCompatActivity() {
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())
viewModel.setConfirmedPassphrase(editable.toString())
}
})
@ -258,7 +251,7 @@ class OnboardingActivity : AppCompatActivity() {
seedTypeButton.setOnClickListener { toggleSeedType() }
torSwitch.setOnCheckedChangeListener { _, b: Boolean ->
mViewModel.setUseProxy(b)
viewModel.setUseProxy(b)
}
walletProxyPortEditText.addTextChangedListener(object : TextWatcher {
@ -266,7 +259,7 @@ class OnboardingActivity : AppCompatActivity() {
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
mViewModel.setProxyPort(text)
viewModel.setProxyPort(text)
}
})
@ -275,39 +268,55 @@ class OnboardingActivity : AppCompatActivity() {
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
mViewModel.setProxyAddress(text)
viewModel.setProxyAddress(text)
}
})
showXmrchanSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
mViewModel.setMonerochan(b)
}
selectNodeButton.setOnClickListener {
val listener = NodeSelectionDialogListenerAdapter(
activity = this,
setSelectNodeButtonText = { selectNodeButton.text = it },
getProxyAndPortValues = { Pair(walletProxyAddressEditText.text.toString(), walletProxyPortEditText.text.toString()) }
)
val nodes = PreferenceUtils.getOrSetDefaultNodes(this, DefaultNode.defaultNodes())
val node = PreferenceUtils.getOrSetDefaultNode(this, DefaultNode.defaultNode())
val dialog = NodeSelectionBottomSheetDialog(listener)
val listener = object : NodeSelectionDialogListenerAdapter(
activity = this,
nodes = nodes,
) {
override fun trySelectNode(
self: NodeSelectionBottomSheetDialog,
node: Node
): Boolean {
return PreferenceUtils.setNode(this@OnboardingActivity, node).fold(
{
selectNodeButton.text = getString(R.string.node_button_text, node.name)
Timber.i("Node selected")
true
},
{
Timber.e(it, "Failed to select node")
Toast.makeText(this@OnboardingActivity, "Failed to select node", Toast.LENGTH_SHORT).show()
false
}
)
}
}
val dialog = NodeSelectionBottomSheetDialog(nodes, node, listener)
dialog.show(supportFragmentManager, "node_selection_dialog")
}
useBundledTor.setOnCheckedChangeListener { _, isChecked ->
mViewModel.setUseBundledTor(isChecked)
viewModel.setUseBundledTor(isChecked)
}
}
private fun toggleSeedType() {
val seedType = mViewModel.seedType.value ?: return
val seedType = viewModel.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)
viewModel.setSeedType(newSeedType)
}
private fun createOrImportWallet(
@ -316,11 +325,11 @@ class OnboardingActivity : AppCompatActivity() {
) {
this.let { act ->
lifecycleScope.launch(Dispatchers.IO) {
mViewModel.createOrImportWallet(
viewModel.createOrImportWallet(
act,
walletSeed,
restoreHeightText,
mViewModel.useOffset,
viewModel.useOffset,
applicationContext
)
}
@ -342,8 +351,6 @@ internal class OnboardingViewModel : ViewModel() {
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
@ -409,10 +416,9 @@ internal class OnboardingViewModel : ViewModel() {
}
return
}
PrefService.instance.edit().putBoolean(Constants.PREF_USES_PASSWORD, true).apply()
}
var restoreHeight = newRestoreHeight
val walletFile = File(mainActivity.applicationInfo.dataDir, Constants.WALLET_NAME)
val walletFile = File(mainActivity.applicationInfo.dataDir, Constants.DEFAULT_WALLET_NAME)
var wallet: Wallet? = null
if (offset.isNotEmpty()) {
PrefService.instance.edit().putBoolean(Constants.PREF_USES_OFFSET, true).apply()
@ -431,7 +437,7 @@ internal class OnboardingViewModel : ViewModel() {
}
return
} else {
WalletManager.instance?.createWalletPolyseed(
WalletManager.instance.createWalletPolyseed(
walletFile,
passphrase,
offset,
@ -440,11 +446,11 @@ internal class OnboardingViewModel : ViewModel() {
}
} else if (seedTypeValue == SeedType.LEGACY) {
val tmpWalletFile =
File(mainActivity.applicationInfo.dataDir, Constants.WALLET_NAME + "_tmp")
File(mainActivity.applicationInfo.dataDir, Constants.DEFAULT_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(
wallet = WalletManager.instance.recoveryWallet(
walletFile,
passphrase,
tmpWallet.getSeed("") ?: return@let,
@ -491,8 +497,9 @@ internal class OnboardingViewModel : ViewModel() {
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) {
MoneroHandlerThread.init(walletFile, passphrase, context)
val intent = Intent(mainActivity, HomeActivity::class.java)
intent.putExtra(Constants.EXTRA_WALLET_NAME, Constants.DEFAULT_WALLET_NAME)
intent.putExtra(Constants.EXTRA_WALLET_PASSWORD, passphrase)
mainActivity.startActivity(intent)
mainActivity.finish()
} else {
@ -542,14 +549,14 @@ internal class OnboardingViewModel : ViewModel() {
_proxyAddress.value = address
if (address.isEmpty()) PrefService.instance.deleteProxy()
val port = _proxyPort.value ?: return
ProxyService.instance?.updateProxy(address, port)
// 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)
// ProxyService.instance?.updateProxy(address, port)
}
fun setUseBundledTor(useBundled: Boolean) {
@ -584,11 +591,6 @@ internal class OnboardingViewModel : ViewModel() {
_confirmedPassphrase.value = confirmedPassphrase
}
fun setMonerochan(b: Boolean) {
_showMonerochan.value = b
PrefService.instance?.edit()?.putBoolean(Constants.PREF_MONEROCHAN, b)?.apply()
}
enum class SeedType(val descResId: Int) {
LEGACY(R.string.seed_desc_legacy), POLYSEED(R.string.seed_desc_polyseed), UNKNOWN(0)
}

View File

@ -15,8 +15,10 @@ import java.io.File
// 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 var preventGoingBack: Boolean = false
private lateinit var walletName: String
private lateinit var passwordEditText: EditText
private lateinit var unlockButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
@ -24,8 +26,9 @@ class PasswordActivity : AppCompatActivity() {
setContentView(R.layout.activity_password)
preventGoingBack = intent.getBooleanExtra(Constants.EXTRA_PREVENT_GOING_BACK, false)
walletName = intent.getStringExtra(Constants.EXTRA_WALLET_NAME)!!
passwordEdittext = findViewById(R.id.wallet_password_edittext)
passwordEditText = findViewById(R.id.wallet_password_edittext)
unlockButton = findViewById(R.id.unlock_wallet_button)
onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
@ -36,18 +39,16 @@ class PasswordActivity : AppCompatActivity() {
}
})
val walletFile = File(applicationInfo.dataDir, Constants.WALLET_NAME)
unlockButton.setOnClickListener {
onUnlockClicked(walletFile, passwordEdittext.text.toString())
onUnlockClicked(passwordEditText.text.toString())
}
}
private fun onUnlockClicked(walletFile: File, password: String) {
if (checkPassword(walletFile, password)) {
private fun onUnlockClicked(walletPassword: String) {
if (checkPassword(walletPassword)) {
val intent = Intent()
intent.putExtra(Constants.EXTRA_WALLET_NAME, walletFile.name)
intent.putExtra(Constants.EXTRA_WALLET_PASSWORD, password)
intent.putExtra(Constants.EXTRA_WALLET_NAME, walletName)
intent.putExtra(Constants.EXTRA_WALLET_PASSWORD, walletPassword)
setResult(RESULT_OK, intent)
finish()
} else {
@ -55,10 +56,12 @@ class PasswordActivity : AppCompatActivity() {
}
}
private fun checkPassword(walletFile: File, password: String): Boolean {
return WalletManager.instance?.verifyWalletPasswordOnly(
private fun checkPassword(walletPassword: String): Boolean {
val walletFile = File(applicationInfo.dataDir, walletName)
return WalletManager.instance.verifyWalletPasswordOnly(
walletFile.absolutePath + ".keys",
password
) == true
walletPassword
)
}
}

View File

@ -1,17 +1,22 @@
package net.mynero.wallet
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.graphics.Bitmap
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
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
import com.google.zxing.BarcodeFormat
@ -23,88 +28,118 @@ 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.service.wallet.WalletServiceObserver
import net.mynero.wallet.service.wallet.WalletService
import net.mynero.wallet.util.Helper.clipBoardCopy
import net.mynero.wallet.util.PreferenceUtils
import java.nio.charset.StandardCharsets
import java.util.Collections
import java.util.EnumMap
class ReceiveActivity : AppCompatActivity() {
class ReceiveActivity : AppCompatActivity(), WalletServiceObserver {
private val viewModel: ReceiveViewModel by viewModels()
private var walletService: WalletService? = null
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
private lateinit var freshAddressImageView : ImageButton
private lateinit var recyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setVisible(false)
setContentView(R.layout.activity_receive)
mViewModel = ViewModelProvider(this)[ReceiveViewModel::class.java]
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)
freshAddressImageView = findViewById(R.id.fresh_address_imageview)
recyclerView = findViewById(R.id.address_list_recyclerview)
bindListeners()
bindObservers()
mViewModel.init()
bindService(Intent(applicationContext, WalletService::class.java), connection, 0)
}
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val walletService = (service as WalletService.WalletServiceBinder).service
this@ReceiveActivity.walletService = walletService
walletService.addObserver(this@ReceiveActivity)
viewModel.refreshSubaddresses(walletService)
}
override fun onServiceDisconnected(className: ComponentName) {
walletService = null
}
}
private fun bindListeners() {
val freshAddressImageView = findViewById<ImageButton>(R.id.fresh_address_imageview)
freshAddressImageView.setOnClickListener { mViewModel.freshSubaddress }
freshAddressImageView.setOnClickListener {
walletService?.generateNewSubaddress("")
}
}
private fun bindObservers() {
val subaddressAdapterListener = object : SubaddressAdapterListener {
override fun onSubaddressSelected(subaddress: Subaddress?) {
mViewModel.selectAddress(subaddress)
override fun onSubaddressSelected(subaddress: Subaddress) {
viewModel.selectAddress(subaddress)
}
override fun onSubaddressEditLabel(subaddress: Subaddress?) {
override fun onSubaddressEditLabel(subaddress: Subaddress) {
editAddressLabel(subaddress)
}
}
val adapter = SubaddressAdapter(emptyList(), null, subaddressAdapterListener)
val recyclerView = findViewById<RecyclerView>(R.id.address_list_recyclerview)
val streetMode = PreferenceUtils.isStreetMode(this)
val adapter = SubaddressAdapter(emptyList(), null, streetMode, subaddressAdapterListener)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
mViewModel.address.observe(this) { address: Subaddress? ->
setAddress(address)
adapter.submitSelectedAddress(address)
viewModel.selectedSubaddress.observe(this) { address: Subaddress? ->
if (address != null) {
setAddress(address)
adapter.submitSelectedAddress(address)
}
}
mViewModel.addresses.observe(this) { addresses: List<Subaddress> ->
viewModel.subaddresses.observe(this) { addresses: List<Subaddress> ->
// We want newer addresses addresses to be shown first
adapter.submitAddresses(addresses.reversed())
}
}
private fun editAddressLabel(subaddress: Subaddress?) {
val dialog = EditAddressLabelBottomSheetDialog()
dialog.addressIndex = subaddress?.addressIndex ?: return
dialog.listener = object : LabelListener {
override fun onDismiss() {
mViewModel.init()
}
override fun onSubaddressesUpdated() {
walletService?.let { walletService ->
viewModel.refreshSubaddresses(walletService)
}
}
private fun editAddressLabel(subaddress: Subaddress) {
val dialog = EditAddressLabelBottomSheetDialog(subaddress.label) {
walletService?.setSubaddressLabel(subaddress.addressIndex, it)
}
dialog.show(supportFragmentManager, "edit_address_dialog")
}
private fun setAddress(subaddress: Subaddress?) {
val label = subaddress?.displayLabel
private fun setAddress(subaddress: Subaddress) {
val label = subaddress.displayLabel
val address = getString(
R.string.subbaddress_info_subtitle,
subaddress?.addressIndex, subaddress?.squashedAddress
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) })
addressLabelTextView.text = label.ifEmpty { address }
addressLabelTextView.visibility = View.VISIBLE
addressTextView.text = subaddress.address
addressTextView.visibility = View.VISIBLE
// TODO: this takes a few hundred milliseconds, do something about it
addressImageView.setImageBitmap(generate(subaddress.address, 256, 256))
addressImageView.visibility = View.VISIBLE
copyAddressImageButton.setOnClickListener {
clipBoardCopy(
this, "address", subaddress?.address
this, "address", subaddress.address
)
}
addressLabelTextView.setOnLongClickListener {
@ -146,34 +181,27 @@ class ReceiveActivity : AppCompatActivity() {
}
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
private val _selectedSubaddress = MutableLiveData<Subaddress?>()
val selectedSubaddress: LiveData<Subaddress?> = _selectedSubaddress
fun init() {
_addresses.value = subaddresses
_address.value = addresses.value?.lastOrNull()
private val _subaddresses = MutableLiveData<List<Subaddress>>()
val subaddresses: LiveData<List<Subaddress>> = _subaddresses
fun refreshSubaddresses(walletService: WalletService) {
val wallet = walletService.getWalletOrThrow()
val newSubaddresses = (0 until wallet.numSubaddresses).map { wallet.getSubaddressObject(it) }
_subaddresses.postValue(newSubaddresses)
val currentSelectedSubaddress = selectedSubaddress.value
if (currentSelectedSubaddress == null) {
_selectedSubaddress.postValue(newSubaddresses.lastOrNull())
} else {
_selectedSubaddress.postValue(newSubaddresses.getOrNull(currentSelectedSubaddress.addressIndex))
}
}
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
_selectedSubaddress.value = subaddress
}
}

View File

@ -1,9 +1,13 @@
package net.mynero.wallet
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.EditText
@ -13,13 +17,13 @@ import android.widget.RadioGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
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
@ -28,18 +32,22 @@ 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.PrefService
import net.mynero.wallet.service.TxService
import net.mynero.wallet.service.UTXOService
import net.mynero.wallet.service.wallet.WalletServiceObserver
import net.mynero.wallet.service.wallet.WalletService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Helper
import net.mynero.wallet.util.PreferenceUtils
import net.mynero.wallet.util.TransactionDestination
import net.mynero.wallet.util.UriData
class SendActivity : AppCompatActivity() {
class SendActivity : AppCompatActivity(), WalletServiceObserver {
var priority: PendingTransaction.Priority = PendingTransaction.Priority.Priority_Default
private lateinit var mViewModel: SendViewModel
private val viewModel: SendViewModel by viewModels()
private var walletService: WalletService? = null
private lateinit var sendMaxButton: Button
private lateinit var addOutputImageView: ImageButton
private lateinit var destList: LinearLayout
@ -77,7 +85,6 @@ class SendActivity : AppCompatActivity() {
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)
@ -95,34 +102,7 @@ class SendActivity : AppCompatActivity() {
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)
connectWalletService()
}
private fun bindListeners() {
@ -135,7 +115,7 @@ class SendActivity : AppCompatActivity() {
R.id.high_fee_radiobutton -> priority = PendingTransaction.Priority.Priority_High
}
}
if (PrefService.instance.getBoolean(Constants.PREF_ALLOW_FEE_OVERRIDE, false)) {
if (PreferenceUtils.isAllowFeeOverride(this)) {
feeRadioGroup.visibility = View.VISIBLE
feeRadioGroupLabelTextView.visibility = View.VISIBLE
} else {
@ -155,14 +135,14 @@ class SendActivity : AppCompatActivity() {
).show()
}
}
sendMaxButton.setOnClickListener { mViewModel.setSendingMax(!isSendAll) }
sendMaxButton.setOnClickListener { viewModel.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())
createTx(rawDests, isSendAll, priority, viewModel.enotes.value ?: ArrayList())
} else {
Toast.makeText(
this,
@ -189,8 +169,93 @@ class SendActivity : AppCompatActivity() {
}
}
private fun bindObservers() {
viewModel.sendingMax.observe(this) { sendingMax: Boolean? ->
if (viewModel.pendingTransaction.value == null) {
if (sendingMax == true) {
prepareOutputsForMaxSend()
sendMaxButton.text = getText(R.string.undo)
} else {
unprepareMaxSend()
sendMaxButton.text = getText(R.string.send_max)
}
}
}
viewModel.showAddOutputButton.observe(this) { show: Boolean? ->
setAddOutputButtonVisibility(
if (show == true && !destsHasPaymentId()) View.VISIBLE else View.INVISIBLE
)
}
viewModel.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 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 enotes = intent.getStringArrayListExtra(Constants.EXTRA_SEND_ENOTES) ?: ArrayList()
addOutput(true, address, amount)
viewModel.setSendingMax(max)
viewModel.setEnotes(enotes)
}
private fun connectWalletService() {
val intent = Intent(applicationContext, WalletService::class.java)
startService(intent)
bindService(intent, connection, BIND_AUTO_CREATE)
}
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val walletService = (service as WalletService.WalletServiceBinder).service
this@SendActivity.walletService = walletService
walletService.addObserver(this@SendActivity)
updateEnotesLabel()
}
override fun onServiceDisconnected(className: ComponentName) {
walletService = null
}
}
private fun updateEnotesLabel() {
walletService?.let { walletService ->
val selectedUtxos = viewModel.enotes.value
if (selectedUtxos?.isNotEmpty() == true) {
var selectedValue: Long = 0
val enotes = walletService.getWalletOrThrow().coins!!.all
for (coinsInfo in enotes) {
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 confirmSlider() {
val pendingTx = mViewModel.pendingTransaction.value ?: return
val pendingTx = viewModel.pendingTransaction.value ?: return
Toast.makeText(this, getString(R.string.sending_tx), Toast.LENGTH_SHORT)
.show()
sendTx(pendingTx)
@ -211,7 +276,7 @@ class SendActivity : AppCompatActivity() {
return false
}
val amountRaw = Wallet.getAmountFromString(amount)
val balance = BalanceService.instance?.unlockedBalanceRaw ?: 0
val balance = walletService!!.getWalletOrThrow().unlockedBalance
if (amountRaw >= balance || amountRaw <= 0) {
Toast.makeText(
this,
@ -260,39 +325,6 @@ class SendActivity : AppCompatActivity() {
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 =
@ -333,11 +365,11 @@ class SendActivity : AppCompatActivity() {
addressField.text = null
} else if (currentOutputs == 1 && hasPaymentId) {
// show add output button: we are sending to integrated address
mViewModel.setShowAddOutputButton(false)
viewModel.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)
viewModel.setShowAddOutputButton(true)
}
if (s.toString() == Constants.DONATE_ADDRESS) {
@ -397,7 +429,7 @@ class SendActivity : AppCompatActivity() {
return dests
}
private val isSendAll: Boolean
get() = mViewModel.sendingMax.value ?: false
get() = viewModel.sendingMax.value ?: false
private fun getDestView(pos: Int): ConstraintLayout {
return destList.getChildAt(pos) as ConstraintLayout
@ -438,7 +470,7 @@ class SendActivity : AppCompatActivity() {
setAddOutputButtonVisibility(View.VISIBLE)
sendMaxButton.visibility = View.VISIBLE
createButton.visibility = View.VISIBLE
if (PrefService.instance.getBoolean(Constants.PREF_ALLOW_FEE_OVERRIDE, false)) {
if (PreferenceUtils.isAllowFeeOverride(this)) {
feeRadioGroup.visibility = View.VISIBLE
feeRadioGroupLabelTextView.visibility = View.VISIBLE
} else {
@ -493,7 +525,7 @@ class SendActivity : AppCompatActivity() {
).show()
return
} else if (currentOutputs == 1 && uriData.hasPaymentId()) {
mViewModel.setShowAddOutputButton(false)
viewModel.setShowAddOutputButton(false)
}
val addressField = entryView.findViewById<EditText>(R.id.address_edittext)
addressField.setText(uriData.address)
@ -518,60 +550,44 @@ class SendActivity : AppCompatActivity() {
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()
}
}
if (sendAll) {
walletService?.createSweepTransaction(dests[0].first, feePriority, utxos)
} else {
val destinations = dests.map { TransactionDestination(it.first, Wallet.getAmountFromString(it.second)) }
walletService?.createTransaction(destinations, feePriority, utxos)
}
}
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()
}
walletService?.sendTransaction(pendingTx)
}
override fun onTransactionCreated(pendingTransaction: PendingTransaction) {
if (pendingTransaction.status === PendingTransaction.Status.Status_Ok) {
viewModel.setPendingTransaction(pendingTransaction)
} else {
runOnUiThread {
createButton.isEnabled = true
sendMaxButton.isEnabled = true
if (pendingTransaction.getErrorString() != null) Toast.makeText(
this,
getString(R.string.error_creating_tx, pendingTransaction.getErrorString()),
Toast.LENGTH_SHORT
).show()
}
}
}
override fun onTransactionSent(pendingTransaction: PendingTransaction, success: Boolean) {
walletService?.refreshTransactionsHistory()
runOnUiThread {
if (success) {
Toast.makeText(this, getString(R.string.sent_tx), Toast.LENGTH_SHORT).show()
finish()
} else {
sendTxSlider.resetSlider()
Toast.makeText(this, getString(R.string.error_sending_tx), Toast.LENGTH_SHORT).show()
Log.e("SA", "pts = ${pendingTransaction.status}")
}
}
}
@ -584,11 +600,11 @@ class SendActivity : AppCompatActivity() {
class SendViewModel : ViewModel() {
private val _sendingMax = MutableLiveData(false)
private val _showAddOutputButton = MutableLiveData(true)
private val _utxos = MutableLiveData<ArrayList<String>>(ArrayList())
private val _enotes = 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 enotes: LiveData<ArrayList<String>> = _enotes
var pendingTransaction: LiveData<PendingTransaction?> = _pendingTransaction
fun setSendingMax(value: Boolean) {
_sendingMax.value = value
@ -599,8 +615,8 @@ class SendViewModel : ViewModel() {
_showAddOutputButton.value = value
}
fun setUtxos(value: ArrayList<String>) {
_utxos.value = value
fun setEnotes(value: ArrayList<String>) {
_enotes.value = value
}
fun setPendingTransaction(pendingTx: PendingTransaction?) {

View File

@ -1,47 +1,56 @@
package net.mynero.wallet
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
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 androidx.activity.OnBackPressedCallback
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
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.DefaultNode
import net.mynero.wallet.data.Node
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog
import net.mynero.wallet.fragment.dialog.WalletKeysBottomSheetDialog
import net.mynero.wallet.listener.NodeSelectionDialogListenerAdapter
import net.mynero.wallet.model.EnumTorState
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.service.wallet.WalletServiceObserver
import net.mynero.wallet.service.wallet.WalletService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Utils
import net.mynero.wallet.util.PreferenceUtils
import timber.log.Timber
class SettingsActivity : AppCompatActivity() {
class SettingsActivity : AppCompatActivity(), WalletServiceObserver {
private val viewModel: SettingsViewModel by viewModels()
private var walletService: WalletService? = null
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 allowFeeOverrideSwitch: SwitchCompat
private lateinit var useBundledTor: CheckBox
private lateinit var displaySeedButton: Button
private lateinit var displayUtxosButton: Button
private lateinit var torSwitch: SwitchCompat
private lateinit var proxySwitch: SwitchCompat
private lateinit var saveProxyButton: Button
private val askForWalletPasswordAndDisplayWalletKeys = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
@ -54,112 +63,141 @@ class SettingsActivity : AppCompatActivity() {
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)
allowFeeOverrideSwitch = findViewById(R.id.allow_fee_override_switch)
torSwitch = findViewById(R.id.tor_switch)
proxySwitch = findViewById(R.id.proxy_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)
saveProxyButton = findViewById(R.id.save_proxy_button)
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
// walletProxyPortEditText.isEnabled = !cachedUsingBundledTor
// walletProxyAddressEditText.isEnabled = !cachedUsingBundledTor
proxySettingsLayout.visibility = View.VISIBLE
streetModeSwitch.isChecked = PrefService.instance.getBoolean(Constants.PREF_STREET_MODE, false)
monerochanSwitch.isChecked = PrefService.instance.getBoolean(Constants.PREF_MONEROCHAN, Constants.DEFAULT_PREF_MONEROCHAN)
allowFeeOverrideSwitch.isChecked = PrefService.instance.getBoolean(Constants.PREF_ALLOW_FEE_OVERRIDE, false)
streetModeSwitch.isChecked = PreferenceUtils.isStreetMode(applicationContext)
allowFeeOverrideSwitch.isChecked = PreferenceUtils.isAllowFeeOverride(applicationContext)
useBundledTor.isChecked = cachedUsingBundledTor
torSwitch.isChecked = cachedUsingProxy
proxySwitch.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?.name)
val node = PreferenceUtils.getOrSetDefaultNode(this, DefaultNode.defaultNode())
selectNodeButton.text = getString(R.string.node_button_text, node.name)
bindListeners()
bindObservers()
bindService(Intent(applicationContext, WalletService::class.java), connection, 0)
}
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val walletService = (service as WalletService.WalletServiceBinder).service
this@SettingsActivity.walletService = walletService
walletService.addObserver(this@SettingsActivity)
}
override fun onServiceDisconnected(className: ComponentName) {
walletService = null
}
}
private fun bindListeners() {
val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Utils.refreshProxy(walletProxyAddressEditText.text.toString(), walletProxyPortEditText.text.toString())
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()
PreferenceUtils.setStreetMode(applicationContext, b)
}
allowFeeOverrideSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PrefService.instance.edit().putBoolean(Constants.PREF_ALLOW_FEE_OVERRIDE, b).apply()
HistoryService.instance?.refreshHistory()
PreferenceUtils.setAllowFeeOverride(applicationContext, b)
}
selectNodeButton.setOnClickListener {
val listener = NodeSelectionDialogListenerAdapter(
activity = this,
setSelectNodeButtonText = { selectNodeButton.text = it },
getProxyAndPortValues = { Pair(walletProxyAddressEditText.text.toString(), walletProxyPortEditText.text.toString()) }
)
val nodes = PreferenceUtils.getOrSetDefaultNodes(this, DefaultNode.defaultNodes())
val node = PreferenceUtils.getOrSetDefaultNode(this, DefaultNode.defaultNode())
val dialog = NodeSelectionBottomSheetDialog(listener)
val listener = object : NodeSelectionDialogListenerAdapter(
activity = this,
nodes = nodes,
) {
override fun trySelectNode(
self: NodeSelectionBottomSheetDialog,
node: Node
): Boolean {
return PreferenceUtils.setNode(this@SettingsActivity, node).fold(
{
selectNodeButton.text = getString(R.string.node_button_text, node.name)
Timber.i("Node selected")
val proxy = PreferenceUtils.getProxyIfEnabled(this@SettingsActivity) ?: ""
walletService?.setDaemonAddress(node.address, node.username ?: "", node.password ?: "", node.trusted, proxy)
true
},
{
Timber.e(it, "Failed to select node")
Toast.makeText(this@SettingsActivity, "Failed to select node", Toast.LENGTH_SHORT).show()
false
}
)
}
}
val dialog = NodeSelectionBottomSheetDialog(nodes, node, listener)
dialog.show(supportFragmentManager, "node_selection_dialog")
}
useBundledTor.setOnCheckedChangeListener { _, isChecked ->
mViewModel.setUseBundledTor(isChecked)
viewModel.setUseBundledTor(isChecked)
}
displaySeedButton.setOnClickListener {
val usesPassword = PrefService.instance.getBoolean(Constants.PREF_USES_PASSWORD, false)
if (usesPassword) {
val intent = Intent(this, PasswordActivity::class.java)
askForWalletPasswordAndDisplayWalletKeys.launch(intent)
} else {
displaySeedDialog("")
}
val intent = Intent(this, PasswordActivity::class.java)
// TODO: use real wallet name here
intent.putExtra(Constants.EXTRA_WALLET_NAME, Constants.DEFAULT_WALLET_NAME)
askForWalletPasswordAndDisplayWalletKeys.launch(intent)
}
displayUtxosButton.setOnClickListener {
startActivity(Intent(this, UtxosActivity::class.java))
startActivity(Intent(this, EnotesActivity::class.java))
}
torSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
mViewModel.setUseProxy(b)
proxySwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
viewModel.setUseProxy(b)
}
saveProxyButton.setOnClickListener {
val proxyAddress = walletProxyAddressEditText.text.toString()
val proxyPort = walletProxyPortEditText.text.toString()
val proxy = if (proxyAddress.isNotBlank() && proxyPort.isNotBlank()) "$proxyAddress:$proxyPort" else ""
PreferenceUtils.setProxy(applicationContext, proxyPort)
if (proxySwitch.isChecked) {
Toast.makeText(this, "Activating proxy", Toast.LENGTH_SHORT).show()
walletService?.setProxy(proxy)
} else {
if (walletService?.getProxy() != "") {
Toast.makeText(this, "Deactivating proxy", Toast.LENGTH_SHORT).show()
walletService?.setProxy("")
}
}
}
}
private fun bindObservers() {
mViewModel.useProxy.observe(this) { useProxy ->
viewModel.useProxy.observe(this) { useProxy ->
useBundledTor.isEnabled = useProxy
walletProxyPortEditText.isEnabled = useProxy && mViewModel.useBundledTor.value == false
walletProxyAddressEditText.isEnabled = useProxy && mViewModel.useBundledTor.value == false
Utils.refreshProxy(walletProxyAddressEditText.text.toString(), walletProxyPortEditText.text.toString())
// Utils.refreshProxy(walletProxyAddressEditText.text.toString(), walletProxyPortEditText.text.toString())
}
mViewModel.useBundledTor.observe(this) { isChecked ->
walletProxyPortEditText.isEnabled = !isChecked && mViewModel.useProxy.value == true
walletProxyAddressEditText.isEnabled = !isChecked && mViewModel.useProxy.value == true
viewModel.useBundledTor.observe(this) { isChecked ->
// TODO: bundled tor support
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
@ -170,7 +208,7 @@ class SettingsActivity : AppCompatActivity() {
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) {
if (viewModel.useProxy.value == true && viewModel.useBundledTor.value == true) {
torIcon?.visibility = View.VISIBLE
indicatorCircle?.visibility = View.INVISIBLE
val proxyString = socketAddress.toString().substring(1)
@ -202,14 +240,37 @@ class SettingsActivity : AppCompatActivity() {
private fun updateProxy(address: String, port: String) {
walletProxyPortEditText.setText(port)
walletProxyAddressEditText.setText(address)
Utils.refreshProxy(address, port)
// Utils.refreshProxy(address, port)
}
private fun displaySeedDialog(password: String) {
val informationDialog = WalletKeysBottomSheetDialog()
informationDialog.password = password
val wallet = walletService!!.getWalletOrThrow()
val usesOffset = PrefService.instance.getBoolean(Constants.PREF_USES_OFFSET, false)
val seed = wallet.getSeed(if (usesOffset) password else "")
val privateViewKey = wallet.getSecretViewKey()
val restoreHeight = wallet.getRestoreHeight()
val informationDialog = WalletKeysBottomSheetDialog(usesOffset, seed, privateViewKey, restoreHeight)
informationDialog.show(supportFragmentManager, "information_seed_dialog")
}
override fun onProxyUpdated(proxy: String, success: Boolean) {
runOnUiThread {
if (proxy.isBlank()) {
if (success) {
Toast.makeText(this, "Proxy deactivated", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Failed to deactivate proxy", Toast.LENGTH_SHORT).show()
}
} else {
if (success) {
Toast.makeText(this, "Proxy activated", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Failed to activate proxy", Toast.LENGTH_SHORT).show()
}
}
}
}
}
class SettingsViewModel : ViewModel() {

View File

@ -4,10 +4,9 @@ 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 timber.log.Timber
import java.io.File
class StartActivity : AppCompatActivity() {
@ -19,10 +18,10 @@ class StartActivity : AppCompatActivity() {
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)
openWallet(walletName, walletPassword)
} else {
// if we ever get here, it's a bug ¯\_(ツ)_/¯ so let's just recreate the activity
Timber.e("Password activity returned null wallet name or password")
recreate()
}
}
@ -31,38 +30,38 @@ class StartActivity : AppCompatActivity() {
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")
// TODO: multiple wallets support
val walletName = Constants.DEFAULT_WALLET_NAME
val walletKeysFile = File(applicationInfo.dataDir, "$walletName.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
}
Timber.d("Wallet keys file exists, launching password activity")
val intent = Intent(this, PasswordActivity::class.java)
intent.putExtra(Constants.EXTRA_PREVENT_GOING_BACK, true)
intent.putExtra(Constants.EXTRA_WALLET_NAME, walletName)
startPasswordActivityForOpeningWallet.launch(intent)
return
} else {
Timber.d("Wallet keys file does not exist, launching onboarding activity")
startActivity(Intent(this, OnboardingActivity::class.java))
finish()
return
}
}
private fun openWallet(walletFile: File, password: String) {
MoneroHandlerThread.init(walletFile, password, applicationContext)
private fun openWallet(walletName: String, walletPassword: String) {
val homeActivityIntent = Intent(this, HomeActivity::class.java)
homeActivityIntent.putExtra(Constants.EXTRA_WALLET_NAME, walletName)
homeActivityIntent.putExtra(Constants.EXTRA_WALLET_PASSWORD, walletPassword)
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))
Timber.d("Uri payment data not present, launching home activity")
startActivity(homeActivityIntent)
} 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)
Timber.d("Uri payment data present, launching home and send activities")
homeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
startActivity(homeActivityIntent)
val sendIntent = Intent(this, SendActivity::class.java)
sendIntent.putExtra(Constants.EXTRA_SEND_ADDRESS, uriData!!.address)

View File

@ -1,27 +1,35 @@
package net.mynero.wallet
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.view.LayoutInflater
import android.os.IBinder
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.HistoryService
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.wallet.WalletServiceObserver
import net.mynero.wallet.service.wallet.WalletService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.DateHelper
import net.mynero.wallet.util.Helper
import net.mynero.wallet.util.PreferenceUtils
import java.util.Calendar
import java.util.Date
import java.util.Objects
class TransactionActivity : AppCompatActivity() {
private var transactionInfo: TransactionInfo? = null
class TransactionActivity : AppCompatActivity(), WalletServiceObserver {
private val viewModel: TransactionActivityViewModel by viewModels()
private var walletService: WalletService? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -31,16 +39,35 @@ class TransactionActivity : AppCompatActivity() {
val tz = cal.timeZone //get the local time zone.
DateHelper.DATETIME_FORMATTER.timeZone = tz
transactionInfo = intent.extras?.getParcelable(Constants.NAV_ARG_TXINFO)
if (viewModel.txHash == null) {
viewModel.txHash = intent.extras?.getString(Constants.NAV_ARG_TXINFO)
}
if (viewModel.txHash == null) {
finish()
}
bindObservers()
bindListeners()
bindService(Intent(applicationContext, WalletService::class.java), connection, 0)
}
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val walletService = (service as WalletService.WalletServiceBinder).service
this@TransactionActivity.walletService = walletService
walletService.addObserver(this@TransactionActivity)
walletService.refreshTransactionsHistory()
}
override fun onServiceDisconnected(className: ComponentName) {}
}
private fun bindListeners() {
val copyTxHashImageButton = findViewById<ImageButton>(R.id.copy_txhash_imagebutton)
copyTxHashImageButton.setOnClickListener {
val txInfo = transactionInfo
val txInfo = viewModel.transactionInfo.value
if (txInfo != null) {
Helper.clipBoardCopy(this, "transaction_hash", txInfo.hash)
}
@ -49,7 +76,7 @@ class TransactionActivity : AppCompatActivity() {
findViewById<ImageButton>(R.id.copy_txaddress_imagebutton)
val addressTextView = findViewById<TextView>(R.id.transaction_address_textview)
copyTxAddressImageButton.setOnClickListener {
val txInfo = transactionInfo
val txInfo = viewModel.transactionInfo.value
if (txInfo != null) {
val destination = addressTextView.text.toString()
Helper.clipBoardCopy(this, "transaction_address", destination)
@ -68,34 +95,32 @@ class TransactionActivity : AppCompatActivity() {
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
)
?: return@observe
txHashTextView.text = newTransactionInfo.hash
txConfTextView.text = "${newTransactionInfo.confirmations}"
txDateTextView.text = getDateTime(newTransactionInfo.timestamp)
if (newTransactionInfo.confirmations > 1) {
viewModel.transactionInfo.observe(this) { transactionInfo: TransactionInfo? ->
if (transactionInfo == null) {
return@observe
}
txHashTextView.text = transactionInfo.hash
txConfTextView.text = "${transactionInfo.confirmations}"
txDateTextView.text = getDateTime(transactionInfo.timestamp)
if (transactionInfo.confirmations > 1) {
confLabel2.text = getString(R.string.transaction_conf_desc2_confirmed)
blockHeightTextView.text = "${newTransactionInfo.blockheight}"
blockHeightTextView.text = "${transactionInfo.blockheight}"
blockHeightTextView.visibility = View.VISIBLE
} else if (newTransactionInfo.confirmations == 1L) {
} else if (transactionInfo.confirmations == 1L) {
confLabel2.text = getString(R.string.transaction_conf_1_desc2_confirmed)
blockHeightTextView.text = "${newTransactionInfo.blockheight}"
blockHeightTextView.text = "${transactionInfo.blockheight}"
blockHeightTextView.visibility = View.VISIBLE
} else {
blockHeightTextView.visibility = View.GONE
confLabel2.text = getString(R.string.transaction_conf_desc2_unconfirmed)
}
val streetModeEnabled =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
val streetModeEnabled = PreferenceUtils.isStreetMode(applicationContext)
val balanceString =
if (streetModeEnabled) Constants.STREET_MODE_BALANCE else Helper.getDisplayAmount(
newTransactionInfo.amount,
transactionInfo.amount,
12
)
if (newTransactionInfo.direction === TransactionInfo.Direction.Direction_In) {
if (transactionInfo.direction === TransactionInfo.Direction.Direction_In) {
txActionTextView.text = getString(R.string.transaction_action_recv)
txAmountTextView.setTextColor(
ContextCompat.getColor(
@ -114,20 +139,20 @@ class TransactionActivity : AppCompatActivity() {
}
txAmountTextView.text = balanceString
var destination: String? = "-"
val wallet = WalletManager.instance?.wallet
if (newTransactionInfo.txKey == null) {
newTransactionInfo.txKey = wallet?.getTxKey(newTransactionInfo.hash)
val wallet = walletService!!.getWalletOrThrow()
if (transactionInfo.txKey == null) {
transactionInfo.txKey = wallet.getTxKey(transactionInfo.hash)
}
if (newTransactionInfo.address == null && newTransactionInfo.direction === TransactionInfo.Direction.Direction_In) {
destination = wallet?.getSubaddress(
newTransactionInfo.accountIndex,
newTransactionInfo.addressIndex
if (transactionInfo.address == null && transactionInfo.direction === TransactionInfo.Direction.Direction_In) {
destination = wallet.getSubaddress(
transactionInfo.accountIndex,
transactionInfo.addressIndex
)
} else if (newTransactionInfo.address != null && newTransactionInfo.direction === TransactionInfo.Direction.Direction_In) {
destination = newTransactionInfo.address
} else if (newTransactionInfo.transfers != null && newTransactionInfo.direction === TransactionInfo.Direction.Direction_Out) {
if (newTransactionInfo.transfers?.size == 1) {
destination = newTransactionInfo.transfers?.get(0)?.address
} else if (transactionInfo.address != null && transactionInfo.direction === TransactionInfo.Direction.Direction_In) {
destination = transactionInfo.address
} else if (transactionInfo.transfers != null && transactionInfo.direction === TransactionInfo.Direction.Direction_Out) {
if (transactionInfo.transfers?.size == 1) {
destination = transactionInfo.transfers?.get(0)?.address
}
}
txAddressTextView.text = Objects.requireNonNullElse(destination, "-")
@ -137,20 +162,23 @@ class TransactionActivity : AppCompatActivity() {
}
}
private fun findNewestVersionOfTransaction(
oldTransactionInfo: TransactionInfo?,
transactionInfoList: List<TransactionInfo>
): TransactionInfo? {
for (transactionInfo in transactionInfoList) {
if (transactionInfo.hash == oldTransactionInfo?.hash) {
this.transactionInfo = transactionInfo
return this.transactionInfo
}
}
return null
}
private fun getDateTime(time: Long): String {
return DateHelper.DATETIME_FORMATTER.format(Date(time * 1000))
}
override fun onWalletHistoryRefreshed(transactions: List<TransactionInfo>) {
val newTransactionInfo = transactions.find { it.hash != null && it.hash == viewModel.txHash }
viewModel.updateTransactionInfo(newTransactionInfo)
}
}
internal class TransactionActivityViewModel : ViewModel() {
var txHash: String? = null
private val _transactionInfo: MutableLiveData<TransactionInfo?> = MutableLiveData()
val transactionInfo: LiveData<TransactionInfo?> = _transactionInfo
fun updateTransactionInfo(newTransactionInfo: TransactionInfo?) {
_transactionInfo.postValue(newTransactionInfo)
}
}

View File

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

View File

@ -25,28 +25,27 @@ import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.R
import net.mynero.wallet.model.CoinsInfo
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.UTXOService
import net.mynero.wallet.util.Constants
class CoinsInfoAdapter(val listener: CoinsInfoAdapterListener?) :
RecyclerView.Adapter<CoinsInfoAdapter.ViewHolder>() {
private var localDataSet // <public-key, coinsinfo>
: List<CoinsInfo>
class EnotesAdapter(
val streetMode: Boolean,
val listener: EnotesAdapterListener
) : RecyclerView.Adapter<EnotesAdapter.ViewHolder>() {
private var enotes: List<CoinsInfo> = listOf()
val selectedUtxos // <public-key, coinsinfo>
: HashMap<String?, CoinsInfo>
: MutableMap<String?, CoinsInfo>
private var editing = false
/**
* Initialize the dataset of the Adapter.
*/
init {
localDataSet = ArrayList()
selectedUtxos = HashMap()
}
fun submitList(dataSet: HashMap<String?, CoinsInfo>) {
localDataSet = ArrayList(dataSet.values)
fun submitList(enotes: List<CoinsInfo>) {
this.enotes = enotes
notifyDataSetChanged()
}
@ -74,34 +73,27 @@ class CoinsInfoAdapter(val listener: CoinsInfoAdapterListener?) :
notifyDataSetChanged()
}
// Create new views (invoked by the layout manager)
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
// Create a new view, which defines the UI of the list item
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.utxo_selection_item, viewGroup, false)
return ViewHolder(listener, view)
.inflate(R.layout.enote_selection_item, viewGroup, false)
return ViewHolder(listener, view, streetMode)
}
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val tx = localDataSet[position]
val tx = enotes[position]
viewHolder.bind(editing, tx, selectedUtxos)
}
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount(): Int {
return localDataSet.size
return enotes.size
}
interface CoinsInfoAdapterListener {
fun onUtxoSelected(coinsInfo: CoinsInfo)
interface EnotesAdapterListener {
fun onEnoteSelected(coinsInfo: CoinsInfo)
}
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
class ViewHolder(private val listener: CoinsInfoAdapterListener?, view: View) :
class ViewHolder(private val listener: EnotesAdapterListener?, view: View, private val streetMode: Boolean) :
RecyclerView.ViewHolder(view), View.OnClickListener, OnLongClickListener {
private var coinsInfo: CoinsInfo? = null
private var editing = false
@ -114,7 +106,7 @@ class CoinsInfoAdapter(val listener: CoinsInfoAdapterListener?) :
fun bind(
editing: Boolean,
coinsInfo: CoinsInfo,
selectedUtxos: HashMap<String?, CoinsInfo>
selectedUtxos: Map<String?, CoinsInfo>
) {
this.editing = editing
this.coinsInfo = coinsInfo
@ -124,10 +116,8 @@ class CoinsInfoAdapter(val listener: CoinsInfoAdapterListener?) :
val addressTextView = itemView.findViewById<TextView>(R.id.utxo_address_textview)
val globalIdxTextView = itemView.findViewById<TextView>(R.id.utxo_global_index_textview)
val outpointTextView = itemView.findViewById<TextView>(R.id.utxo_outpoint_textview)
val streetModeEnabled =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
val balanceString =
if (streetModeEnabled) Constants.STREET_MODE_BALANCE else Wallet.getDisplayAmount(
if (streetMode) Constants.STREET_MODE_BALANCE else Wallet.getDisplayAmount(
coinsInfo.amount
)
amountTextView.text =
@ -146,7 +136,7 @@ class CoinsInfoAdapter(val listener: CoinsInfoAdapterListener?) :
if (selected) {
itemView.backgroundTintList =
ContextCompat.getColorStateList(itemView.context, R.color.oled_colorSecondary)
} else if (coinsInfo.isFrozen || UTXOService.instance?.isCoinFrozen(coinsInfo) == true) {
} else if (coinsInfo.isFrozen) {
itemView.backgroundTintList =
ContextCompat.getColorStateList(itemView.context, R.color.oled_frozen_utxo)
} else if (!coinsInfo.isUnlocked) {
@ -165,7 +155,7 @@ class CoinsInfoAdapter(val listener: CoinsInfoAdapterListener?) :
if (!editing) return
val unlocked = coinsInfo?.isUnlocked == true
if (unlocked) {
coinsInfo?.let { listener?.onUtxoSelected(it) }
coinsInfo?.let { listener?.onEnoteSelected(it) }
}
}
@ -173,7 +163,7 @@ class CoinsInfoAdapter(val listener: CoinsInfoAdapterListener?) :
if (editing) return false
val unlocked = coinsInfo?.isUnlocked == true
if (unlocked) {
coinsInfo?.let { listener?.onUtxoSelected(it) }
coinsInfo?.let { listener?.onEnoteSelected(it) }
}
return unlocked
}

View File

@ -15,75 +15,66 @@
*/
package net.mynero.wallet.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.R
import net.mynero.wallet.data.DefaultNodes
import net.mynero.wallet.data.Node
import net.mynero.wallet.service.PrefService
class NodeSelectionAdapter(val listener: NodeSelectionAdapterListener?) :
RecyclerView.Adapter<NodeSelectionAdapter.ViewHolder>() {
private var localDataSet: List<Node>
class NodeSelectionAdapter(
private var nodes: List<Node>,
private var selectedNode: Node,
private val listener: NodeSelectionAdapterListener
) : RecyclerView.Adapter<NodeSelectionAdapter.ViewHolder>() {
/**
* Initialize the dataset of the Adapter.
*/
init {
localDataSet = ArrayList()
}
fun submitList(dataSet: List<Node>) {
localDataSet = dataSet
@SuppressLint("NotifyDataSetChanged")
fun setNodes(nodes: List<Node>) {
this.nodes = nodes
notifyDataSetChanged()
}
fun updateSelectedNode() {
notifyDataSetChanged()
fun setSelectedNode(selectedNode: Node) {
val oldSelectedNodePosition = nodes.indexOf(this.selectedNode)
val newSelectedNodePosition = nodes.indexOf(selectedNode)
this.selectedNode = selectedNode
if (oldSelectedNodePosition >= 0) {
notifyItemChanged(oldSelectedNodePosition)
}
if (newSelectedNodePosition >= 0) {
notifyItemChanged(newSelectedNodePosition)
}
}
// Create new views (invoked by the layout manager)
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
// Create a new view, which defines the UI of the list item
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.node_selection_item, viewGroup, false)
val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.node_selection_item, viewGroup, false)
return ViewHolder(listener, view)
}
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val node = localDataSet[position]
viewHolder.bind(node)
val node = nodes[position]
viewHolder.bind(node, node == selectedNode)
}
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount(): Int {
return localDataSet.size
return nodes.size
}
interface NodeSelectionAdapterListener {
fun onSelectNode(node: Node)
fun onSelectEditNode(node: Node): Boolean
fun onEditNode(node: Node)
}
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
class ViewHolder(private val listener: NodeSelectionAdapterListener?, view: View) :
class ViewHolder(private val listener: NodeSelectionAdapterListener, view: View) :
RecyclerView.ViewHolder(
view
) {
fun bind(node: Node) {
val currentNode = PrefService.instance.node
val match = node == currentNode
if (match) {
fun bind(node: Node, isSelected: Boolean) {
if (isSelected) {
itemView.setBackgroundColor(
ContextCompat.getColor(
itemView.context,
@ -104,7 +95,7 @@ class NodeSelectionAdapter(val listener: NodeSelectionAdapterListener?) :
val trustedTextView = itemView.findViewById<TextView>(R.id.trusted_node_textview)
nodeNameTextView.text = node.name
nodeAddressTextView.text = node.address
if (node.password.isNotEmpty()) {
if (!node.password.isNullOrBlank()) {
authTextView.visibility = View.VISIBLE
}
if (node.trusted) {
@ -122,33 +113,10 @@ class NodeSelectionAdapter(val listener: NodeSelectionAdapterListener?) :
nodeAnonymityNetworkImageView.visibility = View.GONE
}
itemView.setOnLongClickListener {
if (match) {
Toast.makeText(
itemView.context,
itemView.resources.getString(R.string.cant_edit_current_node),
Toast.LENGTH_SHORT
).show()
return@setOnLongClickListener true
} else if (isDefaultNode(node)) {
Toast.makeText(
itemView.context,
itemView.resources.getString(R.string.cant_edit_default_nodes),
Toast.LENGTH_SHORT
).show()
return@setOnLongClickListener true
} else {
return@setOnLongClickListener listener?.onSelectEditNode(node) == true
}
listener.onEditNode(node)
return@setOnLongClickListener true
}
itemView.setOnClickListener { listener?.onSelectNode(node) }
}
private fun isDefaultNode(currentNode: Node): Boolean {
var isDefault = false
for (defaultNode in DefaultNodes.values()) {
if (currentNode.toNodeString() == defaultNode.nodeString) isDefault = true
}
return isDefault
itemView.setOnClickListener { listener.onSelectNode(node) }
}
}
}

View File

@ -23,13 +23,14 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.R
import net.mynero.wallet.data.Subaddress
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Helper
import timber.log.Timber
class SubaddressAdapter(
private var addresses: List<Subaddress>,
private var selectedAddress: Subaddress?,
private val streetMode: Boolean,
val listener: SubaddressAdapterListener?
): RecyclerView.Adapter<SubaddressAdapter.ViewHolder>() {
@ -54,7 +55,7 @@ class SubaddressAdapter(
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val address = addresses[position]
viewHolder.bind(address, selectedAddress != null && address == selectedAddress)
viewHolder.bind(address, selectedAddress != null && address == selectedAddress, streetMode)
}
// Return the size of your dataset (invoked by the layout manager)
@ -63,8 +64,8 @@ class SubaddressAdapter(
}
interface SubaddressAdapterListener {
fun onSubaddressSelected(subaddress: Subaddress?)
fun onSubaddressEditLabel(subaddress: Subaddress?)
fun onSubaddressSelected(subaddress: Subaddress)
fun onSubaddressEditLabel(subaddress: Subaddress)
}
/**
@ -74,7 +75,7 @@ class SubaddressAdapter(
class ViewHolder(view: View, val listener: SubaddressAdapterListener?) :
RecyclerView.ViewHolder(view) {
fun bind(subaddress: Subaddress, isSelected: Boolean) {
fun bind(subaddress: Subaddress, isSelected: Boolean, streetMode: Boolean) {
val addressTextView =
itemView.findViewById<TextView>(R.id.address_item_address_textview)
val addressLabelTextView = itemView.findViewById<TextView>(R.id.address_label_textview)
@ -93,9 +94,8 @@ class SubaddressAdapter(
)
addressLabelTextView.text = label.ifEmpty { address }
val amount = subaddress.amount
Timber.e("amount = $amount")
if (amount > 0) {
val streetMode =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
if (streetMode) {
addressAmountTextView.text = itemView.context.getString(
R.string.tx_list_amount_positive,

View File

@ -15,6 +15,7 @@
*/
package net.mynero.wallet.adapter
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -23,7 +24,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.CircularProgressIndicator
import net.mynero.wallet.R
import net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.DateHelper
import net.mynero.wallet.util.Helper
@ -31,22 +31,25 @@ import net.mynero.wallet.util.ThemeHelper
import java.util.Calendar
import java.util.Date
class TransactionInfoAdapter(val listener: TxInfoAdapterListener?) :
RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder>() {
private var localDataSet: List<TransactionInfo>
class TransactionInfoAdapter(
private var streetMode: Boolean,
val listener: TxInfoAdapterListener,
) : RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder>() {
/**
* Initialize the dataset of the Adapter.
*/
init {
localDataSet = ArrayList()
}
private var localDataSet: List<TransactionInfo> = emptyList()
fun submitList(dataSet: List<TransactionInfo>) {
localDataSet = dataSet
notifyDataSetChanged()
}
fun submitStreetMode(newStreetMode: Boolean) {
if (streetMode != newStreetMode) {
streetMode = newStreetMode
notifyDataSetChanged()
}
}
// Create new views (invoked by the layout manager)
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
// Create a new view, which defines the UI of the list item
@ -58,7 +61,7 @@ class TransactionInfoAdapter(val listener: TxInfoAdapterListener?) :
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val tx = localDataSet[position]
viewHolder.bind(tx)
viewHolder.bind(tx, streetMode)
}
// Return the size of your dataset (invoked by the layout manager)
@ -67,7 +70,7 @@ class TransactionInfoAdapter(val listener: TxInfoAdapterListener?) :
}
interface TxInfoAdapterListener {
fun onClickTransaction(txInfo: TransactionInfo?)
fun onClickTransaction(txInfo: TransactionInfo)
}
/**
@ -80,6 +83,7 @@ class TransactionInfoAdapter(val listener: TxInfoAdapterListener?) :
private val inboundColour: Int
private val pendingColour: Int
private val failedColour: Int
private val selfColour: Int
private var amountTextView: TextView? = null
init {
@ -87,16 +91,15 @@ class TransactionInfoAdapter(val listener: TxInfoAdapterListener?) :
outboundColour = ThemeHelper.getThemedColor(view.context, R.attr.negativeColor)
pendingColour = ThemeHelper.getThemedColor(view.context, R.attr.neutralColor)
failedColour = ThemeHelper.getThemedColor(view.context, R.attr.neutralColor)
selfColour = ThemeHelper.getThemedColor(view.context, R.attr.softFavouriteColor)
val cal = Calendar.getInstance()
val tz = cal.timeZone //get the local time zone.
DateHelper.DATETIME_FORMATTER.timeZone = tz
}
fun bind(txInfo: TransactionInfo) {
val streetModeEnabled =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false) == true
fun bind(txInfo: TransactionInfo, streetMode: Boolean) {
val displayAmount =
if (streetModeEnabled) Constants.STREET_MODE_BALANCE else Helper.getDisplayAmount(
if (streetMode) Constants.STREET_MODE_BALANCE else Helper.getDisplayAmount(
txInfo.amount,
Helper.DISPLAY_DIGITS_INFO
)
@ -135,8 +138,22 @@ class TransactionInfoAdapter(val listener: TxInfoAdapterListener?) :
}
} else {
setTxColour(outboundColour)
confirmationsProgressBar.visibility = View.GONE
confirmationsTextView.visibility = View.GONE
if (txInfo.amount == 0L) {
setTxColour(Color.CYAN)
}
if (!txInfo.isConfirmed) {
confirmationsProgressBar.visibility = View.VISIBLE
val confirmations = txInfo.confirmations.toInt()
confirmationsProgressBar.setProgressCompat(confirmations, true)
val confCount = confirmations.toString()
confirmationsTextView.text = confCount
if (confCount.length == 1) // we only have space for character in the progress circle
confirmationsTextView.visibility =
View.VISIBLE else confirmationsTextView.visibility = View.GONE
} else {
confirmationsProgressBar.visibility = View.GONE
confirmationsTextView.visibility = View.GONE
}
}
if (txInfo.direction == TransactionInfo.Direction.Direction_Out) {
amountTextView?.text =

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.mynero.wallet.data
enum class DefaultNode(val value: Node) {
MONERUJO(Node("monerujo", "nodex.monerujo.io:18081", null, null)),
SETHFORPRIVACY(Node("sethforprivacy", "node.sethforprivacy.com:18089", null, null)),
RUCKNIUM(Node("Rucknium", "rucknium.me:18081", null, null)),
STACKWALLET(Node("StackWallet", "monero.stackwallet.com:18081", null, null)),
CAKEWALLET(Node("CakeWallet", "xmr-node.cakewallet.com:18081", null, null)),
HASHVAULT(Node("Hashvault", "nodes.hashvault.pro:18081", null, null)),
MONERUJO_ONION(Node("monerujo.onion", "monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion:18081", null, null)),
SETHFORPRIVACY_ONION(Node("sethforprivacy.onion", "sfprpc5klzs5vyitq2mrooicgk2wcs5ho2nm3niqduvzn5o6ylaslaqd.onion:18089", null, null)),
RUCKNIUM_ONION(Node("rucknium.onion", "rucknium757bokwv3ss35ftgc3gzb7hgbvvglbg3hisp7tsj2fkd2nyd.onion:18081", null, null)),
PLOWSOF_ONION1(Node("plowsof.onion.1", "plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion:18089", null, null)),
PLOWSOF_ONION2(Node("plowsof.onion.2", "plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion:18089", null, null)),
BOLDSUCK(Node("boldsuck.onion", "6dsdenp6vjkvqzy4wzsnzn6wixkdzihx3khiumyzieauxuxslmcaeiad.onion:18081", null, null));
companion object {
fun defaultNodes(): List<Node> = entries.map { it.value }
fun defaultNode(): Node = MONERUJO.value
}
}

View File

@ -1,116 +0,0 @@
/*
* Copyright (c) 2020 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.mynero.wallet.data
import org.json.JSONException
import org.json.JSONObject
// Nodes stolen from https://moneroworld.com/#nodes
enum class DefaultNodes(
val address: String,
private val port: Int,
private val network: String,
private val nodeName: String
) {
MONERUJO(
"nodex.monerujo.io",
18081,
"mainnet",
"monerujo"
),
SETHFORPRIVACY(
"node.sethforprivacy.com",
18089,
"mainnet",
"sethforprivacy"
),
RUCKNIUM(
"rucknium.me",
18081,
"mainnet",
"Rucknium"
),
STACKWALLET(
"monero.stackwallet.com",
18081,
"mainnet",
"StackWallet"
),
CAKEWALLET(
"xmr-node.cakewallet.com",
18081,
"mainnet",
"CakeWallet"
),
HASHVAULT(
"nodes.hashvault.pro",
18081,
"mainnet",
"Hashvault"
),
MONERUJO_ONION(
"monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion",
18081,
"mainnet",
"monerujo.onion"
),
SETHFORPRIVACY_ONION(
"sfprpc5klzs5vyitq2mrooicgk2wcs5ho2nm3niqduvzn5o6ylaslaqd.onion",
18089,
"mainnet",
"sethforprivacy.onion"
),
RUCKNIUM_ONION(
"rucknium757bokwv3ss35ftgc3gzb7hgbvvglbg3hisp7tsj2fkd2nyd.onion",
18081,
"mainnet",
"rucknium.onion"
),
PLOWSOF_ONION1(
"plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion",
18089,
"mainnet",
"plowsof.onion.1"
),
PLOWSOF_ONION2(
"plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion",
18089,
"mainnet",
"plowsof.onion.2"
),
Boldsuck(
"6dsdenp6vjkvqzy4wzsnzn6wixkdzihx3khiumyzieauxuxslmcaeiad.onion",
18081,
"mainnet",
"boldsuck.onion"
);
val nodeString: String
get() = "$address:$port/$network/$nodeName"
val json: JSONObject
get() {
val jsonObject = JSONObject()
try {
jsonObject.put("host", address)
jsonObject.put("rpcPort", port)
jsonObject.put("network", network)
if (nodeName.isNotEmpty()) jsonObject.put("name", nodeName)
} catch (e: JSONException) {
throw RuntimeException(e)
}
return jsonObject
}
}

View File

@ -15,289 +15,51 @@
*/
package net.mynero.wallet.data
import android.util.Log
import net.mynero.wallet.model.NetworkType
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.OnionHelper
import org.json.JSONException
import org.json.JSONObject
import java.io.UnsupportedEncodingException
import java.net.InetSocketAddress
import java.net.URLDecoder
import java.net.URLEncoder
import java.net.UnknownHostException
class Node {
var networkType: NetworkType? = null
private set
var rpcPort = 0
var name: String? = null
private set
var host: String? = null
private var levinPort = 0
var username = ""
private set
var password = ""
private set
var trusted = false
private set
data class Node(
val name: String,
val address: String,
val username: String?,
val password: String?,
val networkType: NetworkType = NetworkType.NetworkType_Mainnet,
val trusted: Boolean = false,
) {
internal constructor(nodeString: String?) {
require(!nodeString.isNullOrEmpty()) { "daemon is empty" }
var daemonAddress: String
val a = nodeString.split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
when (a.size) {
1 -> { // no credentials
daemonAddress = a[0]
this.username = ""
this.password = ""
}
val isOnion: Boolean = false
// get() = host.let { OnionHelper.isOnionHost(it) }
val isI2P: Boolean = false
// get() = host.let { OnionHelper.isI2PHost(it) }
2 -> { // credentials
val userPassword =
a[0].split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
require(userPassword.size == 2) { "User:Password invalid" }
this.username = userPassword[0]
this.password = if (this.username.isNotEmpty()) {
userPassword[1]
} else {
""
}
daemonAddress = a[1]
}
else -> {
throw IllegalArgumentException("Too many @")
}
}
val daParts =
daemonAddress.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
require(!(daParts.size > 3 || daParts.isEmpty())) { "Too many '/' or too few" }
daemonAddress = daParts[0]
val da = daemonAddress.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
require(!(da.size > 2 || da.isEmpty())) { "Too many ':' or too few" }
val host = da[0]
this.networkType = if (daParts.size == 1) {
NetworkType.NetworkType_Mainnet
} else {
when (daParts[1]) {
MAINNET -> NetworkType.NetworkType_Mainnet
STAGENET -> NetworkType.NetworkType_Stagenet
TESTNET -> NetworkType.NetworkType_Testnet
else -> throw IllegalArgumentException("invalid net: " + daParts[1])
}
}
require(networkType == WalletManager.instance?.networkType) { "wrong net: $networkType" }
var name: String? = host
if (daParts.size == 3) {
try {
name = URLDecoder.decode(daParts[2], "UTF-8")
} catch (ex: UnsupportedEncodingException) {
Log.w("Node.kt", ex) // if we can't encode it, we don't use it
}
}
this.name = name
val port: Int = if (da.size == 2) {
try {
da[1].toInt()
} catch (ex: NumberFormatException) {
throw IllegalArgumentException("Port not numeric")
}
} else {
this.defaultRpcPort
}
try {
this.host = host
} catch (ex: UnknownHostException) {
throw IllegalArgumentException("cannot resolve host $host")
}
this.rpcPort = port
this.levinPort = this.defaultLevinPort
}
internal constructor(jsonObject: JSONObject?) {
requireNotNull(jsonObject) { "daemon is empty" }
if (jsonObject.has("username")) {
username = jsonObject.getString("username")
}
if (jsonObject.has("password")) {
password = jsonObject.getString("password")
}
if (jsonObject.has("host")) {
this.host = jsonObject.getString("host")
}
this.rpcPort = if (jsonObject.has("rpcPort")) {
jsonObject.getInt("rpcPort")
} else {
defaultRpcPort
}
if (jsonObject.has("name")) {
this.name = jsonObject.getString("name")
}
if (jsonObject.has("network")) {
networkType = when (jsonObject.getString("network")) {
MAINNET -> NetworkType.NetworkType_Mainnet
STAGENET -> NetworkType.NetworkType_Stagenet
TESTNET -> NetworkType.NetworkType_Testnet
else -> throw IllegalArgumentException("invalid net: " + jsonObject.getString("network"))
}
require(networkType == WalletManager.instance?.networkType) { "wrong net: $networkType" }
}
if (jsonObject.has("trusted")) {
this.trusted = jsonObject.getBoolean("trusted")
}
}
constructor() {
networkType = WalletManager.instance?.networkType
}
// constructor used for created nodes from retrieved peer lists
constructor(socketAddress: InetSocketAddress) : this() {
host = socketAddress.hostString
rpcPort = 0 // unknown
levinPort = socketAddress.port
username = ""
password = ""
}
constructor(anotherNode: Node) {
networkType = anotherNode.networkType
overwriteWith(anotherNode)
}
override fun hashCode(): Int {
return host.hashCode()
}
// Nodes are equal if they are the same host address:port & are on the same network
override fun equals(other: Any?): Boolean {
if (other !is Node) return false
return trusted == other.trusted && host == other.host && address == other.address && rpcPort == other.rpcPort && networkType == other.networkType && username == other.username && password == other.password
}
val isOnion: Boolean
get() = host?.let { OnionHelper.isOnionHost(it) } == true
val isI2P: Boolean
get() = host?.let { OnionHelper.isI2PHost(it) } == true
fun toNodeString(): String {
return toString()
}
fun toJson(): JSONObject {
fun toJson(): Result<JSONObject> = runCatching {
val jsonObject = JSONObject()
try {
if (username.isNotEmpty() && password.isNotEmpty()) {
jsonObject.put("username", username)
jsonObject.put("password", password)
}
jsonObject.put("host", host)
jsonObject.put("rpcPort", rpcPort)
when (networkType) {
NetworkType.NetworkType_Mainnet -> jsonObject.put("network", MAINNET)
NetworkType.NetworkType_Stagenet -> jsonObject.put("network", STAGENET)
NetworkType.NetworkType_Testnet -> jsonObject.put("network", TESTNET)
null -> TODO()
}
if (name?.isNotEmpty() == true) jsonObject.put("name", name)
jsonObject.put("trusted", trusted)
} catch (e: JSONException) {
throw RuntimeException(e)
}
return jsonObject
}
override fun toString(): String {
val sb = StringBuilder()
if (username.isNotEmpty() && password.isNotEmpty()) {
sb.append(username).append(":").append(password).append("@")
}
sb.append(host).append(":").append(rpcPort)
sb.append("/")
when (networkType) {
NetworkType.NetworkType_Mainnet -> sb.append(MAINNET)
NetworkType.NetworkType_Stagenet -> sb.append(STAGENET)
NetworkType.NetworkType_Testnet -> sb.append(TESTNET)
null -> TODO()
}
if (name != null) try {
sb.append("/").append(URLEncoder.encode(name, "UTF-8"))
} catch (ex: UnsupportedEncodingException) {
Log.w("Node.kt", ex) // if we can't encode it, we don't store it
}
return sb.toString()
}
val address: String
get() = "$host:$rpcPort"
private fun overwriteWith(anotherNode: Node) {
check(networkType == anotherNode.networkType) { "network types do not match" }
name = anotherNode.name
host = anotherNode.host
rpcPort = anotherNode.rpcPort
levinPort = anotherNode.levinPort
username = anotherNode.username
password = anotherNode.password
trusted = anotherNode.trusted
jsonObject.put(NAME, name)
jsonObject.put(ADDRESS, address)
jsonObject.put(USERNAME, username)
jsonObject.put(PASSWORD, password)
jsonObject.put(NETWORK_TYPE, networkType.string)
jsonObject.put(TRUSTED, trusted)
return Result.success(jsonObject)
}
companion object {
const val MAINNET = "mainnet"
const val STAGENET = "stagenet"
const val TESTNET = "testnet"
private var DEFAULT_LEVIN_PORT = 0
private var DEFAULT_RPC_PORT = 0
fun fromString(nodeString: String?): Node? {
return try {
Node(nodeString)
} catch (ex: IllegalArgumentException) {
Log.w("Node.kt", ex)
null
}
}
private const val NAME = "name"
private const val ADDRESS = "address"
private const val USERNAME = "username"
private const val PASSWORD = "password"
private const val NETWORK_TYPE = "networkType"
private const val TRUSTED = "trusted"
fun fromJson(jsonObject: JSONObject?): Node? {
return try {
Node(jsonObject)
} catch (ex: IllegalArgumentException) {
Log.w("Node.kt", ex)
null
} catch (ex: UnknownHostException) {
Log.w("Node.kt", ex)
null
} catch (ex: JSONException) {
Log.w("Node.kt", ex)
null
}
fun fromJson(jsonObject: JSONObject): Result<Node> = runCatching {
val name = jsonObject.getString(NAME)
val address = jsonObject.getString(ADDRESS)
val username = if (jsonObject.has(USERNAME)) jsonObject.getString(USERNAME) else null
val password = if (jsonObject.has(PASSWORD)) jsonObject.getString(PASSWORD) else null
val networkType = NetworkType.fromString(jsonObject.getString(NETWORK_TYPE))!!
val trusted = jsonObject.getBoolean(TRUSTED)
return Result.success(Node(name, address, username, password, networkType, trusted))
}
}
private val defaultRpcPort: Int
// every node knows its network, but they are all the same
get() {
if (DEFAULT_RPC_PORT > 0) return DEFAULT_RPC_PORT
DEFAULT_RPC_PORT = when (WalletManager.instance?.networkType) {
NetworkType.NetworkType_Mainnet -> 18081
NetworkType.NetworkType_Testnet -> 28081
NetworkType.NetworkType_Stagenet -> 38081
else -> throw IllegalStateException("unsupported net " + WalletManager.instance?.networkType)
}
return DEFAULT_RPC_PORT
}
private val defaultLevinPort: Int
// every node knows its network, but they are all the same
get() {
if (DEFAULT_LEVIN_PORT > 0) return DEFAULT_LEVIN_PORT
DEFAULT_LEVIN_PORT = when (WalletManager.instance?.networkType) {
NetworkType.NetworkType_Mainnet -> 18080
NetworkType.NetworkType_Testnet -> 28080
NetworkType.NetworkType_Stagenet -> 38080
else -> throw IllegalStateException("unsupported net " + WalletManager.instance?.networkType)
}
return DEFAULT_LEVIN_PORT
}
}

View File

@ -13,16 +13,20 @@ import android.widget.ImageButton
import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.mynero.wallet.R
import net.mynero.wallet.data.Node.Companion.fromJson
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.data.Node
import net.mynero.wallet.model.NetworkType
import net.mynero.wallet.util.Helper.getClipBoardText
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
class AddNodeBottomSheetDialog : BottomSheetDialogFragment() {
var listener: AddNodeListener? = null
class AddNodeBottomSheetDialog(val listener: AddNodeListener) : BottomSheetDialogFragment() {
private lateinit var addNodeButton: Button
private lateinit var addressEditText: EditText
private lateinit var nodeNameEditText: EditText
private lateinit var usernameEditText: EditText
private lateinit var passwordEditText: EditText
private lateinit var trustedDaemonCheckbox: CheckBox
private lateinit var pastePasswordImageButton: ImageButton
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -33,16 +37,19 @@ class AddNodeBottomSheetDialog : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val addNodeButton = view.findViewById<Button>(R.id.add_node_button)
val addressEditText = view.findViewById<EditText>(R.id.address_edittext)
val portEditText = view.findViewById<EditText>(R.id.node_port_edittext)
val nodeNameEditText = view.findViewById<EditText>(R.id.node_name_edittext)
val usernameEditText = view.findViewById<EditText>(R.id.username_edittext)
val passwordEditText = view.findViewById<EditText>(R.id.password_edittext)
val trustedDaemonCheckbox = view.findViewById<CheckBox>(R.id.trusted_node_checkbox)
val pastePasswordImageButton =
view.findViewById<ImageButton>(R.id.paste_password_imagebutton)
addNodeButton = view.findViewById(R.id.add_node_button)
addressEditText = view.findViewById(R.id.address_edittext)
nodeNameEditText = view.findViewById(R.id.node_name_edittext)
usernameEditText = view.findViewById(R.id.username_edittext)
passwordEditText = view.findViewById(R.id.password_edittext)
trustedDaemonCheckbox = view.findViewById(R.id.trusted_node_checkbox)
pastePasswordImageButton = view.findViewById(R.id.paste_password_imagebutton)
bindListeners(view)
}
private fun bindListeners(view: View) {
usernameEditText.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) {}
@ -57,70 +64,32 @@ class AddNodeBottomSheetDialog : BottomSheetDialogFragment() {
}
}
})
addPasteListener(view, addressEditText, R.id.paste_address_imagebutton)
addPasteListener(view, usernameEditText, R.id.paste_username_imagebutton)
addPasteListener(view, passwordEditText, R.id.paste_password_imagebutton)
addNodeButton.setOnClickListener {
var nodeAddr = addressEditText.text.toString()
val portString = portEditText.text.toString()
val name = nodeNameEditText.text.toString()
val user = usernameEditText.text.toString()
val pass = passwordEditText.text.toString()
var address = addressEditText.text.toString()
val username = usernameEditText.text.toString().ifBlank { null }
val password = passwordEditText.text.toString().ifBlank { null }
val trusted = trustedDaemonCheckbox.isChecked
if (name.isEmpty()) {
if (name.isBlank()) {
Toast.makeText(context, "Enter node name", Toast.LENGTH_SHORT).show()
return@setOnClickListener
} else if (nodeAddr.isEmpty() || portString.isEmpty()) {
} else if (address.isBlank()) {
Toast.makeText(context, "Enter node address", Toast.LENGTH_SHORT).show()
return@setOnClickListener
} else if (user.isNotEmpty() && pass.isEmpty()) {
} else if (username != null && password == null) {
Toast.makeText(context, "Enter password", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val jsonObject = JSONObject()
try {
if (user.isNotEmpty()) {
jsonObject.put("username", user)
jsonObject.put("password", pass)
}
if (nodeAddr.contains(":") && !nodeAddr.startsWith("[") && !nodeAddr.endsWith("]")) nodeAddr =
"[$nodeAddr]"
jsonObject.put("host", nodeAddr)
jsonObject.put("rpcPort", portString.toInt())
jsonObject.put("network", "mainnet")
jsonObject.put("name", name)
jsonObject.put("trusted", trusted)
addNodeToSaved(jsonObject)
if (listener != null) {
listener?.onNodeAdded()
dismiss()
}
} catch (e: JSONException) {
throw RuntimeException(e)
}
}
}
@Throws(JSONException::class)
private fun addNodeToSaved(newNodeJsonObject: JSONObject) {
val newNode = fromJson(newNodeJsonObject)
val nodesArray = PrefService.instance?.getString(Constants.PREF_CUSTOM_NODES, "[]")
val jsonArray = JSONArray(nodesArray)
var exists = false
for (i in 0 until jsonArray.length()) {
val nodeJsonObject = jsonArray.getJSONObject(i)
val node = fromJson(nodeJsonObject)
if (node?.toNodeString() == newNode?.toNodeString()) exists = true
val node = Node(name, address, username, password, NetworkType.NetworkType_Mainnet, trusted)
listener.onAddNode(this, node)
}
if (!exists) {
jsonArray.put(newNodeJsonObject)
} else {
Toast.makeText(context, "Node already exists", Toast.LENGTH_SHORT).show()
return
}
PrefService.instance?.edit()?.putString(Constants.PREF_CUSTOM_NODES, jsonArray.toString())
?.apply()
}
private fun addPasteListener(root: View, editText: EditText, layoutId: Int) {
@ -134,6 +103,6 @@ class AddNodeBottomSheetDialog : BottomSheetDialogFragment() {
}
interface AddNodeListener {
fun onNodeAdded()
fun onAddNode(self: AddNodeBottomSheetDialog, node: Node)
}
}

View File

@ -15,49 +15,38 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class EditAddressLabelBottomSheetDialog : BottomSheetDialogFragment() {
var listener: LabelListener? = null
var addressIndex = 0
class EditAddressLabelBottomSheetDialog(
private val currentLabel: String,
private val onSave: (String) -> Unit
) : BottomSheetDialogFragment() {
private lateinit var pasteButton: ImageButton
private lateinit var labelEditText: EditText
private lateinit var saveLabelButton: Button
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.bottom_sheet_dialog_edit_address_label, null)
): View {
return inflater.inflate(R.layout.bottom_sheet_dialog_edit_address_label, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val wallet = WalletManager.instance?.wallet
val pasteButton = view.findViewById<ImageButton>(R.id.paste_password_imagebutton)
val labelEditText = view.findViewById<EditText>(R.id.wallet_password_edittext)
val saveLabelButton = view.findViewById<Button>(R.id.unlock_wallet_button)
var isDate = false
try {
wallet?.getSubaddressLabel(
addressIndex
)?.let {
SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.US).parse(
it
)
isDate = true
}
} catch (ignored: ParseException) {
pasteButton = view.findViewById(R.id.paste_password_imagebutton)
labelEditText = view.findViewById(R.id.wallet_password_edittext)
saveLabelButton = view.findViewById(R.id.unlock_wallet_button)
labelEditText.setText(currentLabel)
pasteButton.setOnClickListener {
labelEditText.setText(getClipBoardText(view.context))
}
labelEditText.setText(if (isDate) null else wallet?.getSubaddressLabel(addressIndex))
pasteButton.setOnClickListener { labelEditText.setText(getClipBoardText(view.context)) }
saveLabelButton.setOnClickListener {
val label = labelEditText.text.toString()
wallet?.setSubaddressLabel(addressIndex, label)
wallet?.store()
if (listener != null) {
listener?.onDismiss()
}
onSave(label)
dismiss()
}
}
interface LabelListener {
fun onDismiss()
}
}
}

View File

@ -14,13 +14,20 @@ import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.mynero.wallet.R
import net.mynero.wallet.data.Node
import net.mynero.wallet.data.Node.Companion.fromJson
import net.mynero.wallet.model.NetworkType
import net.mynero.wallet.util.Helper.getClipBoardText
import org.json.JSONException
import org.json.JSONObject
class EditNodeBottomSheetDialog(val listener: EditNodeListener, val node: Node) : BottomSheetDialogFragment() {
private lateinit var deleteNodeButton: Button
private lateinit var doneEditingButton: Button
private lateinit var addressEditText: EditText
private lateinit var nodeNameEditText: EditText
private lateinit var usernameEditText: EditText
private lateinit var passwordEditText: EditText
private lateinit var trustedDaemonCheckbox: CheckBox
private lateinit var pastePasswordImageButton: ImageButton
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -31,32 +38,33 @@ class EditNodeBottomSheetDialog(val listener: EditNodeListener, val node: Node)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val deleteNodeButton = view.findViewById<Button>(R.id.delete_node_button)
val doneEditingButton = view.findViewById<Button>(R.id.done_editing_button)
val addressEditText = view.findViewById<EditText>(R.id.address_edittext)
val portEditText = view.findViewById<EditText>(R.id.node_port_edittext)
val nodeNameEditText = view.findViewById<EditText>(R.id.node_name_edittext)
val usernameEditText = view.findViewById<EditText>(R.id.username_edittext)
val passwordEditText = view.findViewById<EditText>(R.id.password_edittext)
val trustedDaemonCheckBox = view.findViewById<CheckBox>(R.id.trusted_node_checkbox)
val pastePasswordImageButton =
view.findViewById<ImageButton>(R.id.paste_password_imagebutton)
addressEditText.setText(node.host)
portEditText.setText("${node.rpcPort}")
deleteNodeButton = view.findViewById(R.id.delete_node_button)
doneEditingButton = view.findViewById(R.id.done_editing_button)
addressEditText = view.findViewById(R.id.address_edittext)
nodeNameEditText = view.findViewById(R.id.node_name_edittext)
usernameEditText = view.findViewById(R.id.username_edittext)
passwordEditText = view.findViewById(R.id.password_edittext)
trustedDaemonCheckbox = view.findViewById(R.id.trusted_node_checkbox)
pastePasswordImageButton = view.findViewById(R.id.paste_password_imagebutton)
nodeNameEditText.setText(node.name)
usernameEditText.setText(node.username)
trustedDaemonCheckBox.isChecked = node.trusted ?: false
if (node.password.isNotEmpty()) {
passwordEditText.setText(node.password)
addressEditText.setText(node.address)
node.username?.let {
usernameEditText.setText(it)
passwordEditText.visibility = View.VISIBLE
pastePasswordImageButton.visibility = View.VISIBLE
}
node.password.let {
passwordEditText.setText(it)
}
trustedDaemonCheckbox.isChecked = node.trusted
usernameEditText.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) {
if (editable.toString().isEmpty()) {
passwordEditText.text = null
passwordEditText.visibility = View.GONE
pastePasswordImageButton.visibility = View.GONE
} else {
@ -65,49 +73,35 @@ class EditNodeBottomSheetDialog(val listener: EditNodeListener, val node: Node)
}
}
})
addPasteListener(view, addressEditText, R.id.paste_address_imagebutton)
addPasteListener(view, usernameEditText, R.id.paste_username_imagebutton)
addPasteListener(view, passwordEditText, R.id.paste_password_imagebutton)
deleteNodeButton.setOnClickListener {
listener.onNodeDeleted(node)
dismiss()
listener.onDeleteNode(this, node)
}
doneEditingButton.setOnClickListener {
var nodeAddr = addressEditText.text.toString()
val portString = portEditText.text.toString()
val nodeName = nodeNameEditText.text.toString()
val user = usernameEditText.text.toString()
val pass = passwordEditText.text.toString()
val trusted = trustedDaemonCheckBox.isChecked
if (nodeName.isEmpty()) {
val name = nodeNameEditText.text.toString()
var address = addressEditText.text.toString()
val username = usernameEditText.text.toString().ifBlank { null }
val password = passwordEditText.text.toString().ifBlank { null }
val trusted = trustedDaemonCheckbox.isChecked
if (name.isBlank()) {
Toast.makeText(context, "Enter node name", Toast.LENGTH_SHORT).show()
return@setOnClickListener
} else if (nodeAddr.isEmpty() || portString.isEmpty()) {
} else if (address.isBlank()) {
Toast.makeText(context, "Enter node address", Toast.LENGTH_SHORT).show()
return@setOnClickListener
} else if (user.isNotEmpty() && pass.isEmpty()) {
} else if (username != null && password == null) {
Toast.makeText(context, "Enter password", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val jsonObject = JSONObject()
try {
if (user.isNotEmpty()) {
jsonObject.put("username", user)
jsonObject.put("password", pass)
}
if (nodeAddr.contains(":") && !nodeAddr.startsWith("[") && !nodeAddr.endsWith("]"))
nodeAddr = "[$nodeAddr]"
jsonObject.put("host", nodeAddr)
jsonObject.put("rpcPort", portString.toInt())
jsonObject.put("network", "mainnet")
jsonObject.put("name", nodeName)
jsonObject.put("trusted", trusted)
listener.onNodeEdited(node, fromJson(jsonObject))
dismiss()
} catch (e: JSONException) {
throw RuntimeException(e)
}
val newNode = Node(name, address, username, password, NetworkType.NetworkType_Mainnet, trusted)
listener.onEditNode(this, node, newNode)
}
}
@ -122,7 +116,7 @@ class EditNodeBottomSheetDialog(val listener: EditNodeListener, val node: Node)
}
interface EditNodeListener {
fun onNodeDeleted(node: Node)
fun onNodeEdited(oldNode: Node?, newNode: Node?)
fun onDeleteNode(self: EditNodeBottomSheetDialog, node: Node)
fun onEditNode(self: EditNodeBottomSheetDialog, oldNode: Node, newNode: Node)
}
}

View File

@ -11,19 +11,18 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.mynero.wallet.R
import net.mynero.wallet.adapter.NodeSelectionAdapter
import net.mynero.wallet.adapter.NodeSelectionAdapter.NodeSelectionAdapterListener
import net.mynero.wallet.data.DefaultNodes
import net.mynero.wallet.data.Node
import net.mynero.wallet.data.Node.Companion.fromJson
import net.mynero.wallet.data.Node.Companion.fromString
import net.mynero.wallet.service.DaemonService
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.util.Constants
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
class NodeSelectionBottomSheetDialog(val listener: NodeSelectionDialogListener) : BottomSheetDialogFragment(), NodeSelectionAdapterListener {
private var adapter: NodeSelectionAdapter? = null
class NodeSelectionBottomSheetDialog(
private val nodes: List<Node>,
private var selectedNode: Node,
private val listener: NodeSelectionDialogListener,
) : BottomSheetDialogFragment(), NodeSelectionAdapterListener {
private lateinit var addNodeButton: Button
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: NodeSelectionAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -34,73 +33,41 @@ class NodeSelectionBottomSheetDialog(val listener: NodeSelectionDialogListener)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val nodes = ArrayList<Node>()
adapter = NodeSelectionAdapter(this)
val recyclerView = view.findViewById<RecyclerView>(R.id.node_selection_recyclerview)
recyclerView = view.findViewById(R.id.node_selection_recyclerview)
addNodeButton = view.findViewById(R.id.add_node_button)
adapter = NodeSelectionAdapter(nodes, selectedNode, this)
recyclerView.layoutManager = LinearLayoutManager(activity)
recyclerView.adapter = adapter
val addNodeButton = view.findViewById<Button>(R.id.add_node_button)
bindListeners()
adapter.setNodes(nodes)
}
private fun bindListeners() {
addNodeButton.setOnClickListener {
listener.onClickedAddNode()
dismiss()
listener.onAddNode(this)
}
val nodesArray = PrefService.instance.getString(Constants.PREF_CUSTOM_NODES, "[]")
val jsonArray: JSONArray = try {
JSONArray(nodesArray)
} catch (e: JSONException) {
return
}
for (i in 0 until jsonArray.length()) {
var nodeJsonObject: JSONObject?
try {
nodeJsonObject = jsonArray.getJSONObject(i)
if (nodeJsonObject != null) {
val node = fromJson(nodeJsonObject)
if (node != null) {
nodes.add(node)
}
}
} catch (e: JSONException) {
// if stored node is old string format, try parse and upgrade, if fail then remove
try {
val nodeString = jsonArray.getString(i)
val node = fromString(nodeString)
if (node != null) {
nodes.add(node)
jsonArray.put(i, node.toJson())
} else {
jsonArray.remove(i)
}
} catch (ex: JSONException) {
throw RuntimeException(ex)
}
}
}
PrefService.instance.edit().putString(Constants.PREF_CUSTOM_NODES, jsonArray.toString()).apply()
for (defaultNode in DefaultNodes.values()) {
fromJson(defaultNode.json)?.let { nodes.add(it) }
}
adapter?.submitList(nodes)
}
fun setSelectedNode(selectedNode: Node) {
this.selectedNode = selectedNode
adapter.setSelectedNode(selectedNode)
}
override fun onSelectNode(node: Node) {
PrefService.instance.edit().putString(Constants.PREF_NODE_2, node.toJson().toString()).apply()
DaemonService.instance?.setDaemon(node)
activity?.runOnUiThread {
adapter?.updateSelectedNode()
}
listener.onNodeSelected()
listener.onSelectNode(this, node)
}
override fun onSelectEditNode(node: Node): Boolean {
listener.onClickedEditNode(node)
dismiss()
return true
override fun onEditNode(node: Node) {
listener.onEditNode(this, node)
}
interface NodeSelectionDialogListener {
fun onNodeSelected()
fun onClickedEditNode(node: Node)
fun onClickedAddNode()
fun onSelectNode(self: NodeSelectionBottomSheetDialog, node: Node)
fun onEditNode(self: NodeSelectionBottomSheetDialog, node: Node)
fun onAddNode(self: NodeSelectionBottomSheetDialog)
}
}

View File

@ -8,13 +8,14 @@ import android.widget.ImageButton
import android.widget.TextView
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.mynero.wallet.R
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Helper.clipBoardCopy
class WalletKeysBottomSheetDialog : BottomSheetDialogFragment() {
var password = ""
class WalletKeysBottomSheetDialog(
private val usesOffset: Boolean,
private val seed: String,
private val privateViewKey: String,
private val restoreHeight: Long,
) : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
@ -30,17 +31,12 @@ class WalletKeysBottomSheetDialog : BottomSheetDialogFragment() {
val informationTextView = view.findViewById<TextView>(R.id.information_textview) // seed
val viewKeyTextView = view.findViewById<TextView>(R.id.viewkey_textview)
val restoreHeightTextView = view.findViewById<TextView>(R.id.restore_height_textview)
val wallet = WalletManager.instance?.wallet
var seed = wallet?.getSeed("")
val usesOffset = PrefService.instance?.getBoolean(Constants.PREF_USES_OFFSET, false) == true
if (usesOffset) {
seed = wallet?.getSeed(password)
view.findViewById<View>(R.id.wallet_seed_offset_textview).visibility = View.VISIBLE
}
val privateViewKey = wallet?.getSecretViewKey()
informationTextView.text = seed
viewKeyTextView.text = privateViewKey
restoreHeightTextView.text = "${wallet?.getRestoreHeight()}"
restoreHeightTextView.text = "${restoreHeight}"
copyViewKeyImageButton.setOnClickListener {
clipBoardCopy(
context, "private view-key", privateViewKey

View File

@ -2,72 +2,59 @@ package net.mynero.wallet.listener
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
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.EditNodeBottomSheetDialog
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Utils
import org.json.JSONArray
import net.mynero.wallet.util.PreferenceUtils
import timber.log.Timber
class NodeSelectionDialogListenerAdapter(
val activity: FragmentActivity,
val setSelectNodeButtonText: (String) -> Unit,
val getProxyAndPortValues: () -> Pair<String, String>,
abstract class NodeSelectionDialogListenerAdapter(
private val activity: FragmentActivity,
private var nodes: List<Node>,
) : NodeSelectionBottomSheetDialog.NodeSelectionDialogListener {
override fun onNodeSelected() {
val node = PrefService.instance.node
setSelectNodeButtonText(activity.getString(R.string.node_button_text, node?.name))
abstract fun trySelectNode(self: NodeSelectionBottomSheetDialog, node: Node): Boolean
val (proxyAddress, proxyPort) = getProxyAndPortValues()
Utils.refreshProxy(proxyAddress, proxyPort)
activity.runOnUiThread {
Toast.makeText(
activity,
activity.getString(R.string.node_selected, node?.name ?: node?.host),
Toast.LENGTH_SHORT
).show()
final override fun onSelectNode(self: NodeSelectionBottomSheetDialog, node: Node) {
if (trySelectNode(self, node)) {
self.setSelectedNode(node)
}
}
override fun onClickedEditNode(node: Node) {
final override fun onEditNode(self: NodeSelectionBottomSheetDialog, node: Node) {
val 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)
override fun onDeleteNode(self: EditNodeBottomSheetDialog, node: Node) {
val newNodes = nodes.toMutableList()
newNodes.remove(node)
PreferenceUtils.setNodes(activity, newNodes).fold(
{
Toast.makeText(activity, "Node delete", Toast.LENGTH_SHORT).show()
self.dismiss()
},
{
Timber.e(it, "Failed to delete node")
Toast.makeText(activity, "Failed to delete node", Toast.LENGTH_SHORT).show()
}
saveNodes(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()
)
override fun onEditNode(
self: EditNodeBottomSheetDialog,
oldNode: Node,
newNode: Node,
) {
val newNodes = nodes.map { node -> if (node != oldNode) node else newNode }
PreferenceUtils.setNodes(activity, newNodes).fold(
{
Toast.makeText(activity, "Node saved", Toast.LENGTH_SHORT).show()
self.dismiss()
},
{
Timber.e(it, "Failed to save node")
Toast.makeText(activity, "Failed to save node", Toast.LENGTH_SHORT).show()
}
saveNodes(jsonArray)
} catch (e: Exception) {
e.printStackTrace()
}
)
}
}
@ -76,15 +63,29 @@ class NodeSelectionDialogListenerAdapter(
editNodeDialog.show(activity.supportFragmentManager, "edit_node_dialog")
}
override fun onClickedAddNode() {
val addNodeDialog = AddNodeBottomSheetDialog()
addNodeDialog.listener = object : AddNodeBottomSheetDialog.AddNodeListener {
override fun onNodeAdded() {}
}
final override fun onAddNode(self: NodeSelectionBottomSheetDialog) {
val addNodeDialog = AddNodeBottomSheetDialog(object : AddNodeBottomSheetDialog.AddNodeListener {
override fun onAddNode(self: AddNodeBottomSheetDialog, node: Node) {
if (nodes.any { it.name == node.name }) {
Timber.i("Failed to add nodes due to node with the same name already existing")
Toast.makeText(activity, "Node with this name already exists", Toast.LENGTH_SHORT).show()
} else {
val newNodes = nodes.toMutableList()
newNodes.add(node)
PreferenceUtils.setNodes(activity, newNodes).fold(
{
Timber.i("New node ${node.name} added successfully")
Toast.makeText(activity, "Node added", Toast.LENGTH_SHORT).show()
self.dismiss()
},
{
Timber.e(it, "Failed to add new node")
Toast.makeText(activity, "Failed to add node: ${it.message}", Toast.LENGTH_SHORT).show()
}
)
}
}
})
addNodeDialog.show(activity.supportFragmentManager, "add_node_dialog")
}
private fun saveNodes(jsonArray: JSONArray) {
PrefService.instance.edit().putString(Constants.PREF_CUSTOM_NODES, jsonArray.toString()).apply()
}
}

View File

@ -1,77 +0,0 @@
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.mynero.wallet.livedata
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes.
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner) { value ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(value)
}
}
}
@MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
@MainThread
override fun postValue(value: T) {
mPending.set(true)
super.postValue(value)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
companion object {
private const val TAG = "SingleLiveEvent"
}
}

View File

@ -0,0 +1,3 @@
package net.mynero.wallet.model
data class Balance(val total: Long, val unlocked: Long, val frozen: Long, val pending: Long)

View File

@ -1,32 +0,0 @@
package net.mynero.wallet.model
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.util.Constants
class BalanceInfo(val rawUnlocked: Long, val rawLocked: Long) {
val isUnlockedBalanceZero: Boolean
get() = rawUnlocked == 0L
val isLockedBalanceZero: Boolean
get() = rawLocked == 0L
val unlockedDisplay: String
get() {
val streetModeEnabled =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false)
return if (streetModeEnabled == true) {
Constants.STREET_MODE_BALANCE
} else {
Wallet.getDisplayAmount(rawUnlocked)
}
}
val lockedDisplay: String
get() {
val streetModeEnabled =
PrefService.instance?.getBoolean(Constants.PREF_STREET_MODE, false)
return if (streetModeEnabled == true) {
Constants.STREET_MODE_BALANCE
} else {
Wallet.getDisplayAmount(rawLocked)
}
}
}

View File

@ -28,12 +28,6 @@ class Coins(private val handle: Long) {
}
external fun setFrozen(publicKey: String?, frozen: Boolean)
private external fun refreshJ(): List<CoinsInfo>
external fun refreshJ(): List<CoinsInfo>
external fun getCount(): Int
companion object {
init {
System.loadLibrary("monerujo")
}
}
}

View File

@ -90,9 +90,5 @@ class CoinsInfo : Parcelable, Comparable<CoinsInfo> {
return arrayOfNulls(size)
}
}
init {
System.loadLibrary("monerujo")
}
}
}

View File

@ -15,17 +15,16 @@
*/
package net.mynero.wallet.model
enum class NetworkType(val value: Int) {
NetworkType_Mainnet(0), NetworkType_Testnet(1), NetworkType_Stagenet(2);
enum class NetworkType(val value: Int, val string: String) {
NetworkType_Mainnet(0, "mainnet"),
NetworkType_Testnet(1, "mainnet"),
NetworkType_Stagenet(2, "mainnet");
companion object {
fun fromInteger(n: Int): NetworkType? {
when (n) {
0 -> return NetworkType_Mainnet
1 -> return NetworkType_Testnet
2 -> return NetworkType_Stagenet
}
return null
}
fun fromInteger(n: Int): NetworkType? = entries.find { it.value == n }
fun fromString(s: String): NetworkType? = entries.find { it.string == s }
}
}
}

View File

@ -38,7 +38,7 @@ class PendingTransaction internal constructor(var handle: Long) {
Status_Ok, Status_Error, Status_Critical
}
enum class Priority(value: Int) {
enum class Priority(val value: Int) {
Priority_Default(0), Priority_Low(1), Priority_Medium(2), Priority_High(3), Priority_Last(4);
companion object {
@ -54,10 +54,4 @@ class PendingTransaction internal constructor(var handle: Long) {
}
}
}
companion object {
init {
System.loadLibrary("monerujo")
}
}
}

View File

@ -54,11 +54,6 @@ class TransactionHistory(private val handle: Long, var accountIndex: Int) {
all = transactionInfos
}
private external fun refreshJ(): MutableList<TransactionInfo>
companion object {
init {
System.loadLibrary("monerujo")
}
}
external fun refreshJ(): MutableList<TransactionInfo>
external fun getAllJ(): MutableList<TransactionInfo>
}

View File

@ -101,11 +101,6 @@ class TransactionInfo : Parcelable, Comparable<TransactionInfo> {
val isConfirmed: Boolean
get() = confirmations >= CONFIRMATION
val displayLabel: String?
get() = if (subaddressLabel?.isEmpty() == true || Subaddress.DEFAULT_LABEL_FORMATTER.matcher(
subaddressLabel.toString()
).matches()
) "#$addressIndex" else subaddressLabel
override fun toString(): String {
return "$direction@$blockheight $amount"

View File

@ -1,3 +0,0 @@
package net.mynero.wallet.model
class TransactionOutput(val destination: String, val amount: Long)

View File

@ -18,6 +18,8 @@ package net.mynero.wallet.model
import android.util.Log
import android.util.Pair
import net.mynero.wallet.data.Subaddress
import net.mynero.wallet.util.TransactionDestination
import timber.log.Timber
import java.io.File
class Wallet {
@ -65,7 +67,7 @@ class Wallet {
val name: String
get() = getPath()?.let { File(it).name }.toString()
external fun getSeed(offset: String?): String?
external fun getSeed(offset: String): String
external fun getLegacySeed(offset: String?): String?
external fun isPolyseedSupported(offset: String?): Boolean
external fun getSeedLanguage(): String?
@ -81,6 +83,10 @@ class Wallet {
private external fun statusWithErrorString(): Status
fun getTransactionHistory(): TransactionHistory {
return TransactionHistory(getHistoryJ(), accountIndex)
}
@Synchronized
external fun setPassword(password: String?): Boolean
val address: String
@ -124,8 +130,7 @@ class Wallet {
}
external fun getPath(): String?
val networkType: NetworkType?
get() = NetworkType.fromInteger(nettype())
fun networkType(): NetworkType? = NetworkType.fromInteger(nettype())
external fun nettype(): Int
@ -145,52 +150,12 @@ class Wallet {
external fun store(path: String?): Boolean
fun close(): Boolean {
disposePendingTransaction()
return WalletManager.instance?.close(this) == true
return WalletManager.instance.closeJ(this)
}
external fun getFilename(): String
// virtual std::string keysFilename() const = 0;
fun init(upperTransactionSizeLimit: Long): Boolean {
var daemonAddress = WalletManager.instance?.getDaemonAddress()
var daemonUsername = WalletManager.instance?.daemonUsername
var daemonPassword = WalletManager.instance?.daemonPassword
var proxyAddress = WalletManager.instance?.proxy
Log.d("Wallet.kt", "init(")
if (daemonAddress != null) {
Log.d("Wallet.kt", daemonAddress.toString())
} else {
Log.d("Wallet.kt", "daemon_address == null")
daemonAddress = ""
}
Log.d("Wallet.kt", "upper_transaction_size_limit = 0 (probably)")
if (daemonUsername != null) {
Log.d("Wallet.kt", daemonUsername)
} else {
Log.d("Wallet.kt", "daemon_username == null")
daemonUsername = ""
}
if (daemonPassword != null) {
Log.d("Wallet.kt", daemonPassword)
} else {
Log.d("Wallet.kt", "daemon_password == null")
daemonPassword = ""
}
if (proxyAddress != null) {
Log.d("Wallet.kt", proxyAddress)
} else {
Log.d("Wallet.kt", "proxy_address == null")
proxyAddress = ""
}
Log.d("Wallet.kt", ");")
return initJ(
daemonAddress, upperTransactionSizeLimit,
daemonUsername, daemonPassword,
proxyAddress
)
}
private external fun initJ(
external fun initJ(
daemonAddress: String, upperTransactionSizeLimit: Long,
daemonUsername: String, daemonPassword: String, proxyAddress: String
): Boolean
@ -198,7 +163,7 @@ class Wallet {
external fun getRestoreHeight(): Long
external fun setRestoreHeight(height: Long)
private val connectionStatus: ConnectionStatus
val connectionStatus: ConnectionStatus
get() {
val s = getConnectionStatusJ()
return ConnectionStatus.values()[s]
@ -234,17 +199,11 @@ class Wallet {
external fun getUnlockedBalance(accountIndex: Int): Long
external fun isWatchOnly(): Boolean
fun getBlockChainHeight(): Long {
return getBlockChainHeightJ().minus(1)
}
private external fun getBlockChainHeightJ(): Long
external fun getBlockChainHeightJ(): Long
external fun getApproximateBlockChainHeight(): Long
fun getDaemonBlockChainHeight(): Long {
return getDaemonBlockChainHeightJ().minus(1)
}
private external fun getDaemonBlockChainHeightJ(): Long
external fun getDaemonBlockChainHeightJ(): Long
external fun getDaemonBlockChainTargetHeight(): Long
fun setSynchronized() {
@ -291,26 +250,26 @@ class Wallet {
dstAddr: String,
priority: PendingTransaction.Priority,
keyImages: ArrayList<String>
): PendingTransaction? {
): PendingTransaction {
disposePendingTransaction()
val _priority = priority.ordinal
val txHandle = createSweepTransaction(dstAddr, "", 0, _priority, accountIndex, keyImages)
pendingTransaction = PendingTransaction(txHandle)
return pendingTransaction
return pendingTransaction!!
}
fun createTransactionMultDest(
outputs: List<TransactionOutput>,
outputs: List<TransactionDestination>,
priority: PendingTransaction.Priority,
keyImages: ArrayList<String>
): PendingTransaction? {
): PendingTransaction {
disposePendingTransaction()
val _priority = priority.ordinal
val destinations = ArrayList<String>()
val amounts = LongArray(outputs.size)
for (i in outputs.indices) {
val output = outputs[i]
destinations.add(output.destination)
destinations.add(output.address)
amounts[i] = output.amount
}
val txHandle = createTransactionMultDestJ(
@ -318,7 +277,7 @@ class Wallet {
accountIndex, keyImages
)
pendingTransaction = PendingTransaction(txHandle)
return pendingTransaction
return pendingTransaction!!
}
private external fun createTransactionMultDestJ(
@ -463,10 +422,6 @@ class Wallet {
const val SWEEP_ALL = Long.MAX_VALUE
private const val NEW_ACCOUNT_NAME = "Untitled account" // src/wallet/wallet2.cpp:941
init {
System.loadLibrary("monerujo")
}
@JvmStatic
external fun getDisplayAmount(amount: Long): String

View File

@ -18,39 +18,19 @@ package net.mynero.wallet.model
import android.util.Log
import net.mynero.wallet.data.Node
import net.mynero.wallet.util.RestoreHeight
import timber.log.Timber
import java.io.File
import java.util.Calendar
import java.util.Locale
class WalletManager {
val networkType = NetworkType.NetworkType_Mainnet
var wallet: Wallet? = null
private set
private var daemonAddress: String? = null
var daemonUsername = ""
private set
var daemonPassword = ""
private set
var proxy = ""
private set
private fun manageWallet(wallet: Wallet) {
Log.d("WalletManager.kt", "Managing ${wallet.name}")
this.wallet = wallet
}
private fun unmanageWallet(wallet: Wallet?) {
requireNotNull(wallet) { "Cannot unmanage null!" }
checkNotNull(this.wallet) { "No wallet under management!" }
check(this.wallet === wallet) { wallet.name + " not under management!" }
Log.d("WalletManager.kt", "Unmanaging ${wallet.name}")
this.wallet = null
}
fun createWallet(aFile: File, password: String, language: String, height: Long): Wallet {
val walletHandle = createWalletJ(aFile.absolutePath, password, language, networkType.value)
val wallet = Wallet(walletHandle)
manageWallet(wallet)
if (wallet.status.isOk) {
// (Re-)Estimate restore height based on what we know
val oldHeight = wallet.getRestoreHeight()
@ -90,7 +70,6 @@ class WalletManager {
networkType.value
)
val wallet = Wallet(walletHandle)
manageWallet(wallet)
if (wallet.status.isOk) {
wallet.setPassword(password) // this rewrites the keys file (which contains the restore height)
} else Log.e("WalletManager.kt", wallet.status.toString())
@ -105,18 +84,10 @@ class WalletManager {
networkType: Int
): Long
fun openAccount(path: String, accountIndex: Int, password: String): Wallet {
val walletHandle = openWalletJ(path, password, networkType.value)
val wallet = Wallet(walletHandle, accountIndex)
manageWallet(wallet)
return wallet
}
//TODO virtual bool checkPayment(const std::string &address, const std::string &txid, const std::string &txkey, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const = 0;
fun openWallet(path: String, password: String): Wallet {
val walletHandle = openWalletJ(path, password, networkType.value)
val wallet = Wallet(walletHandle)
manageWallet(wallet)
return wallet
}
@ -132,7 +103,6 @@ class WalletManager {
networkType.value, restoreHeight
)
val wallet = Wallet(walletHandle)
manageWallet(wallet)
return wallet
}
@ -152,7 +122,6 @@ class WalletManager {
networkType.value
)
val wallet = Wallet(walletHandle)
manageWallet(wallet)
return wallet
}
@ -172,7 +141,6 @@ class WalletManager {
addressString, viewKeyString, spendKeyString
)
val wallet = Wallet(walletHandle)
manageWallet(wallet)
return wallet
}
@ -186,20 +154,6 @@ class WalletManager {
spendKeyString: String
): Long
fun createWalletFromDevice(
aFile: File, password: String, restoreHeight: Long,
deviceName: String
): Wallet {
val walletHandle = createWalletFromDeviceJ(
aFile.absolutePath, password,
networkType.value, deviceName, restoreHeight,
"5:20"
)
val wallet = Wallet(walletHandle)
manageWallet(wallet)
return wallet
}
private external fun createWalletFromDeviceJ(
path: String, password: String,
networkType: Int,
@ -208,17 +162,7 @@ class WalletManager {
subaddressLookahead: String
): Long
private external fun closeJ(wallet: Wallet?): Boolean
fun close(wallet: Wallet): Boolean {
unmanageWallet(wallet)
val closed = closeJ(wallet)
if (!closed) {
// in case we could not close it
// we manage it again
manageWallet(wallet)
}
return closed
}
external fun closeJ(wallet: Wallet?): Boolean
fun walletExists(aFile: File): Boolean {
return walletExists(aFile.absolutePath)
@ -237,29 +181,7 @@ class WalletManager {
private external fun queryWalletDeviceJ(keysFileName: String, password: String): Int
// this should not be called on the main thread as it connects to the node (and takes a long time)
fun setDaemon(node: Node?) {
if (node != null) {
daemonAddress = node.address
require(networkType === node.networkType) { "network type does not match" }
daemonUsername = node.username
daemonPassword = node.password
daemonAddress?.let { addr -> setDaemonAddressJ(addr) }
} else {
daemonAddress = null
daemonUsername = ""
daemonPassword = ""
//setDaemonAddressJ(""); // don't disconnect as monero code blocks for many seconds!
//TODO: need to do something about that later
}
}
fun getDaemonAddress(): String? {
checkNotNull(daemonAddress) { "use setDaemon() to initialise daemon and net first!" }
return daemonAddress
}
private external fun setDaemonAddressJ(address: String)
external fun setDaemonAddressJ(address: String)
external fun getDaemonVersion(): Int
external fun getBlockchainHeight(): Long
external fun getBlockchainTargetHeight(): Long
@ -306,20 +228,7 @@ class WalletManager {
var LOGLEVEL_TRACE = 3
var LOGLEVEL_MAX = 4
// no need to keep a reference to the REAL WalletManager (we get it every tvTime we need it)
@get:Synchronized
var instance: WalletManager? = null
get() {
if (field == null) {
field = WalletManager()
}
return field
}
private set
init {
System.loadLibrary("monerujo")
}
val instance: WalletManager = WalletManager()
fun addressPrefix(networkType: NetworkType): String {
return when (networkType) {

View File

@ -1,38 +0,0 @@
package net.mynero.wallet.service
import net.mynero.wallet.data.Subaddress
import net.mynero.wallet.model.WalletManager
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class AddressService {
var numAddresses = 1
private set
init {
instance = this
}
fun refreshAddresses() {
WalletManager.instance?.wallet?.numSubaddresses?.let { numAddresses = it }
}
fun freshSubaddress(): Subaddress? {
val timeStamp = SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.US).format(Date())
val wallet = WalletManager.instance?.wallet
wallet?.addSubaddress(wallet.getAccountIndex(), timeStamp)
refreshAddresses()
wallet?.store()
return wallet?.getSubaddressObject(numAddresses - 1)
}
fun currentSubaddress(): Subaddress? {
val wallet = WalletManager.instance?.wallet
return wallet?.getSubaddressObject(numAddresses - 1)
}
companion object {
var instance: AddressService? = null
}
}

View File

@ -1,55 +0,0 @@
package net.mynero.wallet.service
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.BalanceInfo
class BalanceService {
private val _balanceInfo = MutableLiveData<BalanceInfo?>(null)
var balanceInfo: LiveData<BalanceInfo?> = _balanceInfo
init {
instance = this
}
fun refreshBalance() {
val rawUnlocked = unlockedBalanceRaw
val rawLocked = lockedBalanceRaw
_balanceInfo.postValue(BalanceInfo(rawUnlocked, rawLocked))
}
val unlockedBalanceRaw: Long
get() {
var unlocked: Long = 0
val utxos = UTXOService.instance?.getUtxos() ?: emptyList()
for (coinsInfo in utxos) {
if (!coinsInfo.isSpent && !coinsInfo.isFrozen && coinsInfo.isUnlocked && UTXOService.instance?.isCoinFrozen(
coinsInfo
) == false
) {
unlocked += coinsInfo.amount
}
}
return unlocked
}
val totalBalanceRaw: Long
get() {
var total: Long = 0
val utxos = UTXOService.instance?.getUtxos() ?: emptyList()
for (coinsInfo in utxos) {
if (!coinsInfo.isSpent && !coinsInfo.isFrozen && UTXOService.instance?.isCoinFrozen(
coinsInfo
) == false
) {
total += coinsInfo.amount
}
}
return total
}
val lockedBalanceRaw: Long
get() = totalBalanceRaw - unlockedBalanceRaw
companion object {
var instance: BalanceService? = null
}
}

View File

@ -1,40 +0,0 @@
package net.mynero.wallet.service
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.Wallet.ConnectionStatus
import net.mynero.wallet.model.WalletManager
class BlockchainService {
private val _currentHeight = MutableLiveData(0L)
var height: LiveData<Long> = _currentHeight
var daemonHeight: Long = 0
set(height) {
val t = System.currentTimeMillis()
if (height > 0) {
field = height
lastDaemonHeightUpdateTimeMs = t
} else {
if (t - lastDaemonHeightUpdateTimeMs > 15000) {
field = WalletManager.instance?.wallet?.getDaemonBlockChainHeight() ?: return
lastDaemonHeightUpdateTimeMs = t
}
}
}
private var lastDaemonHeightUpdateTimeMs: Long = 0
init {
instance = this
}
fun refreshBlockchain() {
_currentHeight.postValue(currentHeight)
}
private val currentHeight: Long
get() = WalletManager.instance?.wallet?.getBlockChainHeight() ?: -1
companion object {
var instance: BlockchainService? = null
}
}

View File

@ -1,22 +0,0 @@
package net.mynero.wallet.service
import net.mynero.wallet.data.Node
import net.mynero.wallet.livedata.SingleLiveEvent
class DaemonService {
val daemonChangeEvents: SingleLiveEvent<Node> = SingleLiveEvent()
init {
instance = this
}
fun setDaemon(daemon: Node) {
daemonChangeEvents.postValue(daemon)
}
companion object {
var instance: DaemonService? = null
private set
}
}

View File

@ -1,28 +0,0 @@
package net.mynero.wallet.service
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.model.WalletManager
class HistoryService {
private val _history = MutableLiveData<List<TransactionInfo>>()
var history: LiveData<List<TransactionInfo>> = _history
init {
instance = this
}
fun refreshHistory() {
_history.postValue(getHistory())
}
private fun getHistory(): List<TransactionInfo> {
return WalletManager.instance?.wallet?.history?.all ?: emptyList()
}
companion object {
var instance: HistoryService? = null
private set
}
}

View File

@ -16,202 +16,22 @@
*/
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
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
import android.os.Looper
/**
* 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(val wallet: Wallet, val context: Context) : Thread(null, null, "WalletService", THREAD_STACK_SIZE), WalletListener {
private val handler = Handler(context.mainLooper)
class MoneroHandlerThread(private var onLooperCreated: ((Looper) -> Unit)?) : Thread(null, null, "Monero", THREAD_STACK_SIZE) {
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()
AddressService()
HistoryService()
BlockchainService()
DaemonService()
UTXOService()
thread.start()
}
}
@Synchronized
override fun start() {
super.start()
onRefresh(false)
}
override fun run() {
val prefService = PrefService.instance
val usesTor = ProxyService.instance?.usingProxy == true
val currentNode = prefService.node
val isLocalIp =
currentNode?.address?.startsWith("10.") == true ||
currentNode?.address?.startsWith("192.168.") == true ||
currentNode?.address == "localhost" ||
currentNode?.address == "127.0.0.1"
if (usesTor && !isLocalIp) {
val proxy = ProxyService.instance?.proxy
proxy?.let { WalletManager.instance?.setProxy(it) }
wallet.setProxy(proxy)
}
WalletManager.instance?.setDaemon(currentNode)
wallet.init(0)
currentNode?.trusted?.let { wallet.setTrustedDaemon(it) }
wallet.setListener(this)
wallet.startRefresh()
}
Looper.prepare();
val looper = Looper.myLooper()!!
override fun moneySpent(txId: String?, amount: Long) {}
override fun moneyReceived(txId: String?, amount: Long) {}
override fun unconfirmedMoneyReceived(txId: String?, amount: Long) {}
override fun newBlock(height: Long) {
refresh(false)
BlockchainService.instance?.daemonHeight =
if (wallet.isSynchronized) height else 0 // when 0 it fetches from C++
}
onLooperCreated?.let { callback -> callback(looper) }
onLooperCreated = null
override fun updated() {
refresh(false)
}
override fun refreshed() {
val status = wallet.fullStatus.connectionStatus
val daemonHeight = wallet.getDaemonBlockChainHeight()
val chainHeight = wallet.getBlockChainHeight()
BlockchainService.instance?.daemonHeight = daemonHeight
if (status === ConnectionStatus.ConnectionStatus_Disconnected || status == null) {
tryRestartConnection()
} else {
val heightDiff = daemonHeight - chainHeight
if (heightDiff >= 2) {
tryRestartConnection()
} else {
Log.d("MoneroHandlerThread.kt", "refreshed() Synchronized")
wallet.setSynchronized()
wallet.store()
refresh(true)
}
}
}
private fun tryRestartConnection() {
Log.d("MoneroHandlerThread.kt", "refreshed() Starting connection retry")
onConnectionFail()
wallet.init(0)
wallet.startRefresh()
}
private fun refresh(walletSynced: Boolean) {
wallet.refreshHistory()
if (walletSynced) {
wallet.refreshCoins()
}
onRefresh(walletSynced)
}
@Throws(Exception::class)
fun createTx(
address: String,
amountStr: String,
sendAll: Boolean,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
): PendingTransaction? {
val dests = ArrayList<Pair<String, String>>()
dests.add(Pair(address, amountStr))
return createTx(dests, sendAll, feePriority, selectedUtxos)
}
@Throws(Exception::class)
fun createTx(
dests: List<Pair<String, String>>,
sendAll: Boolean,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
): PendingTransaction? {
var totalAmount: Long = 0
val outputs = ArrayList<TransactionOutput>()
for (dest in dests) {
val amount = getAmountFromString(dest.component2())
totalAmount += amount
outputs.add(TransactionOutput(dest.component1(), amount))
}
val preferredInputs: ArrayList<String>
if (selectedUtxos.isEmpty()) {
// no inputs manually selected, we are sending from home screen most likely, or user somehow broke the app
preferredInputs =
UTXOService.instance?.selectUtxos(totalAmount, sendAll, feePriority) ?: ArrayList()
} else {
preferredInputs = selectedUtxos
checkSelectedAmounts(preferredInputs, totalAmount, sendAll)
}
if (sendAll) {
val dest = dests[0]
val address = dest.component1()
return wallet.createSweepTransaction(address, feePriority, preferredInputs)
}
return wallet.createTransactionMultDest(outputs, feePriority, preferredInputs)
}
@Throws(Exception::class)
private fun checkSelectedAmounts(selectedUtxos: List<String?>, amount: Long, sendAll: Boolean) {
if (!sendAll) {
var amountSelected: Long = 0
val utxos = UTXOService.instance?.getUtxos() ?: emptyList()
for (coinsInfo in utxos) {
if (selectedUtxos.contains(coinsInfo.keyImage)) {
amountSelected += coinsInfo.amount
}
}
if (amountSelected <= amount) {
throw Exception("insufficient wallet balance")
}
}
}
fun sendTx(pendingTx: PendingTransaction): Boolean {
return pendingTx.commit("", true)
}
fun onRefresh(walletSynced: Boolean) {
if (walletSynced) {
UTXOService.instance?.refreshUtxos()
}
HistoryService.instance?.refreshHistory()
BalanceService.instance?.refreshBalance()
BlockchainService.instance?.refreshBlockchain()
AddressService.instance?.refreshAddresses()
}
fun onConnectionFail() {
handler.post {
Toast.makeText(context, R.string.connection_failed, Toast.LENGTH_SHORT).show()
}
Looper.loop()
}
}

View File

@ -4,10 +4,9 @@ import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import net.mynero.wallet.MoneroApplication
import net.mynero.wallet.data.DefaultNodes
import net.mynero.wallet.data.DefaultNode
import net.mynero.wallet.data.Node
import net.mynero.wallet.data.Node.Companion.fromJson
import net.mynero.wallet.data.Node.Companion.fromString
import net.mynero.wallet.util.Constants
import org.json.JSONException
import org.json.JSONObject
@ -27,40 +26,6 @@ class PrefService(application: MoneroApplication) {
return preferences.edit()
}
val node: Node?
get() {
val usesProxy = ProxyService.instance?.usingProxy == true
var defaultNode = DefaultNodes.MONERUJO
if (usesProxy) {
val proxyPort = ProxyService.instance?.proxyPort
if (proxyPort?.isNotEmpty() == true) {
val port = proxyPort.toInt()
if (port == 9050) {
defaultNode = DefaultNodes.MONERUJO_ONION
}
}
}
val nodeString = getString(Constants.PREF_NODE_2, defaultNode.nodeString)
return try {
val nodeJson = nodeString?.let { JSONObject(it) }
fromJson(nodeJson)
} catch (e: JSONException) {
// stored node is not json format, upgrade if possible
nodeString?.let { upgradeOldNode(it) }
}
}
private fun upgradeOldNode(nodeString: String): Node? {
if (nodeString.isNotEmpty()) {
val node = fromString(nodeString)
if (node != null) {
edit().putString(Constants.PREF_NODE_2, node.toJson().toString())?.apply()
return node
}
}
return null
}
fun getString(key: String?, defaultValue: String): String? {
val value = preferences.getString(key, "")
if (value?.isEmpty() == true && defaultValue.isNotEmpty()) {
@ -80,18 +45,6 @@ class PrefService(application: MoneroApplication) {
return value
}
fun saveProxy(address: String, port: String): String? {
if (address.isEmpty() || port.isEmpty()) {
deleteProxy()
return null
}
val proxyAddress = "$address:$port"
if (proxyAddress == ":") return null
Log.d("PrefService", "Setting proxy. proxyAddress=$proxyAddress")
edit().putString(Constants.PREF_PROXY, proxyAddress)?.apply()
return proxyAddress
}
fun deleteProxy() {
Log.d("PrefService", "Deleting proxy...")
edit().putString(Constants.PREF_PROXY, "")?.apply()

View File

@ -1,14 +1,9 @@
package net.mynero.wallet.service
import android.app.Application
import android.util.Log
import android.util.Patterns
import net.mynero.wallet.livedata.SingleLiveEvent
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Constants
class ProxyService(application: Application) {
val proxyChangeEvents: SingleLiveEvent<String> = SingleLiveEvent()
var samouraiTorManager: SamouraiTorManager? = null
var usingProxy: Boolean = false
get() = PrefService.instance.getBoolean(Constants.PREF_USES_PROXY, false)
@ -32,29 +27,6 @@ class ProxyService(application: Application) {
}
}
fun updateProxy(proxyAddress: String, proxyPort: String) {
var finalProxyAddress = proxyAddress
var finalProxyPort = proxyPort
val curretNode = PrefService.instance.node
val isNodeLocalIp =
curretNode?.address?.startsWith("10.") == true || curretNode?.address?.startsWith("192.168.") == true || curretNode?.address == "localhost" || curretNode?.address == "127.0.0.1"
curretNode?.trusted?.let { WalletManager.instance?.wallet?.setTrustedDaemon(it) }
if (!usingProxy || isNodeLocalIp) {
// User is not using proxy, or is using local node currently, so we will disable proxy here.
proxyChangeEvents.postValue("")
Log.d("ProxyService", "Not using proxy...")
return
}
// We are using proxy at this point, but user set them to empty. We will fallback to Tor defaults here.
if (proxyAddress.isEmpty()) finalProxyAddress = "127.0.0.1"
if (proxyPort.isEmpty()) finalProxyPort = "9050"
val validIpAddress = Patterns.IP_ADDRESS.matcher(finalProxyAddress).matches()
if (validIpAddress) {
val proxy = PrefService.instance.saveProxy(finalProxyAddress, finalProxyPort)
proxy?.let { proxyChangeEvents.postValue(it) }
}
}
private fun hasProxySet(): Boolean {
val proxyString = proxy
return proxyString?.contains(":") == true

View File

@ -1,38 +0,0 @@
package net.mynero.wallet.service
import net.mynero.wallet.model.PendingTransaction
class TxService(val thread: MoneroHandlerThread) {
init {
instance = this
}
@Throws(Exception::class)
fun createTx(
address: String,
amount: String,
sendAll: Boolean,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
): PendingTransaction? {
return thread.createTx(address, amount, sendAll, feePriority, selectedUtxos)
}
@Throws(Exception::class)
fun createTx(
dests: List<Pair<String, String>>,
sendAll: Boolean,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
): PendingTransaction? {
return thread.createTx(dests, sendAll, feePriority, selectedUtxos)
}
fun sendTx(pendingTransaction: PendingTransaction): Boolean {
return thread.sendTx(pendingTransaction)
}
companion object {
var instance: TxService? = null
}
}

View File

@ -1,140 +0,0 @@
package net.mynero.wallet.service
import android.util.Pair
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.CoinsInfo
import net.mynero.wallet.model.PendingTransaction
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Constants
import org.json.JSONArray
import org.json.JSONException
import java.util.Collections
class UTXOService {
private val _utxos = MutableLiveData<List<CoinsInfo>>()
var utxos: LiveData<List<CoinsInfo>> = _utxos
private var internalCachedUtxos: List<CoinsInfo> = ArrayList()
private var frozenCoins = ArrayList<String?>()
val utxosInternal: List<CoinsInfo>
get() {
return WalletManager.instance?.wallet?.coins?.all ?: emptyList()
}
init {
instance = this
try {
loadFrozenCoins()
} catch (e: JSONException) {
throw RuntimeException(e)
}
}
fun refreshUtxos() {
val coinsInfos: List<CoinsInfo> = this.utxosInternal
_utxos.postValue(coinsInfos)
internalCachedUtxos = coinsInfos
}
fun getUtxos(): List<CoinsInfo> {
return Collections.unmodifiableList(internalCachedUtxos)
}
fun toggleFrozen(selectedCoins: HashMap<String?, CoinsInfo>) {
val frozenCoinsCopy = ArrayList(frozenCoins)
for (coin in selectedCoins.values) {
if (frozenCoinsCopy.contains(coin.pubKey)) {
frozenCoinsCopy.remove(coin.pubKey)
} else {
frozenCoinsCopy.add(coin.pubKey)
}
}
frozenCoins = frozenCoinsCopy
saveFrozenCoins()
refreshUtxos()
BalanceService.instance?.refreshBalance()
}
fun isCoinFrozen(coinsInfo: CoinsInfo): Boolean {
return frozenCoins.contains(coinsInfo.pubKey)
}
@Throws(JSONException::class)
private fun loadFrozenCoins() {
val prefService = PrefService.instance
val frozenCoinsArrayString = prefService?.getString(Constants.PREF_FROZEN_COINS, "[]")
val frozenCoinsArray = JSONArray(frozenCoinsArrayString)
for (i in 0 until frozenCoinsArray.length()) {
val pubKey = frozenCoinsArray.getString(i)
frozenCoins.add(pubKey)
}
refreshUtxos()
}
private fun saveFrozenCoins() {
val prefService = PrefService.instance
val jsonArray = JSONArray()
val frozenCoinsCopy = ArrayList(frozenCoins)
for (pubKey in frozenCoinsCopy) {
jsonArray.put(pubKey)
}
prefService?.edit()?.putString(Constants.PREF_FROZEN_COINS, jsonArray.toString())?.apply()
}
@Throws(Exception::class)
fun selectUtxos(
amount: Long,
sendAll: Boolean,
feePriority: PendingTransaction.Priority
): ArrayList<String> {
val basicFeeEstimate = calculateBasicFee(amount, feePriority) ?: return arrayListOf()
val amountWithBasicFee = amount + basicFeeEstimate
val selectedUtxos = ArrayList<String>()
val seenTxs = ArrayList<String>()
val utxos: List<CoinsInfo> = ArrayList(getUtxos())
var amountSelected: Long = 0
val sortedUtxos = utxos.sorted()
//loop through each utxo
for (coinsInfo in sortedUtxos) {
if (!coinsInfo.isSpent && coinsInfo.isUnlocked && !coinsInfo.isFrozen && !frozenCoins.contains(
coinsInfo.pubKey
)
) { //filter out spent, locked, and frozen outputs
if (sendAll) {
// if send all, add all utxos and set amount to send all
coinsInfo.keyImage?.let { selectedUtxos.add(it) }
amountSelected = Wallet.SWEEP_ALL
} else {
//if amount selected is still less than amount needed, and the utxos tx hash hasn't already been seen, add utxo
if (amountSelected <= amountWithBasicFee && !seenTxs.contains(coinsInfo.hash)) {
coinsInfo.keyImage?.let { selectedUtxos.add(it) }
// we don't want to spend multiple utxos from the same transaction, so we prevent that from happening here.
coinsInfo.hash?.let { seenTxs.add(it) }
amountSelected += coinsInfo.amount
}
}
}
}
if (amountSelected < amountWithBasicFee && !sendAll) {
throw Exception("insufficient wallet balance")
}
return selectedUtxos
}
private fun calculateBasicFee(amount: Long, feePriority: PendingTransaction.Priority): Long? {
val destinations = ArrayList<Pair<String, Long>>()
destinations.add(
Pair(
Constants.DONATE_ADDRESS,
amount
)
)
// destination string doesn't actually matter here, so i'm using the donation address. amount also technically doesn't matter
return WalletManager.instance?.wallet?.estimateTransactionFee(destinations, feePriority)
}
companion object {
var instance: UTXOService? = null
}
}

View File

@ -0,0 +1,667 @@
package net.mynero.wallet.service.wallet
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.util.Log
import androidx.lifecycle.AtomicReference
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import net.mynero.wallet.model.Balance
import net.mynero.wallet.model.CoinsInfo
import net.mynero.wallet.model.PendingTransaction
import net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.WalletListener
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.MoneroHandlerThread
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.TransactionDestination
import timber.log.Timber
import java.io.File
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.atomic.AtomicLong
private const val EXTRA_WALLET_NAME = "wallet_name"
private const val EXTRA_WALLET_PASSWORD = "wallet_password"
private const val EXTRA_DAEMON_ADDRESS = "daemon_address"
private const val EXTRA_DAEMON_USERNAME = "daemon_username"
private const val EXTRA_DAEMON_PASSWORD = "daemon_password"
private const val EXTRA_DAEMON_TRUSTED = "daemon_trusted"
private const val EXTRA_SUBADDRESS_LABEL = "subaddress_label"
private const val EXTRA_SUBADDRESS_INDEX = "subaddress_index"
private const val EXTRA_TRANSACTION_DESTINATION = "transaction_destination"
private const val EXTRA_TRANSACTION_DESTINATIONS = "transaction_destinations"
private const val EXTRA_TRANSACTION_PRIORITY = "transaction_priority"
private const val EXTRA_TRANSACTION_SELECTED_UTXOS = "transaction_selected_utxos"
private const val EXTRA_PENDING_TRANSACTION = "pending_transaction"
private const val EXTRA_PROXY = "proxy"
private const val EXTRA_ENOTE = "enote"
private const val EXTRA_ENOTES = "enotes"
class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
private var daemonHeight: AtomicLong = AtomicLong(0)
// height of the wallet when it was opened
private var walletBeginSyncHeight: AtomicLong = AtomicLong(0)
private var proxyRef: AtomicReference<String> = AtomicReference("")
private var walletRef: AtomicReference<Wallet?> = AtomicReference(null)
private val observers = mutableSetOf<WalletServiceObserver>()
inner class WalletServiceBinder : Binder() {
val service: WalletService = this@WalletService
}
inner class WalletHandler(looper: Looper) : Handler(looper) {
override fun handleMessage(message: Message) {
val commandType = message.arg1
val data = message.data
when (WalletServiceCommand.fromInt(commandType)) {
WalletServiceCommand.UNKNOWN, null -> {
Timber.e("WalletHandler received UNKNOWN(%d) command", commandType)
}
WalletServiceCommand.OPEN_WALLET -> {
val walletName = data.getString(EXTRA_WALLET_NAME)!!
val walletFile = File(applicationInfo.dataDir, walletName)
val walletPassword = data.getString(EXTRA_WALLET_PASSWORD)!!
val daemonAddress = data.getString(EXTRA_DAEMON_ADDRESS)!!
val daemonUsername = data.getString(EXTRA_DAEMON_USERNAME)!!
val daemonPassword = data.getString(EXTRA_DAEMON_PASSWORD)!!
val daemonTrusted = data.getBoolean(EXTRA_DAEMON_TRUSTED, false)
val proxy = data.getString(EXTRA_PROXY)!!
handleOpenWallet(walletFile, walletPassword, daemonAddress, daemonUsername, daemonPassword, daemonTrusted, proxy)
}
WalletServiceCommand.SET_DAEMON_ADDRESS -> {
val daemonAddress = data.getString(EXTRA_DAEMON_ADDRESS)!!
val daemonUsername = data.getString(EXTRA_DAEMON_USERNAME)!!
val daemonPassword = data.getString(EXTRA_DAEMON_PASSWORD)!!
val daemonTrusted = data.getBoolean(EXTRA_DAEMON_TRUSTED, false)
val proxy = data.getString(EXTRA_PROXY)!!
setDaemonAddress(daemonAddress, daemonUsername, daemonPassword, daemonTrusted, proxy)
}
WalletServiceCommand.FETCH_BLOCKCHAIN_HEIGHT -> {
handleFetchDaemonHeight()
}
WalletServiceCommand.REFRESH_WALLET_TRANSACTION_HISTORY -> {
handleRefreshTransactionHistory()
}
WalletServiceCommand.GENERATE_NEW_SUBADDRESS -> {
val subaddressLabel = data.getString(EXTRA_SUBADDRESS_LABEL)!!
handleGenerateNewSubaddress(subaddressLabel)
}
WalletServiceCommand.SET_SUBADDRESS_LABEL -> {
val subaddressIndex = data.getInt(EXTRA_SUBADDRESS_INDEX)
val subaddressLabel = data.getString(EXTRA_SUBADDRESS_LABEL)!!
handleSetSubaddressLabel(subaddressIndex, subaddressLabel)
}
WalletServiceCommand.CREATE_SWEEP_TX -> {
val transactionDestination = data.getString(EXTRA_TRANSACTION_DESTINATION)!!
val transactionPriority = PendingTransaction.Priority.fromInteger(message.data.getInt(
EXTRA_TRANSACTION_PRIORITY
))!!
val selectedUtxos = message.data.getStringArrayList(
EXTRA_TRANSACTION_SELECTED_UTXOS
)!!
handleCreateSweepTransaction(transactionDestination, transactionPriority, selectedUtxos)
}
WalletServiceCommand.CREATE_TX -> {
val transactionDestinations = data.getParcelableArrayList<TransactionDestination>(
EXTRA_TRANSACTION_DESTINATIONS
)!!
val transactionPriority = PendingTransaction.Priority.fromInteger(message.data.getInt(
EXTRA_TRANSACTION_PRIORITY
))!!
val selectedUtxos = message.data.getStringArrayList(
EXTRA_TRANSACTION_SELECTED_UTXOS
)!!
handleCreateTransaction(transactionDestinations, transactionPriority, selectedUtxos)
}
WalletServiceCommand.SEND_TX -> {
val pendingTransactionHandle = data.getLong(EXTRA_PENDING_TRANSACTION)
val pendingTransaction = PendingTransaction(pendingTransactionHandle)
handleSendPendingTransaction(pendingTransaction)
}
WalletServiceCommand.SET_PROXY -> {
val proxy = data.getString(EXTRA_PROXY)!!
handleSetProxy(proxy)
}
WalletServiceCommand.REFRESH_ENOTES -> {
handleRefreshEnotes()
}
WalletServiceCommand.FREEZE_ENOTES -> {
val enotes = data.getStringArrayList(EXTRA_ENOTES)!!
handleFreezeEnotes(enotes)
}
WalletServiceCommand.THAW_ENOTES -> {
val enotes = data.getStringArrayList(EXTRA_ENOTES)!!
handleThawEnotes(enotes)
}
}
}
private fun handleOpenWallet(
walletFile: File,
walletPassword: String,
daemonAddress: String,
daemonUsername: String,
daemonPassword: String,
daemonTrusted: Boolean,
proxy: String
) {
WalletManager.instance.setProxy(proxy)
WalletManager.instance.setDaemonAddressJ(daemonAddress)
val wallet = WalletManager.instance.openWallet(walletFile.absolutePath, walletPassword)
Timber.i("Initializing wallet with daemon address = $daemonAddress, daemon username = $daemonUsername, daemon password = ${"*".repeat(daemonPassword.length)}, proxy = $proxy")
wallet.initJ(daemonAddress, 0, daemonUsername, daemonPassword, proxy)
wallet.setTrustedDaemon(daemonTrusted)
this@WalletService.walletBeginSyncHeight.set(wallet.getBlockChainHeightJ())
this@WalletService.walletRef.set(wallet)
forEachObserver {
it.onWalletOpened(wallet)
}
handleRefreshTransactionHistory()
handleRefreshEnotes()
wallet.setListener(this@WalletService)
wallet.startRefresh()
handleFetchDaemonHeight()
}
private fun setDaemonAddress(
daemonAddress: String,
daemonUsername: String,
daemonPassword: String,
daemonTrusted: Boolean,
proxy: String
) {
WalletManager.instance.setProxy(proxy)
WalletManager.instance.setDaemonAddressJ(daemonAddress)
walletRef.get()?.let { wallet ->
wallet.initJ(daemonAddress, 0, daemonUsername, daemonPassword, proxy)
wallet.setTrustedDaemon(daemonTrusted)
wallet.setListener(this@WalletService)
wallet.startRefresh()
forEachObserver {
it.onWalletOpened(wallet)
}
handleRefreshTransactionHistory()
handleRefreshEnotes()
}
handleFetchDaemonHeight()
}
private fun handleFetchDaemonHeight() {
val newHeight = WalletManager.instance.getBlockchainHeight()
if (newHeight > 0) {
daemonHeight.set(newHeight)
}
forEachObserver {
it.onBlockchainHeightFetched(newHeight)
}
}
private fun handleRefreshTransactionHistory() {
val history = walletRef.get()!!.getTransactionHistory().refreshJ()
forEachObserver {
it.onWalletHistoryRefreshed(history)
}
}
private fun handleGenerateNewSubaddress(label: String) {
walletRef.get()!!.addSubaddress(0, label)
walletRef.get()!!.store()
forEachObserver {
it.onSubaddressesUpdated()
}
}
private fun handleSetSubaddressLabel(index: Int, label: String) {
walletRef.get()!!.setSubaddressLabel(index, label)
walletRef.get()!!.store()
forEachObserver {
it.onSubaddressesUpdated()
}
}
private fun handleCreateSweepTransaction(
destination: String,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
) {
val preferredInputs = if (selectedUtxos.isEmpty()) {
selectUtxos(Long.MAX_VALUE, true, feePriority)
} else {
selectedUtxos
}
val pendingTransaction = walletRef.get()!!.createSweepTransaction(destination, feePriority, preferredInputs)
forEachObserver {
it.onTransactionCreated(pendingTransaction)
}
}
private fun handleCreateTransaction(
destinations: List<TransactionDestination>,
feePriority: PendingTransaction.Priority,
selectedUtxos: ArrayList<String>
) {
val totalAmount = destinations.sumOf { it.amount }
val preferredInputs = if (selectedUtxos.isEmpty()) {
selectUtxos(totalAmount, false, feePriority)
} else {
selectedUtxos
}
val pendingTransaction = walletRef.get()!!.createTransactionMultDest(destinations, feePriority, preferredInputs)
forEachObserver {
it.onTransactionCreated(pendingTransaction)
}
}
private fun handleSendPendingTransaction(pendingTransaction: PendingTransaction) {
val success = pendingTransaction.commit("", false)
forEachObserver {
it.onTransactionSent(pendingTransaction, success)
}
}
@Throws(Exception::class)
fun selectUtxos(
amount: Long,
sendAll: Boolean,
feePriority: PendingTransaction.Priority
): ArrayList<String> {
// this is bugged, throws insufficient balance even when there is enough money (prob coins is not updated?)
val basicFeeEstimate = calculateBasicFee(amount, feePriority) ?: return arrayListOf()
val amountWithBasicFee = amount + basicFeeEstimate
val selectedUtxos = ArrayList<String>()
val seenTxs = ArrayList<String>()
val utxos: List<CoinsInfo> = ArrayList(walletRef.get()!!.coins?.all!!)
var amountSelected: Long = 0
val sortedUtxos = utxos.sorted()
//loop through each utxo
Log.e("WS", "sortedUtxos sz = ${sortedUtxos.size}")
for (coinsInfo in sortedUtxos) {
Log.e("WS", "coinsInfo = $coinsInfo")
if (!coinsInfo.isSpent && coinsInfo.isUnlocked && !coinsInfo.isFrozen
) { //filter out spent, locked, and frozen outputs
if (sendAll) {
// if send all, add all utxos and set amount to send all
coinsInfo.keyImage?.let { selectedUtxos.add(it) }
amountSelected = Wallet.SWEEP_ALL
} else {
//if amount selected is still less than amount needed, and the utxos tx hash hasn't already been seen, add utxo
if (amountSelected <= amountWithBasicFee && !seenTxs.contains(coinsInfo.hash)) {
coinsInfo.keyImage?.let { selectedUtxos.add(it) }
// we don't want to spend multiple utxos from the same transaction, so we prevent that from happening here.
coinsInfo.hash?.let { seenTxs.add(it) }
amountSelected += coinsInfo.amount
}
}
}
}
if (amountSelected < amountWithBasicFee && !sendAll) {
Log.e("WS", "amountSelected = $amountSelected, amountWithBasicFee = $amountWithBasicFee")
throw Exception("insufficient wallet balance")
}
return selectedUtxos
}
private fun calculateBasicFee(amount: Long, feePriority: PendingTransaction.Priority): Long? {
val destinations = ArrayList<android.util.Pair<String, Long>>()
destinations.add(
android.util.Pair(
Constants.DONATE_ADDRESS,
amount
)
)
// destination string doesn't actually matter here, so i'm using the donation address. amount also technically doesn't matter
return walletRef.get()?.estimateTransactionFee(destinations, feePriority)
}
private fun handleSetProxy(value: String) {
val success = WalletManager.instance.setProxy(value)
// we assume that if setting proxy for daemon was successful,
// then setting proxy for wallet must have been successful as well
walletRef.get()?.setProxy(value)
if (success) {
this@WalletService.proxyRef.set(value)
}
forEachObserver {
it.onProxyUpdated(value, success)
}
}
private fun calculateBalanceAndNotifyAboutRefreshedEnotes(enotes: List<CoinsInfo>) {
var total = 0L
var unlocked = 0L
var frozen = 0L
var pending = 0L
enotes.filter { !it.isSpent }.forEach { enote ->
total += enote.amount
if (enote.isFrozen) {
frozen += enote.amount
} else if (enote.isUnlocked) {
unlocked += enote.amount
} else {
pending += enote.amount
}
}
forEachObserver {
it.onEnotesRefreshed(enotes, Balance(total, unlocked, frozen, pending))
}
}
private fun handleRefreshEnotes() {
val refreshedEnotes = walletRef.get()!!.coins!!.refreshJ()
calculateBalanceAndNotifyAboutRefreshedEnotes(refreshedEnotes)
}
private fun handleFreezeEnotes(enotes: List<String>) {
enotes.forEach {
walletRef.get()!!.coins!!.setFrozen(it, true)
}
val refreshedEnotes = walletRef.get()!!.coins!!.refreshJ()
calculateBalanceAndNotifyAboutRefreshedEnotes(refreshedEnotes)
}
private fun handleThawEnotes(enotes: List<String>) {
enotes.forEach {
walletRef.get()!!.coins!!.setFrozen(it, false)
}
val refreshedEnotes = walletRef.get()!!.coins!!.refreshJ()
calculateBalanceAndNotifyAboutRefreshedEnotes(refreshedEnotes)
}
}
private val mBinder: IBinder = WalletServiceBinder()
private lateinit var handler: WalletHandler
override fun onBind(intent: Intent?): IBinder {
return mBinder
}
override fun onCreate() {
// ArrayBlockingQueue is used as a oneshot channel to receive a Looper from MoneroHandlerThread
val oneShotQueue = ArrayBlockingQueue<Looper>(1)
val thread = MoneroHandlerThread { looper ->
oneShotQueue.add(looper)
}
thread.start()
val looper = oneShotQueue.take()
handler = WalletHandler(looper)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null) {
return START_STICKY
} else {
stopSelf()
return START_NOT_STICKY
}
}
override fun onDestroy(owner: LifecycleOwner) {
synchronized(observers) {
observers.remove(owner)
}
}
private fun forEachObserver(f: (WalletServiceObserver) -> Unit) {
synchronized(observers) {
for (observer in observers) {
f(observer)
}
}
}
override fun moneySpent(txId: String?, amount: Long) {
Timber.d("Money spent callback")
refreshTransactionsHistory()
}
override fun moneyReceived(txId: String?, amount: Long) {
Timber.d("Money received callback")
refreshTransactionsHistory()
}
override fun unconfirmedMoneyReceived(txId: String?, amount: Long) {
Timber.d("Unconfirmed money received callback")
refreshTransactionsHistory()
}
override fun newBlock(height: Long) {
Timber.d("New block callback at height $height")
daemonHeight.updateAndGet { if (it > 0 && it < height) height else it }
forEachObserver {
it.onNewBlock(height)
}
val wallet = getWalletOrThrow()
refreshTransactionsHistory()
wallet.coins!!.refresh()
if (wallet.isSynchronized) {
wallet.store()
}
}
override fun updated() {
Timber.d("Updated callback")
forEachObserver {
it.onUpdated()
}
}
override fun refreshed() {
Timber.d("Refreshed callback")
forEachObserver {
it.onRefreshed()
}
if (daemonHeight.get() <= 1) {
Timber.d("Fetching blockchain height")
fetchBlockchainHeight()
}
val wallet = getWalletOrThrow()
val wasSynchronized = wallet.isSynchronized
if (!wasSynchronized) {
Timber.d("Storing wallet after synchronization was completed")
wallet.isSynchronized = true
wallet.store()
}
}
fun addObserver(observer: WalletServiceObserver) {
observer.lifecycle.addObserver(this)
synchronized(observers) {
observers.add(observer)
}
}
fun getWalletBeginSyncHeight(): Long {
return walletBeginSyncHeight.get()
}
fun getDaemonHeight(): Long {
return daemonHeight.get()
}
fun getProxy(): String {
return proxyRef.get()
}
fun getWallet(): Wallet? {
return walletRef.get()
}
fun getWalletOrThrow(): Wallet {
val wallet = this.walletRef.get()
if (wallet != null) {
return wallet
} else {
throw IllegalStateException("Get wallet called on null wallet")
}
}
fun openWallet(
walletName: String,
walletPassword: String,
daemonAddress: String,
daemonUsername: String,
daemonPassword: String,
daemonTrusted: Boolean,
proxy: String
) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.OPEN_WALLET.value()
message.data.putString(EXTRA_WALLET_NAME, walletName)
message.data.putString(EXTRA_WALLET_PASSWORD, walletPassword)
message.data.putString(EXTRA_DAEMON_ADDRESS, daemonAddress)
message.data.putString(EXTRA_DAEMON_USERNAME, daemonUsername)
message.data.putString(EXTRA_DAEMON_PASSWORD, daemonPassword)
message.data.putBoolean(EXTRA_DAEMON_TRUSTED, daemonTrusted)
message.data.putString(EXTRA_PROXY, proxy)
handler.sendMessage(message)
}
fun setDaemonAddress(
daemonAddress: String,
daemonUsername: String,
daemonPassword: String,
daemonTrusted: Boolean,
proxy: String
) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.SET_DAEMON_ADDRESS.value()
message.data.putString(EXTRA_DAEMON_ADDRESS, daemonAddress)
message.data.putString(EXTRA_DAEMON_USERNAME, daemonUsername)
message.data.putString(EXTRA_DAEMON_PASSWORD, daemonPassword)
message.data.putBoolean(EXTRA_DAEMON_TRUSTED, daemonTrusted)
message.data.putString(EXTRA_PROXY, proxy)
handler.sendMessage(message)
}
private fun fetchBlockchainHeight() {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.FETCH_BLOCKCHAIN_HEIGHT.value()
handler.sendMessage(message)
}
fun refreshTransactionsHistory() {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.REFRESH_WALLET_TRANSACTION_HISTORY.value()
handler.sendMessage(message)
}
fun generateNewSubaddress(label: String) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.GENERATE_NEW_SUBADDRESS.value()
message.data.putString(EXTRA_SUBADDRESS_LABEL, label)
handler.sendMessage(message)
}
fun setSubaddressLabel(index: Int, label: String) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.SET_SUBADDRESS_LABEL.value()
message.data.putInt(EXTRA_SUBADDRESS_INDEX, index)
message.data.putString(EXTRA_SUBADDRESS_LABEL, label)
handler.sendMessage(message)
}
fun sendTransaction(pendingTransaction: PendingTransaction) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.SEND_TX.value()
message.data.putLong(EXTRA_PENDING_TRANSACTION, pendingTransaction.handle)
handler.sendMessage(message)
}
fun createSweepTransaction(destination: String, priority: PendingTransaction.Priority, selectedUtxos: ArrayList<String>) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.CREATE_SWEEP_TX.value()
message.data.putString(EXTRA_TRANSACTION_DESTINATION, destination)
message.data.putInt(EXTRA_TRANSACTION_PRIORITY, priority.value)
message.data.putStringArrayList(EXTRA_TRANSACTION_SELECTED_UTXOS, selectedUtxos)
handler.sendMessage(message)
}
fun createTransaction(destinations: List<TransactionDestination>, priority: PendingTransaction.Priority, selectedUtxos: ArrayList<String>) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.CREATE_TX.value()
message.data.putParcelableArrayList(EXTRA_TRANSACTION_DESTINATIONS, ArrayList(destinations))
message.data.putInt(EXTRA_TRANSACTION_PRIORITY, priority.value)
message.data.putStringArrayList(EXTRA_TRANSACTION_SELECTED_UTXOS, selectedUtxos)
handler.sendMessage(message)
}
fun setProxy(value: String) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.SET_PROXY.value()
message.data.putString(EXTRA_PROXY, value)
handler.sendMessage(message)
}
fun refreshEnotes() {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.REFRESH_ENOTES.value()
handler.sendMessage(message)
}
fun freezeEnote(enotes: List<String>) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.FREEZE_ENOTES.value()
message.data.putStringArrayList(EXTRA_ENOTES, ArrayList(enotes))
handler.sendMessage(message)
}
fun thawEnote(enotes: List<String>) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.THAW_ENOTES.value()
message.data.putStringArrayList(EXTRA_ENOTES, ArrayList(enotes))
handler.sendMessage(message)
}
}
interface WalletServiceObserver : LifecycleOwner {
fun onWalletOpened(wallet: Wallet) {}
fun onWalletHistoryRefreshed(transactions: List<TransactionInfo>) {}
fun onBlockchainHeightFetched(height: Long) {}
fun onEnotesRefreshed(enotes: List<CoinsInfo>, balance: Balance) {}
fun onSubaddressesUpdated() {}
fun onTransactionCreated(pendingTransaction: PendingTransaction) {}
fun onTransactionSent(pendingTransaction: PendingTransaction, success: Boolean) {}
fun onProxyUpdated(proxy: String, success: Boolean) {}
fun onUpdated() {}
fun onRefreshed() {}
fun onNewBlock(height: Long) {}
}
enum class WalletServiceCommand {
UNKNOWN,
OPEN_WALLET,
SET_DAEMON_ADDRESS,
FETCH_BLOCKCHAIN_HEIGHT,
REFRESH_WALLET_TRANSACTION_HISTORY,
GENERATE_NEW_SUBADDRESS,
SET_SUBADDRESS_LABEL,
CREATE_SWEEP_TX,
CREATE_TX,
SEND_TX,
SET_PROXY,
REFRESH_ENOTES,
FREEZE_ENOTES,
THAW_ENOTES;
fun value() = ordinal
companion object {
fun fromInt(value: Int): WalletServiceCommand? = entries.getOrNull(value)
}
}

View File

@ -1,18 +1,22 @@
package net.mynero.wallet.util
object Constants {
const val WALLET_NAME = "xmr_wallet"
// TODO: change this when new wallet name is decided
// Also change native filenames
const val PREFERENCES_KEY = "mysu"
const val DEFAULT_WALLET_NAME = "xmr_wallet"
const val MNEMONIC_LANGUAGE = "English"
const val PREF_USES_PASSWORD = "pref_uses_password"
const val PREF_USES_PROXY = "pref_uses_tor"
const val PREF_USE_PROXY = "pref_use_proxy"
const val PREF_PROXY = "pref_proxy"
const val PREF_NODE_2 = "pref_node_2"
// _3 can be removed after preferences key is changed
const val PREF_NODE_3 = "pref_node_3"
const val PREF_CUSTOM_NODES = "pref_custom_nodes"
const val PREF_NODES = "pref_custom_nodes"
const val PREF_USES_OFFSET = "pref_uses_offset"
const val PREF_STREET_MODE = "pref_street_mode"
const val PREF_MONEROCHAN = "pref_monerochan"
const val PREF_ALLOW_FEE_OVERRIDE = "pref_allow_fee_override"
const val PREF_FROZEN_COINS = "pref_frozen_coins"
const val PREF_USE_BUNDLED_TOR = "pref_use_bundled_tor"
const val URI_PREFIX = "monero:"
@ -27,9 +31,7 @@ object Constants {
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
const val EXTRA_SEND_ENOTES = "send_enotes"
// Donation address is also specified in strings.xml, it is used as a tooltip in address fields
const val DONATE_ADDRESS =

View File

@ -22,6 +22,7 @@ import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@ -37,6 +38,7 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.ContextCompat
import net.mynero.wallet.R
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.io.InputStreamReader

View File

@ -1,54 +0,0 @@
/*
* Copyright (c) 2017 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.mynero.wallet.util
import net.mynero.wallet.service.MoneroHandlerThread
import java.util.concurrent.BlockingQueue
import java.util.concurrent.Executor
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
object MoneroThreadPoolExecutor {
var MONERO_THREAD_POOL_EXECUTOR: Executor? = null
private val CPU_COUNT = Runtime.getRuntime().availableProcessors()
private val CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4))
private val MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1
private const val KEEP_ALIVE_SECONDS = 30
private val sThreadFactory: ThreadFactory = object : ThreadFactory {
private val mCount = AtomicInteger(1)
override fun newThread(r: Runnable): Thread {
return Thread(
null,
r,
"MoneroTask #" + mCount.getAndIncrement(),
MoneroHandlerThread.THREAD_STACK_SIZE
)
}
}
private val sPoolWorkQueue: BlockingQueue<Runnable> = LinkedBlockingQueue(128)
init {
val threadPoolExecutor = ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS.toLong(), TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory
)
threadPoolExecutor.allowCoreThreadTimeOut(true)
MONERO_THREAD_POOL_EXECUTOR = threadPoolExecutor
}
}

View File

@ -0,0 +1,147 @@
package net.mynero.wallet.util
import android.content.Context
import android.content.SharedPreferences
import net.mynero.wallet.data.Node
import org.json.JSONArray
import org.json.JSONObject
import timber.log.Timber
object PreferenceUtils {
fun getSharedPreferences(context: Context): SharedPreferences = context.getSharedPreferences(
Constants.PREFERENCES_KEY,
Context.MODE_PRIVATE
)
fun getBoolean(context: Context, key: String, default: Boolean): Boolean = getSharedPreferences(context).getBoolean(key, default)
fun setBoolean(context: Context, key: String, value: Boolean) = getSharedPreferences(context).edit().putBoolean(key, value).apply()
fun getString(context: Context, key: String, default: String? = null): String? = getSharedPreferences(context).getString(key, default)
fun setString(context: Context, key: String, value: String) = getSharedPreferences(context).edit().putString(key, value).apply()
fun isUseProxy(context: Context): Boolean = getBoolean(context, Constants.PREF_USE_PROXY, false)
fun setUseProxy(context: Context, value: Boolean) = setBoolean(context, Constants.PREF_USE_PROXY, value)
fun isStreetMode(context: Context): Boolean = getBoolean(context, Constants.PREF_STREET_MODE, false)
fun setStreetMode(context: Context, value: Boolean) = setBoolean(context, Constants.PREF_STREET_MODE, value)
fun isAllowFeeOverride(context: Context): Boolean = getBoolean(context, Constants.PREF_ALLOW_FEE_OVERRIDE, false)
fun setAllowFeeOverride(context: Context, value: Boolean) = setBoolean(context, Constants.PREF_ALLOW_FEE_OVERRIDE, value)
fun getProxy(context: Context): String? = getString(context, Constants.PREF_PROXY)
fun setProxy(context: Context, value: String) = setString(context, Constants.PREF_PROXY, value)
fun getProxyIfEnabled(context: Context): String? = if (isUseProxy(context)) getProxy(context) else null
private fun getNodes(context: Context): Result<List<Node>> = runCatching {
val jsonArrayString = getString(context, Constants.PREF_NODES, "[]")
val jsonArray = JSONArray(jsonArrayString)
val nodes = mutableListOf<Node>()
for (i in 0 until jsonArray.length()) {
val nodeJson = jsonArray.getJSONObject(i)
Node.fromJson(nodeJson).fold(
{
nodes.add(it)
},
{
Timber.e(it, "Failed to parse node from string $nodeJson")
}
)
}
return Result.success(nodes)
}
fun setNodes(context: Context, nodes: List<Node>): Result<Unit> = kotlin.runCatching {
val jsonNodes = nodes.mapNotNull { node ->
node.toJson().fold(
{
it
},
{
Timber.e(it, "Failed to convert node $node to json")
null
}
)
}
val jsonArray = JSONArray()
jsonNodes.forEach {
jsonArray.put(it)
}
val jsonArrayString = jsonArray.toString()
setString(context, Constants.PREF_NODES, jsonArrayString)
}
private fun setDefaultNodesAndLogError(context: Context, defaultNodes: List<Node>) {
setNodes(context, defaultNodes).exceptionOrNull()?.let { setNodesException ->
Timber.e(setNodesException, "Failed to set default nodes")
}
}
fun getOrSetDefaultNodes(context: Context, defaultNodes: List<Node>): List<Node> {
return getNodes(context).fold(
{ nodes ->
if (nodes.isEmpty()) {
Timber.i("No nodes found, saving default nodes")
setDefaultNodesAndLogError(context, defaultNodes)
return@fold defaultNodes
} else {
return@fold nodes
}
},
{ getNodesException ->
Timber.e(getNodesException, "Failed to get nodes, saving default nodes")
setDefaultNodesAndLogError(context, defaultNodes)
return@fold defaultNodes
}
)
}
fun setNode(context: Context, node: Node): Result<Unit> = node.toJson().map { nodeJson ->
setString(context, Constants.PREF_NODE_3, nodeJson.toString())
}
private fun getNode(context: Context): Result<Node?> {
val nodeJsonString = getString(context, Constants.PREF_NODE_3) ?: return Result.success(null)
val nodeJson = kotlin.runCatching { JSONObject(nodeJsonString) }
return nodeJson.fold(
{
Node.fromJson(it)
},
{
Result.failure(it)
}
)
}
private fun setDefaultNodeAndLogError(context: Context, defaultNode: Node) {
setNode(context, defaultNode).exceptionOrNull()?.let { setNodesException ->
Timber.e(setNodesException, "Failed to set default node")
}
}
fun getOrSetDefaultNode(context: Context, defaultNode: Node): Node {
return getNode(context).fold(
{ node ->
if (node == null) {
Timber.i("No node found, saving default node")
setDefaultNodeAndLogError(context, defaultNode)
return@fold defaultNode
} else {
return@fold node
}
},
{ getNodeException ->
Timber.e(getNodeException, "Failed to get node, saving default node")
setDefaultNodeAndLogError(context, defaultNode)
return@fold defaultNode
}
)
}
}

View File

@ -0,0 +1,30 @@
package net.mynero.wallet.util
import android.os.Parcel
import android.os.Parcelable
data class TransactionDestination(val address: String, val amount: Long) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readLong()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(address)
parcel.writeLong(amount)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<TransactionDestination> {
override fun createFromParcel(parcel: Parcel): TransactionDestination {
return TransactionDestination(parcel)
}
override fun newArray(size: Int): Array<TransactionDestination?> {
return arrayOfNulls(size)
}
}
}

View File

@ -1,14 +1,13 @@
package net.mynero.wallet.util
import net.mynero.wallet.model.NetworkType
import net.mynero.wallet.model.Wallet.Companion.getPaymentIdFromAddress
import net.mynero.wallet.model.Wallet.Companion.isAddressValid
import net.mynero.wallet.model.WalletManager.Companion.instance
class UriData(val address: String, val params: HashMap<String, String>) {
fun hasPaymentId(): Boolean {
return instance?.wallet?.nettype()?.let { getPaymentIdFromAddress(address, it) }
?.isNotEmpty() == true
return getPaymentIdFromAddress(address, NetworkType.NetworkType_Mainnet.value)?.isNotEmpty() == true
}
val amount: String?

View File

@ -1,16 +0,0 @@
package net.mynero.wallet.util
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.service.ProxyService
// Collection of various utilities, probably to be refactored later
object Utils {
fun refreshProxy(proxyAddress: String, proxyPort: String) {
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)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="net.mynero.wallet.fragment.home.HomeFragment">
tools:context="net.mynero.wallet.EnotesActivity">
<TextView
android:id="@+id/view_utxos_textview"
@ -13,7 +13,7 @@
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:text="@string/view_utxos"
android:text="@string/view_enotes"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
@ -45,35 +45,38 @@
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/freeze_utxos_button"
android:id="@+id/freeze_enotes_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="1dp"
android:background="@drawable/button_bg_left"
android:enabled="false"
android:text="@string/freeze"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/churn_utxos_button"
app:layout_constraintEnd_toStartOf="@id/send_enotes_button"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/churn_utxos_button"
android:id="@+id/send_enotes_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/button_bg_center"
android:text="@string/churn"
android:enabled="false"
android:text="@string/send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/send_utxos_button"
app:layout_constraintStart_toEndOf="@id/freeze_utxos_button" />
app:layout_constraintEnd_toStartOf="@id/unfreeze_enotes_button"
app:layout_constraintStart_toEndOf="@id/freeze_enotes_button" />
<Button
android:id="@+id/send_utxos_button"
android:id="@+id/unfreeze_enotes_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="1dp"
android:background="@drawable/button_bg_right"
android:text="@string/send"
android:enabled="false"
android:text="@string/unfreeze"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/churn_utxos_button" />
app:layout_constraintStart_toEndOf="@id/send_enotes_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="net.mynero.wallet.fragment.home.HomeFragment">
tools:context="net.mynero.wallet.HomeActivity">
<ProgressBar
android:id="@+id/sync_progress_bar"
@ -32,7 +32,8 @@
app:layout_constraintEnd_toEndOf="@id/sync_progress_bar"
app:layout_constraintStart_toStartOf="@id/sync_progress_bar"
app:layout_constraintTop_toTopOf="@id/sync_progress_bar"
tools:text="Syncing... 3102333/40010203" />
tools:text="Syncing... 3102333/40010203"
tools:visibility="visible" />
<TextView
android:id="@+id/balance_unlocked_textview"
@ -47,16 +48,31 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="100.000000000000" />
<TextView
android:id="@+id/balance_frozen_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/balance_unlocked_textview"
tools:text="+ 100.000000000000 frozen"
tools:visibility="visible" />
<TextView
android:id="@+id/balance_locked_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/balance_unlocked_textview"
tools:text="+ 100.000000000000 confirming" />
app:layout_constraintTop_toBottomOf="@id/balance_frozen_textview"
tools:text="+ 100.000000000000 confirming"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/transaction_history_recyclerview"
@ -82,25 +98,6 @@
app:layout_constraintTop_toBottomOf="@id/balance_locked_textview"
tools:visibility="visible">
<ImageView
android:id="@+id/monerochan_imageview"
android:layout_width="0dp"
android:layout_height="400dp"
android:src="@drawable/xmrchan_empty2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/monerochan_empty_tx_textview"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/monerochan_empty_tx_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/no_history_nget_some_monero_in_here"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/monerochan_imageview"
app:layout_constraintTop_toTopOf="@id/monerochan_imageview" />
<TextView
android:id="@+id/empty_tx_textview"
android:layout_width="0dp"

View File

@ -3,16 +3,8 @@
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">
<ImageView
android:id="@+id/xmrchan_onboarding_imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="false"
android:scaleType="fitEnd"
android:src="@drawable/xmrchan_half2"
app:layout_constraintBottom_toBottomOf="parent" />
android:layout_height="match_parent"
tools:context="net.mynero.wallet.OnboardingActivity">
<ScrollView
android:layout_width="match_parent"

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="net.mynero.wallet.MainActivity">
tools:context="net.mynero.wallet.PasswordActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

View File

@ -4,7 +4,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
android:padding="24dp"
tools:context="net.mynero.wallet.ReceiveActivity">
<TextView
android:id="@+id/recv_monero_textview"
@ -39,19 +40,11 @@
android:layout_height="256dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_fingerprint"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recv_monero_textview" />
<ImageView
android:id="@+id/monero_logo_imageview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_monero_qr"
app:layout_constraintBottom_toBottomOf="@id/monero_qr_imageview"
app:layout_constraintEnd_toEndOf="@id/monero_qr_imageview"
app:layout_constraintStart_toStartOf="@id/monero_qr_imageview"
app:layout_constraintTop_toTopOf="@id/monero_qr_imageview" />
app:layout_constraintTop_toBottomOf="@id/recv_monero_textview"
tools:visibility="visible" />
<TextView
android:id="@+id/address_textview"
@ -65,26 +58,29 @@
android:singleLine="true"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/address_label_textview"
app:layout_constraintEnd_toStartOf="@id/copy_address_imagebutton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/monero_qr_imageview"
tools:text="ADDRESS" />
tools:text="ADDRESS"
tools:visibility="visible" />
<TextView
android:id="@+id/address_label_textview"
style="@style/MoneroText.Subaddress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="middle"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:textSize="14sp"
android:visibility="invisible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/address_textview"
tools:text="LABEL" />
tools:text="LABEL"
tools:visibility="visible" />
<ImageButton
android:id="@+id/copy_address_imagebutton"

View File

@ -3,7 +3,8 @@
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:layout_height="match_parent"
tools:context="net.mynero.wallet.SendActivity">
<TextView
android:id="@+id/send_monero_textview"

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="net.mynero.wallet.fragment.settings.SettingsFragment">
tools:context="net.mynero.wallet.SettingsActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@ -81,12 +81,26 @@
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:background="@drawable/button_bg"
android:text="@string/view_utxos"
android:text="@string/view_enotes"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/display_seed_button" />
<Button
android:id="@+id/save_proxy_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:background="@drawable/button_bg"
android:text="@string/settings_save_proxy"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/wallet_proxy_settings_layout" />
<TextView
android:id="@+id/appearance_settings_textview"
android:layout_width="match_parent"
@ -126,19 +140,7 @@
app:layout_constraintTop_toBottomOf="@id/appearance_settings_textview" />
<TextView
android:id="@+id/monerochan_label_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="@string/option_hide_xmrchan"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@id/monerochan_switch"
app:layout_constraintEnd_toStartOf="@id/monerochan_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/monerochan_switch" />
<TextView
android:id="@+id/monerochan_label_textview2"
android:id="@+id/allow_fee_override_label_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
@ -150,16 +152,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/allow_fee_override_switch" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/monerochan_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:minWidth="48dp"
android:minHeight="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/street_mode_switch" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/allow_fee_override_switch"
android:layout_width="wrap_content"
@ -168,7 +160,7 @@
android:minWidth="48dp"
android:minHeight="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/monerochan_switch" />
app:layout_constraintTop_toBottomOf="@+id/street_mode_switch" />
<TextView
android:id="@+id/network_settings_textview"
@ -210,13 +202,13 @@
android:layout_marginStart="24dp"
android:text="@string/tor_switch_label"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@id/tor_switch"
app:layout_constraintEnd_toStartOf="@id/tor_switch"
app:layout_constraintBottom_toBottomOf="@id/proxy_switch"
app:layout_constraintEnd_toStartOf="@id/proxy_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/tor_switch" />
app:layout_constraintTop_toTopOf="@id/proxy_switch" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/tor_switch"
android:id="@+id/proxy_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
@ -233,7 +225,7 @@
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tor_switch">
app:layout_constraintTop_toBottomOf="@id/proxy_switch">
<CheckBox
android:id="@+id/bundled_tor_checkbox"

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context="net.mynero.wallet.fragment.settings.SettingsFragment">
tools:context="net.mynero.wallet.TransactionActivity">
<TextView
android:id="@+id/transaction_action_textview"

View File

@ -51,7 +51,6 @@
android:src="@drawable/ic_content_paste_24dp"
app:layout_constraintBottom_toBottomOf="@id/address_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/node_port_edittext"
app:layout_constraintTop_toTopOf="@id/address_edittext"
tools:ignore="SpeakableTextPresentCheck" />
@ -65,24 +64,10 @@
android:hint="@string/node_address_hint"
android:inputType="text"
app:layout_constraintBottom_toTopOf="@id/trusted_node_checkbox"
app:layout_constraintEnd_toStartOf="@id/node_port_edittext"
app:layout_constraintEnd_toStartOf="@+id/paste_address_imagebutton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/node_name_edittext" />
<EditText
android:id="@+id/node_port_edittext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/edittext_bg"
android:digits="-QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890.:"
android:hint="@string/node_port_hint"
android:inputType="number"
app:layout_constraintBottom_toBottomOf="@id/address_edittext"
app:layout_constraintEnd_toStartOf="@id/paste_address_imagebutton"
app:layout_constraintStart_toEndOf="@id/address_edittext"
app:layout_constraintTop_toTopOf="@id/address_edittext" />
<CheckBox
android:id="@+id/trusted_node_checkbox"
android:layout_width="match_parent"

View File

@ -51,7 +51,6 @@
android:src="@drawable/ic_content_paste_24dp"
app:layout_constraintBottom_toBottomOf="@id/address_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/node_port_edittext"
app:layout_constraintTop_toTopOf="@id/address_edittext"
tools:ignore="SpeakableTextPresentCheck" />
@ -65,24 +64,10 @@
android:hint="@string/node_address_hint"
android:inputType="text"
app:layout_constraintBottom_toTopOf="@id/trusted_node_checkbox"
app:layout_constraintEnd_toStartOf="@id/node_port_edittext"
app:layout_constraintEnd_toStartOf="@+id/paste_address_imagebutton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/node_name_edittext" />
<EditText
android:id="@+id/node_port_edittext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/edittext_bg"
android:digits="-QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890.:"
android:hint="@string/node_port_hint"
android:inputType="number"
app:layout_constraintBottom_toBottomOf="@id/address_edittext"
app:layout_constraintEnd_toStartOf="@id/paste_address_imagebutton"
app:layout_constraintStart_toEndOf="@id/address_edittext"
app:layout_constraintTop_toTopOf="@id/address_edittext" />
<CheckBox
android:id="@+id/trusted_node_checkbox"
android:layout_width="match_parent"

View File

@ -75,8 +75,7 @@
<string name="add_node">Add Node</string>
<string name="nodes">Nodes</string>
<string name="node_name_hint">My Monero Node</string>
<string name="node_address_hint">127.0.0.1</string>
<string name="node_port_hint">18081</string>
<string name="node_address_hint">127.0.0.1:18081</string>
<string name="node_username_hint">Username (optional)</string>
<string name="node_password_hint">Passphrase</string>
<string name="transaction_action_sent">You sent</string>
@ -102,7 +101,8 @@
<string name="default_fee">Default</string>
<string name="medium_fee">Medium</string>
<string name="high_fee">High</string>
<string name="view_utxos">View coins</string>
<string name="view_enotes">View enotes</string>
<string name="settings_save_proxy">Save &amp; update</string>
<string name="selected_utxos_value">Selected value: %1$s XMR</string>
<string name="selected_utxos_value_churning">Selected value: %1$s XMR\n\nThe anonymity benefits of churning are still being researched. Only proceed if you know what you are doing.</string>
<string name="global_index_text">%1$d</string>
@ -111,7 +111,6 @@
<string name="churn">Churn</string>
<string name="freeze">Freeze</string>
<string name="unfreeze">Unfreeze</string>
<string name="toggle_freeze">(Un)freeze</string>
<string name="wallet_keys_label">Wallet Keys</string>
<string name="done">Done</string>
<string name="delete">Delete</string>
@ -154,5 +153,5 @@
<string name="approve_the_transaction">Send transaction</string>
<string name="paste_clipboard_into_passphrase_field">Paste clipboard into passphrase field</string>
<string name="slide_to_send_transaction">Slide to send transaction</string>
<string name="service_description">Wallet is open</string>
</resources>