mirror of
https://codeberg.org/anoncontributorxmr/mysu.git
synced 2025-01-17 14:46:32 -07:00
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:
parent
e82930ae29
commit
4ed7071872
@ -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'
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
147
app/src/main/java/net/mynero/wallet/EnotesActivity.kt
Normal file
147
app/src/main/java/net/mynero/wallet/EnotesActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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?) {
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 =
|
||||
|
39
app/src/main/java/net/mynero/wallet/data/DefaultNode.kt
Normal file
39
app/src/main/java/net/mynero/wallet/data/DefaultNode.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
3
app/src/main/java/net/mynero/wallet/model/Balance.kt
Normal file
3
app/src/main/java/net/mynero/wallet/model/Balance.kt
Normal 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)
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -90,9 +90,5 @@ class CoinsInfo : Parcelable, Comparable<CoinsInfo> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
System.loadLibrary("monerujo")
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
}
|
@ -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"
|
||||
|
@ -1,3 +0,0 @@
|
||||
package net.mynero.wallet.model
|
||||
|
||||
class TransactionOutput(val destination: String, val amount: Long)
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 =
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
147
app/src/main/java/net/mynero/wallet/util/PreferenceUtils.kt
Normal file
147
app/src/main/java/net/mynero/wallet/util/PreferenceUtils.kt
Normal 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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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?
|
||||
|
@ -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 |
@ -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>
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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 & 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>
|
||||
|
Loading…
Reference in New Issue
Block a user