Compare commits

...

2 Commits

Author SHA1 Message Date
-
a099040556 multi account and multi wallet support 2024-12-27 20:45:40 +01:00
-
cf8b1e9ce0 use coroutines for qr codes generation, adapter changes, etc 2024-12-23 15:43:32 +01:00
60 changed files with 1832 additions and 2215 deletions

2
.gitmodules vendored
View File

@ -1,4 +1,4 @@
[submodule "external-libs/monero"]
path = external-libs/monero
url = https://codeberg.org/anoncontributorxmr/monero.git
branch = v0.18.3.4-mysu
branch = v0.18.3.4-mysu-2

View File

@ -143,9 +143,4 @@ dependencies {
// QR Code stuff
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
// Tor
def vTor = '4.8.6-0'
def vKmpTor = '1.4.4'
implementation "io.matthewnelson.kotlin-components:kmp-tor:$vTor-$vKmpTor"
}

View File

@ -40,14 +40,13 @@
</activity>
<activity
android:name=".OnboardingActivity"
android:name=".WalletActivity"
android:exported="true">
</activity>
<activity
android:name=".PasswordActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
android:name=".OnboardingActivity"
android:exported="true">
</activity>
<activity

View File

@ -66,7 +66,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
class_WalletStatus = static_cast<jclass>(jenv->NewGlobalRef(
jenv->FindClass("net/mynero/wallet/model/Wallet$Status")));
class_CoinsInfo = static_cast<jclass>(jenv->NewGlobalRef(
jenv->FindClass("net/mynero/wallet/model/CoinsInfo")));
jenv->FindClass("net/mynero/wallet/model/Enote")));
return JNI_VERSION_1_6;
}
#ifdef __cplusplus
@ -103,7 +103,7 @@ struct MyWalletListener : Monero::WalletListener {
jobject jlistener;
MyWalletListener(JNIEnv *env, jobject aListener) {
LOGD("Created MyListener");
LOGD("Created MyListener TEST");
jlistener = env->NewGlobalRef(aListener);;
}
@ -643,7 +643,9 @@ Java_net_mynero_wallet_model_WalletManager_getDaemonVersion(JNIEnv *env,
JNIEXPORT jlong JNICALL
Java_net_mynero_wallet_model_WalletManager_getBlockchainHeight(JNIEnv *env, jobject instance) {
return Monero::WalletManagerFactory::getWalletManager()->blockchainHeight();
auto wm = Monero::WalletManagerFactory::getWalletManager();
auto result = wm->blockchainHeight();
return static_cast<jlong>(result);
}
JNIEXPORT jlong JNICALL
@ -1270,30 +1272,27 @@ Java_net_mynero_wallet_model_Wallet_disposeTransaction(JNIEnv *env, jobject inst
//virtual bool exportKeyImages(const std::string &filename) = 0;
//virtual bool importKeyImages(const std::string &filename) = 0;
JNIEXPORT jlong JNICALL
Java_net_mynero_wallet_model_Wallet_getCoinsJ(JNIEnv *env, jobject instance) {
Monero::Wallet *wallet = getHandle<Monero::Wallet>(env, instance);
return reinterpret_cast<jlong>(wallet->coins());
}
jobject newCoinsInfo(JNIEnv *env, const Monero::Enote& info) {
jobject newCoinsInfo(JNIEnv *env, Monero::CoinsInfo *info) {
jmethodID c = env->GetMethodID(class_CoinsInfo, "<init>",
"(JZLjava/lang/String;JLjava/lang/String;Ljava/lang/String;ZJZLjava/lang/String;)V");
jstring _key_image = env->NewStringUTF(info->keyImage().c_str());
jstring _pub_key = env->NewStringUTF(info->pubKey().c_str());
jstring _hash = env->NewStringUTF(info->hash().c_str());
jstring _address = env->NewStringUTF(info->address().c_str());
"(JJZLjava/lang/String;JLjava/lang/String;Ljava/lang/String;ZJZLjava/lang/String;)V");
jstring _key_image = env->NewStringUTF(info.keyImage.c_str());
jstring _pub_key = env->NewStringUTF(info.pubKey.c_str());
jstring _hash = env->NewStringUTF(info.hash.c_str());
jstring _address = env->NewStringUTF(info.address.c_str());
jobject result = env->NewObject(class_CoinsInfo, c,
static_cast<jlong> (info->globalOutputIndex()),
info->spent(),
static_cast<jlong> (info.idx),
static_cast<jlong> (info.globalOutputIndex),
info.spent,
_key_image,
static_cast<jlong> (info->amount()),
static_cast<jlong> (info.amount),
_hash,
_pub_key,
info->unlocked(),
static_cast<jlong> (info->internalOutputIndex()),
info->frozen(),
info.unlocked,
static_cast<jlong> (info.internalOutputIndex),
info.frozen,
_address);
env->DeleteLocalRef(_key_image);
env->DeleteLocalRef(_hash);
env->DeleteLocalRef(_pub_key);
@ -1301,7 +1300,7 @@ jobject newCoinsInfo(JNIEnv *env, Monero::CoinsInfo *info) {
return result;
}
jobject coins_cpp2java(JNIEnv *env, const std::vector<Monero::CoinsInfo *> &vector) {
jobject coins_cpp2java(JNIEnv *env, const std::vector<Monero::Enote>& vector) {
jmethodID java_util_ArrayList_ = env->GetMethodID(class_ArrayList, "<init>", "(I)V");
jmethodID java_util_ArrayList_add = env->GetMethodID(class_ArrayList, "add",
@ -1309,38 +1308,32 @@ jobject coins_cpp2java(JNIEnv *env, const std::vector<Monero::CoinsInfo *> &vect
jobject arrayList = env->NewObject(class_ArrayList, java_util_ArrayList_,
static_cast<jint> (vector.size()));
for (Monero::CoinsInfo *s: vector) {
for (const Monero::Enote& s: vector) {
jobject info = newCoinsInfo(env, s);
env->CallBooleanMethod(arrayList, java_util_ArrayList_add, info);
env->DeleteLocalRef(info);
}
return arrayList;
}
JNIEXPORT jint JNICALL
Java_net_mynero_wallet_model_Coins_getCount(JNIEnv *env, jobject instance) {
Monero::Coins *coins = getHandle<Monero::Coins>(env, instance);
return coins->count();
}
JNIEXPORT jobject JNICALL
Java_net_mynero_wallet_model_Coins_refreshJ(JNIEnv *env, jobject instance) {
Monero::Coins *coins = getHandle<Monero::Coins>(env, instance);
coins->refresh();
return coins_cpp2java(env, coins->getAll());
Java_net_mynero_wallet_model_Wallet_getEnotesJ(JNIEnv *env, jobject instance) {
Monero::Wallet *wallet = getHandle<Monero::Wallet>(env, instance);
std::vector<Monero::Enote> enotes = wallet->enotes();
return coins_cpp2java(env, enotes);
}
JNIEXPORT void JNICALL
Java_net_mynero_wallet_model_Coins_setFrozen(JNIEnv *env, jobject instance, jstring publicKey,
jboolean frozen) {
Monero::Coins *coins = getHandle<Monero::Coins>(env, instance);
const char *_publicKey = env->GetStringUTFChars(publicKey, nullptr);
if (frozen) {
coins->setFrozen(_publicKey);
} else {
coins->thaw(_publicKey);
}
env->ReleaseStringUTFChars(publicKey, _publicKey);
Java_net_mynero_wallet_model_Wallet_freeze(JNIEnv *env, jobject instance, jlong idx) {
Monero::Wallet *wallet = getHandle<Monero::Wallet>(env, instance);
wallet->freeze(static_cast<ssize_t>(idx));
}
JNIEXPORT void JNICALL
Java_net_mynero_wallet_model_Wallet_thaw(JNIEnv *env, jobject instance, jlong idx) {
Monero::Wallet *wallet = getHandle<Monero::Wallet>(env, instance);
wallet->thaw(static_cast<ssize_t>(idx));
}
//virtual TransactionHistory * history() const = 0;

View File

@ -1,15 +1,11 @@
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
@ -17,18 +13,17 @@ 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.model.Enote
import net.mynero.wallet.model.Wallet
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
import net.mynero.wallet.util.acitivity.MoneroActivity
class EnotesActivity : AppCompatActivity(), WalletServiceObserver {
class EnotesActivity : MoneroActivity() {
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
@ -46,12 +41,12 @@ class EnotesActivity : AppCompatActivity(), WalletServiceObserver {
val streetMode = PreferenceUtils.isStreetMode(this)
adapter = EnotesAdapter(listOf(), streetMode, object : EnotesAdapter.EnotesAdapterListener {
override fun onEnoteSelected(coinsInfo: CoinsInfo) {
val selected = adapter.contains(coinsInfo)
override fun onEnoteSelected(enote: Enote) {
val selected = adapter.contains(enote)
if (selected) {
adapter.deselectEnote(coinsInfo)
adapter.deselectEnote(enote)
} else {
adapter.selectEnote(coinsInfo)
adapter.selectEnote(enote)
}
var frozenExists = false
var unfrozenExists = false
@ -76,21 +71,6 @@ class EnotesActivity : AppCompatActivity(), WalletServiceObserver {
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() {
@ -107,41 +87,57 @@ class EnotesActivity : AppCompatActivity(), WalletServiceObserver {
freezeUtxosButton.setOnClickListener {
Toast.makeText(this, "Freezing enotes, please wait.", Toast.LENGTH_SHORT)
.show()
walletService?.freezeEnote(adapter.getSelectedEnotes().keys.filterNotNull().toList())
walletService?.freezeEnote(adapter.getSelectedEnotes().keys.toList())
}
unfreezeUtxosButton.setOnClickListener {
Toast.makeText(this, "Thawing enotes, please wait.", Toast.LENGTH_SHORT)
.show()
walletService?.thawEnote(adapter.getSelectedEnotes().keys.filterNotNull().toList())
walletService?.thawEnote(adapter.getSelectedEnotes().keys.toList())
}
}
private fun bindObservers() {
enotesRecyclerView.layoutManager = LinearLayoutManager(this)
enotesRecyclerView.adapter = adapter
viewModel.enotes.observe(this) { enotes: List<CoinsInfo> ->
viewModel.enotes.observe(this) { enotes: List<Enote> ->
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)
override fun onWalletServiceBound(walletService: WalletService) {
walletService.getWallet()?.let { wallet ->
updateState(wallet)
}
}
override fun onWalletUpdated(wallet: Wallet) {
updateState(wallet)
}
private fun updateState(wallet: Wallet) {
val enotes = wallet.getEnotes()
val addresses = (0 until wallet.numSubaddresses).map {
wallet.getSubaddress(wallet.getAccountIndex(), it)
}.toSet()
val filteredEnotes = enotes.filter {
it.address != null && addresses.contains(it.address)
}
viewModel.updateEnotes(filteredEnotes)
}
}
internal class EnotesViewModel : ViewModel() {
private val _enotes: MutableLiveData<List<CoinsInfo>> = MutableLiveData()
val enotes: LiveData<List<CoinsInfo>> = _enotes
private val _enotes: MutableLiveData<List<Enote>> = MutableLiveData()
val enotes: LiveData<List<Enote>> = _enotes
fun updateEnotes(enotes: List<CoinsInfo>) {
fun updateEnotes(enotes: List<Enote>) {
_enotes.postValue(enotes)
}
}

View File

@ -1,45 +1,33 @@
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.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
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.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.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
import net.mynero.wallet.util.acitivity.MoneroActivity
class HomeActivity : AppCompatActivity(), WalletServiceObserver {
class HomeActivity : MoneroActivity() {
private val viewModel: HomeActivityViewModel by viewModels()
private var walletService: WalletService? = null
private lateinit var walletName: String
private lateinit var walletPassword: String
private lateinit var walletAndAccountTextView: TextView
private lateinit var progressBar: ProgressBar
private lateinit var progressBarText: TextView
@ -58,9 +46,6 @@ class HomeActivity : AppCompatActivity(), WalletServiceObserver {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
walletName = intent.extras?.getString(Constants.EXTRA_WALLET_NAME)!!
walletPassword = intent.extras?.getString(Constants.EXTRA_WALLET_PASSWORD)!!
settingsImageView = findViewById(R.id.settings_imageview)
sendButton = findViewById(R.id.send_button)
receiveButton = findViewById(R.id.receive_button)
@ -70,6 +55,7 @@ class HomeActivity : AppCompatActivity(), WalletServiceObserver {
frozenBalanceTextView = findViewById(R.id.balance_frozen_textview)
lockedBalanceTextView = findViewById(R.id.balance_locked_textview)
walletAndAccountTextView = findViewById(R.id.wallet_and_account_textview)
progressBar = findViewById(R.id.sync_progress_bar)
progressBarText = findViewById(R.id.sync_progress_text)
@ -103,58 +89,27 @@ class HomeActivity : AppCompatActivity(), WalletServiceObserver {
updateTransactionHistory(history)
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
samouraiTorManager?.getTorStateLiveData()?.observe(this) {
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if (socketAddress.toString().isEmpty()) return@let
if (ProxyService.instance?.usingProxy == true && ProxyService.instance?.useBundledTor == true) {
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
if (WalletManager.instance.proxy != proxyString)
refreshProxy(address, port)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
walletService?.closeWallet() // TODO: just stop the service instead?
finish()
if (PreferenceUtils.isMultiAccountMode(this@HomeActivity)) {
startActivity(Intent(this@HomeActivity, WalletActivity::class.java))
}
}
}
connectWalletService()
})
}
override fun onResume() {
super.onResume()
// label, balances and transaction history must be updated here because street mode may have been changed by another activity
updateWalletAndAccountLabel(walletService?.getWallet())
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 newProxy = "$proxyAddress:$proxyPort"
if ((proxyAddress != cachedProxyAddress) || (proxyPort != cachedProxyPort) || (newProxy != currentWalletProxy && newProxy != ":")) {
// ProxyService.instance?.updateProxy(proxyAddress, proxyPort)
}
override fun onWalletServiceBound(walletService: WalletService) {
updateState(walletService, walletService.getWallet())
}
private fun displayEmptyHistory(
@ -169,24 +124,37 @@ class HomeActivity : AppCompatActivity(), WalletServiceObserver {
textView.setText(textResId)
}
private fun updateState(walletService: WalletService, wallet: Wallet?) {
wallet?.let {
updateWalletAndAccountLabel(wallet)
}
updateSynchronizationProgress(walletService, wallet)
updateBalances(wallet?.getBalance())
updateTransactionHistory(wallet?.getHistory())
}
override fun onWalletUpdated(wallet: Wallet) {
runOnUiThread {
updateState(walletService!!, wallet)
}
}
override fun onBlockchainHeightFetched(height: Long) {
updateSynchronizationProgress()
val walletService = walletService!!
runOnUiThread {
updateSynchronizationProgress(walletService, walletService.getWallet())
}
}
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 updateWalletAndAccountLabel(wallet: Wallet?) {
val isMultiWalletMode = PreferenceUtils.isMultiWalletMode(this)
val isMultiAccountMode = PreferenceUtils.isMultiAccountMode(this)
if (wallet != null && (isMultiWalletMode || isMultiAccountMode)) {
walletAndAccountTextView.visibility = View.VISIBLE
walletAndAccountTextView.text = "${wallet.name} / ${wallet.getAccountIndex()}"
} else {
walletAndAccountTextView.visibility = View.GONE
}
}
private fun updateBalances(balance: Balance?) {
@ -215,7 +183,7 @@ class HomeActivity : AppCompatActivity(), WalletServiceObserver {
private fun updateTransactionHistory(history: List<TransactionInfo>?) {
if (history.isNullOrEmpty()) {
val wallet = walletService?.getWallet()
val textResId: Int = if (wallet != null && wallet.isSynchronized) {
val textResId: Int = if (wallet != null) {
R.string.no_history_nget_some_monero_in_here
} else {
R.string.no_history_loading
@ -240,34 +208,33 @@ class HomeActivity : AppCompatActivity(), WalletServiceObserver {
}
}
private fun updateSynchronizationProgress() {
walletService?.let { walletService ->
private fun updateSynchronizationProgress(walletService: WalletService, wallet: Wallet?) {
wallet?.let {
val walletBeginSyncHeight = walletService.getWalletBeginSyncHeight()
val walletHeight = walletService.getWalletOrThrow().getBlockChainHeightJ()
val walletHeight = wallet.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)"
val synchronized = daemonHeight > 0 && diff == 0L
val walletDisplayHeight = (walletHeight - 1).toString()
val daemonDisplayHeight = if (daemonHeight > 0) (daemonHeight - 1).toString() else "???"
progressBarText.visibility = View.VISIBLE
if (synchronized) {
progressBar.visibility = View.INVISIBLE
progressBarText.text = "Synchronized at $walletDisplayHeight"
} else {
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
if (walletHeight > walletBeginSyncHeight) {
progressBarText.text = "Synchronizing: $walletDisplayHeight / $daemonDisplayHeight ($diff blocks remaining)"
} else {
progressBar.visibility = View.INVISIBLE
progressBar.isIndeterminate = true
progressBarText.text = "Connecting..."
progressBarText.text = "Starting wallet synchronization..."
}
}
}
@ -275,21 +242,12 @@ class HomeActivity : AppCompatActivity(), WalletServiceObserver {
}
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)
}
@ -298,5 +256,3 @@ internal class HomeActivityViewModel : ViewModel() {
_balance.postValue(newBalance)
}
}
internal data class HeightInfo(val daemon: Long, val wallet: Long)

View File

@ -1,8 +1,6 @@
package net.mynero.wallet
import android.app.Application
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.NightmodeHelper
import timber.log.Timber
@ -17,8 +15,6 @@ class MoneroApplication : Application() {
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
PrefService(this)
ProxyService(this)
NightmodeHelper.preferredNightmode
}
}

View File

@ -1,8 +1,6 @@
package net.mynero.wallet
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
@ -16,14 +14,11 @@ 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.lifecycleScope
import com.google.android.material.progressindicator.CircularProgressIndicator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.mynero.wallet.data.DefaultNode
@ -31,45 +26,42 @@ 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.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.Helper
import net.mynero.wallet.util.PreferenceUtils
import net.mynero.wallet.util.RestoreHeight
import net.mynero.wallet.util.acitivity.WalletOpeningActivity
import timber.log.Timber
import java.io.File
import java.util.Calendar
class OnboardingActivity : AppCompatActivity() {
class OnboardingActivity : WalletOpeningActivity() {
private val viewModel: OnboardingViewModel by viewModels()
private lateinit var walletProxyAddressEditText: EditText
private lateinit var walletProxyPortEditText: EditText
private lateinit var walletNameEditText: EditText
private lateinit var walletPasswordEditText: EditText
private lateinit var walletPasswordConfirmEditText: EditText
private lateinit var walletSeedEditText: EditText
private lateinit var walletRestoreHeightEditText: EditText
private lateinit var createWalletButton: Button
private lateinit var moreOptionsDropdownTextView: TextView
private lateinit var torSwitch: SwitchCompat
private lateinit var advancedOptionsLayout: ConstraintLayout
private lateinit var moreOptionsChevronImageView: ImageView
private lateinit var seedOffsetCheckbox: CheckBox
private lateinit var selectNodeButton: Button
private lateinit var showXmrchanSwitch: SwitchCompat
private lateinit var seedTypeButton: Button
private lateinit var seedTypeDescTextView: TextView
private lateinit var useBundledTor: CheckBox
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_onboarding)
selectNodeButton = findViewById(R.id.select_node_button)
walletNameEditText = findViewById(R.id.wallet_name_edittext)
walletPasswordEditText = findViewById(R.id.wallet_password_edittext)
walletPasswordConfirmEditText = findViewById(R.id.wallet_password_confirm_edittext)
walletSeedEditText = findViewById(R.id.wallet_seed_edittext)
@ -77,31 +69,31 @@ class OnboardingActivity : AppCompatActivity() {
createWalletButton = findViewById(R.id.create_wallet_button)
moreOptionsDropdownTextView = findViewById(R.id.advanced_settings_dropdown_textview)
moreOptionsChevronImageView = findViewById(R.id.advanced_settings_chevron_imageview)
torSwitch = findViewById(R.id.tor_onboarding_switch)
seedOffsetCheckbox = findViewById(R.id.seed_offset_checkbox)
walletProxyAddressEditText = findViewById(R.id.wallet_proxy_address_edittext)
walletProxyPortEditText = findViewById(R.id.wallet_proxy_port_edittext)
advancedOptionsLayout = findViewById(R.id.more_options_layout)
showXmrchanSwitch = findViewById(R.id.show_xmrchan_switch)
seedTypeButton = findViewById(R.id.seed_type_button)
seedTypeDescTextView = findViewById(R.id.seed_type_desc_textview)
useBundledTor = findViewById(R.id.bundled_tor_checkbox)
seedOffsetCheckbox.isChecked = viewModel.useOffset
val usingProxy = ProxyService.instance?.usingProxy == true
val usingBundledTor = ProxyService.instance?.useBundledTor == true
val useProxy = PreferenceUtils.isUseProxy(this)
torSwitch.isChecked = usingProxy
useBundledTor.isChecked = usingBundledTor
useBundledTor.isEnabled = usingProxy
walletProxyAddressEditText.isEnabled = usingProxy && !usingBundledTor
walletProxyPortEditText.isEnabled = usingProxy && !usingBundledTor
walletProxyAddressEditText.isEnabled = useProxy
val node = PreferenceUtils.getOrSetDefaultNode(this, DefaultNode.defaultNode())
selectNodeButton.text = getString(R.string.node_button_text, node.name)
walletNameEditText.setText(Constants.DEFAULT_WALLET_NAME)
bindListeners()
bindObservers()
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (!intent.getBooleanExtra(Constants.EXTRA_PREVENT_GOING_BACK, false)) {
finish()
}
}
})
}
private fun bindObservers() {
@ -144,65 +136,12 @@ class OnboardingActivity : AppCompatActivity() {
}
}
viewModel.useBundledTor.observe(this) { isChecked ->
walletProxyPortEditText.isEnabled = !isChecked && viewModel.useProxy.value == true
walletProxyAddressEditText.isEnabled = !isChecked && viewModel.useProxy.value == true
}
viewModel.useProxy.observe(this) { useProxy ->
useBundledTor.isEnabled = useProxy
walletProxyAddressEditText.isEnabled = useProxy && viewModel.useBundledTor.value == false
walletProxyPortEditText.isEnabled = useProxy && viewModel.useBundledTor.value == false
walletProxyAddressEditText.isEnabled = useProxy
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
val indicatorCircle = findViewById<CircularProgressIndicator>(R.id.onboarding_tor_loading_progressindicator)
val torIcon = findViewById<ImageView>(R.id.onboarding_tor_icon)
samouraiTorManager?.getTorStateLiveData()?.observe(this) { state ->
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if (socketAddress.toString().isEmpty()) return@let
if (viewModel.useProxy.value == true && viewModel.useBundledTor.value == true) {
torIcon?.visibility = View.VISIBLE
indicatorCircle?.visibility = View.INVISIBLE
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
updateProxy(address, port)
}
}
indicatorCircle.isIndeterminate = state.progressIndicator == 0
indicatorCircle.progress = state.progressIndicator
when (state.state) {
EnumTorState.OFF -> {
torIcon.visibility = View.INVISIBLE
indicatorCircle.visibility = View.INVISIBLE
}
EnumTorState.STARTING, EnumTorState.STOPPING -> {
torIcon.visibility = View.INVISIBLE
indicatorCircle.visibility = View.VISIBLE
}
else -> {}
}
}
}
private fun updateProxy(address: String, port: String) {
walletProxyPortEditText.setText(port)
walletProxyAddressEditText.setText(address)
}
private fun bindListeners() {
// Disable onBack click
val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
moreOptionsDropdownTextView.setOnClickListener { viewModel.onMoreOptionsClicked() }
moreOptionsChevronImageView.setOnClickListener { viewModel.onMoreOptionsClicked() }
@ -212,7 +151,6 @@ class OnboardingActivity : AppCompatActivity() {
}
createWalletButton.setOnClickListener {
onBackPressedCallback.isEnabled = false
createOrImportWallet(
walletSeedEditText.text.toString().trim { it <= ' ' },
walletRestoreHeightEditText.text.toString().trim { it <= ' ' }
@ -250,19 +188,6 @@ class OnboardingActivity : AppCompatActivity() {
seedTypeButton.setOnClickListener { toggleSeedType() }
torSwitch.setOnCheckedChangeListener { _, b: Boolean ->
viewModel.setUseProxy(b)
}
walletProxyPortEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
viewModel.setProxyPort(text)
}
})
walletProxyAddressEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
@ -302,10 +227,6 @@ class OnboardingActivity : AppCompatActivity() {
val dialog = NodeSelectionBottomSheetDialog(nodes, node, listener)
dialog.show(supportFragmentManager, "node_selection_dialog")
}
useBundledTor.setOnCheckedChangeListener { _, isChecked ->
viewModel.setUseBundledTor(isChecked)
}
}
private fun toggleSeedType() {
@ -323,16 +244,14 @@ class OnboardingActivity : AppCompatActivity() {
walletSeed: String,
restoreHeightText: String
) {
this.let { act ->
lifecycleScope.launch(Dispatchers.IO) {
viewModel.createOrImportWallet(
act,
walletSeed,
restoreHeightText,
viewModel.useOffset,
applicationContext
)
}
lifecycleScope.launch(Dispatchers.IO) {
viewModel.createOrImportWallet(
this@OnboardingActivity,
walletNameEditText.text.toString(),
walletSeed,
restoreHeightText,
viewModel.useOffset,
)
}
}
}
@ -346,37 +265,26 @@ internal class OnboardingViewModel : ViewModel() {
val useProxy: LiveData<Boolean> = _useProxy
private val _proxyAddress = MutableLiveData("")
private val _proxyPort = MutableLiveData("")
private val _useBundledTor = MutableLiveData(false)
val useBundledTor: LiveData<Boolean> = _useBundledTor
private val _passphrase = MutableLiveData("")
val passphrase: LiveData<String> = _passphrase
private val _confirmedPassphrase = MutableLiveData("")
var showMoreOptions: LiveData<Boolean> = _showMoreOptions
var seedType: LiveData<SeedType> = _seedType
init {
_useProxy.value = ProxyService.instance?.usingProxy
_useBundledTor.value = ProxyService.instance?.useBundledTor
}
val enableButton = combineLiveDatas(
seedType,
_useProxy,
_proxyAddress,
_proxyPort,
_useBundledTor,
_passphrase,
_confirmedPassphrase,
_creatingWallet,
ProxyService.instance?.samouraiTorManager?.getTorStateLiveData()
) { seedType, useProxy, proxyAddress, proxyPort, useBundledTor, passphrase, confirmedPassphrase, creatingWallet, torState ->
if (seedType == null || useProxy == null || proxyAddress == null || proxyPort == null || useBundledTor == null || passphrase == null || confirmedPassphrase == null || creatingWallet == null) return@combineLiveDatas false
) { seedType, useProxy, proxyAddress, proxyPort, passphrase, confirmedPassphrase, creatingWallet ->
if (seedType == null || useProxy == null || proxyAddress == null || proxyPort == null || passphrase == null || confirmedPassphrase == null || creatingWallet == null) return@combineLiveDatas false
if ((passphrase.isNotEmpty() || confirmedPassphrase.isNotEmpty()) && passphrase != confirmedPassphrase) return@combineLiveDatas false
if (creatingWallet) return@combineLiveDatas false
if (seedType == SeedType.POLYSEED && (passphrase.isEmpty() || confirmedPassphrase.isEmpty())) return@combineLiveDatas false
if (useProxy && (proxyAddress.isEmpty() || proxyPort.isEmpty()) && !useBundledTor) return@combineLiveDatas false
val progress = torState?.progressIndicator ?: 0
if (useBundledTor && progress < 100 && useProxy) return@combineLiveDatas false
if (useProxy && (proxyAddress.isEmpty() || proxyPort.isEmpty())) return@combineLiveDatas false
return@combineLiveDatas true
}
@ -392,24 +300,24 @@ internal class OnboardingViewModel : ViewModel() {
}
fun createOrImportWallet(
mainActivity: Activity,
activity: OnboardingActivity,
walletName: String,
walletSeed: String,
restoreHeightText: String,
useOffset: Boolean,
context: Context
) {
val passphrase = _passphrase.value ?: return
val confirmedPassphrase = _confirmedPassphrase.value ?: return
val application = mainActivity.application as MoneroApplication
val application = activity.application as MoneroApplication
_creatingWallet.postValue(true)
val offset = if (useOffset) confirmedPassphrase else ""
if (passphrase.isNotEmpty()) {
if (passphrase != confirmedPassphrase) {
_creatingWallet.postValue(false)
mainActivity.runOnUiThread {
activity.runOnUiThread {
Toast.makeText(
mainActivity,
activity,
application.getString(R.string.invalid_confirmed_password),
Toast.LENGTH_SHORT
).show()
@ -418,19 +326,16 @@ internal class OnboardingViewModel : ViewModel() {
}
}
var restoreHeight = newRestoreHeight
val walletFile = File(mainActivity.applicationInfo.dataDir, Constants.DEFAULT_WALLET_NAME)
val walletFile = File(Helper.getWalletRoot(activity), walletName)
var wallet: Wallet? = null
if (offset.isNotEmpty()) {
PrefService.instance.edit().putBoolean(Constants.PREF_USES_OFFSET, true).apply()
}
val seedTypeValue = seedType.value ?: return
if (walletSeed.isEmpty()) {
if (seedTypeValue == SeedType.POLYSEED) {
wallet = if (offset.isEmpty()) {
mainActivity.runOnUiThread {
activity.runOnUiThread {
_creatingWallet.postValue(false)
Toast.makeText(
mainActivity,
activity,
application.getString(R.string.invalid_empty_passphrase),
Toast.LENGTH_SHORT
).show()
@ -446,26 +351,23 @@ internal class OnboardingViewModel : ViewModel() {
}
} else if (seedTypeValue == SeedType.LEGACY) {
val tmpWalletFile =
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(
walletFile,
passphrase,
tmpWallet.getSeed("") ?: return@let,
offset,
restoreHeight
)
tmpWalletFile.delete()
}
File(activity.applicationInfo.dataDir, walletName + "_tmp")
val tmpWallet = createTempWallet(tmpWalletFile) //we do this to get seed, then recover wallet so we can use seed offset
wallet = WalletManager.instance.recoveryWallet(
walletFile,
passphrase,
tmpWallet.getSeed(""),
offset,
restoreHeight
)
tmpWalletFile.delete()
}
} else {
if (getMnemonicType(walletSeed) == SeedType.UNKNOWN) {
mainActivity.runOnUiThread {
activity.runOnUiThread {
_creatingWallet.postValue(false)
Toast.makeText(
mainActivity,
activity,
application.getString(R.string.invalid_mnemonic_code),
Toast.LENGTH_SHORT
).show()
@ -476,14 +378,14 @@ internal class OnboardingViewModel : ViewModel() {
restoreHeight = restoreHeightText.toLong()
}
if (seedTypeValue == SeedType.POLYSEED) {
wallet = WalletManager.instance?.recoveryWalletPolyseed(
wallet = WalletManager.instance.recoveryWalletPolyseed(
walletFile,
passphrase,
walletSeed,
offset
)
} else if (seedTypeValue == SeedType.LEGACY) {
wallet = WalletManager.instance?.recoveryWallet(
wallet = WalletManager.instance.recoveryWallet(
walletFile,
passphrase,
walletSeed,
@ -497,16 +399,12 @@ 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) {
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()
activity.openWallet(walletFile.absolutePath, passphrase)
} else {
mainActivity.runOnUiThread {
activity.runOnUiThread {
_creatingWallet.postValue(false)
Toast.makeText(
mainActivity,
activity,
application.getString(
R.string.create_wallet_failed,
walletStatus?.errorString
@ -536,8 +434,8 @@ internal class OnboardingViewModel : ViewModel() {
}
}
private fun createTempWallet(tmpWalletFile: File): Wallet? {
return WalletManager.instance?.createWallet(
private fun createTempWallet(tmpWalletFile: File): Wallet {
return WalletManager.instance.createWallet(
tmpWalletFile,
"",
Constants.MNEMONIC_LANGUAGE,
@ -547,40 +445,11 @@ internal class OnboardingViewModel : ViewModel() {
fun setProxyAddress(address: String) {
_proxyAddress.value = address
if (address.isEmpty()) PrefService.instance.deleteProxy()
val port = _proxyPort.value ?: return
// ProxyService.instance?.updateProxy(address, port)
}
fun setProxyPort(port: String) {
_proxyPort.value = port
if (port.isEmpty()) PrefService.instance.deleteProxy()
val address = _proxyAddress.value ?: return
// ProxyService.instance?.updateProxy(address, port)
}
fun setUseBundledTor(useBundled: Boolean) {
_useBundledTor.value = useBundled
ProxyService.instance?.useBundledTor = useBundled
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (useBundled && ProxyService.instance?.usingProxy == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
fun setUseProxy(useProxy: Boolean) {
_useProxy.value = useProxy
ProxyService.instance?.usingProxy = useProxy
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (useProxy && ProxyService.instance?.useBundledTor == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
fun setPassphrase(passphrase: String) {

View File

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

View File

@ -6,41 +6,41 @@ 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.AtomicReference
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.journeyapps.barcodescanner.BarcodeEncoder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.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.EnumMap
import net.mynero.wallet.util.QrCodeHelper
import net.mynero.wallet.util.acitivity.MoneroActivity
import timber.log.Timber
import kotlin.time.DurationUnit
import kotlin.time.measureTimedValue
class ReceiveActivity : AppCompatActivity(), WalletServiceObserver {
class ReceiveActivity : MoneroActivity() {
private val viewModel: ReceiveViewModel by viewModels()
private var walletService: WalletService? = null
private lateinit var addressTextView: TextView
private lateinit var addressLabelTextView: TextView
private lateinit var addressImageView: ImageView
@ -50,7 +50,6 @@ class ReceiveActivity : AppCompatActivity(), WalletServiceObserver {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setVisible(false)
setContentView(R.layout.activity_receive)
addressImageView = findViewById(R.id.monero_qr_imageview)
@ -62,21 +61,6 @@ class ReceiveActivity : AppCompatActivity(), WalletServiceObserver {
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@ReceiveActivity.walletService = walletService
walletService.addObserver(this@ReceiveActivity)
viewModel.refreshSubaddresses(walletService)
}
override fun onServiceDisconnected(className: ComponentName) {
walletService = null
}
}
private fun bindListeners() {
@ -88,7 +72,7 @@ class ReceiveActivity : AppCompatActivity(), WalletServiceObserver {
private fun bindObservers() {
val subaddressAdapterListener = object : SubaddressAdapterListener {
override fun onSubaddressSelected(subaddress: Subaddress) {
viewModel.selectAddress(subaddress)
viewModel.selectAddress(subaddress, qrCodeBackgroundColor())
}
override fun onSubaddressEditLabel(subaddress: Subaddress) {
@ -105,18 +89,32 @@ class ReceiveActivity : AppCompatActivity(), WalletServiceObserver {
adapter.submitSelectedAddress(address)
}
}
viewModel.qrCode.observe(this) { bitmap: Bitmap? ->
if (bitmap == null) {
addressImageView.visibility = View.INVISIBLE
} else {
addressImageView.setImageBitmap(bitmap)
addressImageView.visibility = View.VISIBLE
}
}
viewModel.subaddresses.observe(this) { addresses: List<Subaddress> ->
// We want newer addresses addresses to be shown first
adapter.submitAddresses(addresses.reversed())
}
}
override fun onWalletServiceBound(walletService: WalletService) {
viewModel.refreshSubaddresses(walletService, qrCodeBackgroundColor())
}
override fun onSubaddressesUpdated() {
walletService?.let { walletService ->
viewModel.refreshSubaddresses(walletService)
viewModel.refreshSubaddresses(walletService, qrCodeBackgroundColor())
}
}
private fun qrCodeBackgroundColor(): Int = ContextCompat.getColor(this, R.color.oled_colorBackground)
private fun editAddressLabel(subaddress: Subaddress) {
val dialog = EditAddressLabelBottomSheetDialog(subaddress.label) {
walletService?.setSubaddressLabel(subaddress.addressIndex, it)
@ -134,9 +132,6 @@ class ReceiveActivity : AppCompatActivity(), WalletServiceObserver {
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
@ -151,43 +146,21 @@ class ReceiveActivity : AppCompatActivity(), WalletServiceObserver {
true
}
}
private fun generate(text: String, width: Int, height: Int): Bitmap? {
if (width <= 0 || height <= 0) return null
val hints: MutableMap<EncodeHintType, Any?> =
EnumMap(com.google.zxing.EncodeHintType::class.java)
hints[EncodeHintType.CHARACTER_SET] = StandardCharsets.UTF_8
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
try {
val barcodeEncoder = BarcodeEncoder()
val bitMatrix = barcodeEncoder.encode(text, BarcodeFormat.QR_CODE, width, height, hints)
val pixels = IntArray(bitMatrix.width * bitMatrix.height)
for (i in 0 until bitMatrix.height) {
for (j in 0 until bitMatrix.width) {
if (bitMatrix[j, i]) {
pixels[i * width + j] = -0x1
} else {
pixels[i * height + j] = ContextCompat.getColor(this, R.color.oled_colorBackground)
}
}
}
return Bitmap.createBitmap(pixels, 0, bitMatrix.width, bitMatrix.width, bitMatrix.height, Bitmap.Config.ARGB_8888)
} catch (ex: WriterException) {
Log.e("ReceiveFragment.kt", ex.toString())
}
return null
}
}
class ReceiveViewModel : ViewModel() {
private var qrCodeJob: AtomicReference<Job?> = AtomicReference(null)
private val _selectedSubaddress = MutableLiveData<Subaddress?>()
val selectedSubaddress: LiveData<Subaddress?> = _selectedSubaddress
private val _subaddresses = MutableLiveData<List<Subaddress>>()
val subaddresses: LiveData<List<Subaddress>> = _subaddresses
fun refreshSubaddresses(walletService: WalletService) {
private val _qrCode = MutableLiveData<Bitmap?>()
val qrCode: LiveData<Bitmap?> = _qrCode
fun refreshSubaddresses(walletService: WalletService, backgroundColor: Int) {
val wallet = walletService.getWalletOrThrow()
val newSubaddresses = (0 until wallet.numSubaddresses).map { wallet.getSubaddressObject(it) }
@ -195,13 +168,30 @@ class ReceiveViewModel : ViewModel() {
val currentSelectedSubaddress = selectedSubaddress.value
if (currentSelectedSubaddress == null) {
_selectedSubaddress.postValue(newSubaddresses.lastOrNull())
selectAddress(newSubaddresses.lastOrNull(), backgroundColor)
} else {
_selectedSubaddress.postValue(newSubaddresses.getOrNull(currentSelectedSubaddress.addressIndex))
selectAddress(newSubaddresses.getOrNull(currentSelectedSubaddress.addressIndex), backgroundColor)
}
}
fun selectAddress(subaddress: Subaddress?) {
_selectedSubaddress.value = subaddress
fun selectAddress(subaddress: Subaddress?, backgroundColor: Int) {
qrCodeJob.getAndUpdate { job ->
job?.cancel()
_selectedSubaddress.postValue(subaddress)
_qrCode.postValue(null)
subaddress?.let {
viewModelScope.launch {
withContext(Dispatchers.Default) {
val (bitmap, time) = measureTimedValue {
QrCodeHelper.generateQrCode(subaddress.address, 256, 256, backgroundColor)
}
Timber.d("Generated QR code bitmap in ${time.toInt(DurationUnit.MILLISECONDS)} milliseconds")
if (isActive) {
_qrCode.postValue(bitmap)
}
}
}
}
}
}
}

View File

@ -1,10 +1,6 @@
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.text.Editable
import android.text.TextWatcher
import android.util.Log
@ -18,7 +14,6 @@ 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
@ -32,22 +27,20 @@ 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.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
import net.mynero.wallet.util.acitivity.MoneroActivity
class SendActivity : AppCompatActivity(), WalletServiceObserver {
class SendActivity : MoneroActivity() {
var priority: PendingTransaction.Priority = PendingTransaction.Priority.Priority_Default
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
@ -101,8 +94,6 @@ class SendActivity : AppCompatActivity(), WalletServiceObserver {
bindListeners()
bindObservers()
init()
connectWalletService()
}
private fun bindListeners() {
@ -212,23 +203,8 @@ class SendActivity : AppCompatActivity(), WalletServiceObserver {
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
}
override fun onWalletServiceBound(walletService: WalletService) {
updateEnotesLabel()
}
private fun updateEnotesLabel() {
@ -236,7 +212,7 @@ class SendActivity : AppCompatActivity(), WalletServiceObserver {
val selectedUtxos = viewModel.enotes.value
if (selectedUtxos?.isNotEmpty() == true) {
var selectedValue: Long = 0
val enotes = walletService.getWalletOrThrow().coins!!.all
val enotes = walletService.getWalletOrThrow().getEnotes()
for (coinsInfo in enotes) {
if (selectedUtxos.contains(coinsInfo.keyImage)) {
selectedValue += coinsInfo.amount
@ -579,7 +555,6 @@ class SendActivity : AppCompatActivity(), WalletServiceObserver {
}
override fun onTransactionSent(pendingTransaction: PendingTransaction, success: Boolean) {
walletService?.refreshTransactionsHistory()
runOnUiThread {
if (success) {
Toast.makeText(this, getString(R.string.sent_tx), Toast.LENGTH_SHORT).show()

View File

@ -1,124 +1,129 @@
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 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 com.google.android.material.progressindicator.CircularProgressIndicator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.adapter.AccountAdapter
import net.mynero.wallet.data.DefaultNode
import net.mynero.wallet.data.Node
import net.mynero.wallet.fragment.dialog.EditAccountLabelBottomSheetDialog
import net.mynero.wallet.fragment.dialog.NodeSelectionBottomSheetDialog
import net.mynero.wallet.fragment.dialog.PasswordBottomSheetDialog
import net.mynero.wallet.fragment.dialog.WalletKeysBottomSheetDialog
import net.mynero.wallet.listener.NodeSelectionDialogListenerAdapter
import net.mynero.wallet.model.EnumTorState
import net.mynero.wallet.service.PrefService
import net.mynero.wallet.service.ProxyService
import net.mynero.wallet.service.wallet.WalletServiceObserver
import net.mynero.wallet.model.Account
import net.mynero.wallet.model.Balance
import net.mynero.wallet.service.wallet.WalletService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.PreferenceUtils
import net.mynero.wallet.util.acitivity.MoneroActivity
import timber.log.Timber
class SettingsActivity : AppCompatActivity(), WalletServiceObserver {
private val viewModel: SettingsViewModel by viewModels()
private var walletService: WalletService? = null
class SettingsActivity : MoneroActivity() {
private lateinit var walletProxyAddressEditText: EditText
private lateinit var walletProxyPortEditText: EditText
private lateinit var selectNodeButton: Button
private lateinit var multiWalletModeSwitch: SwitchCompat
private lateinit var multiAccountModeSwitch: SwitchCompat
private lateinit var streetModeSwitch: SwitchCompat
private lateinit var allowFeeOverrideSwitch: SwitchCompat
private lateinit var useBundledTor: CheckBox
private lateinit var displaySeedButton: Button
private lateinit var displayUtxosButton: Button
private lateinit var displayEnotesButton: Button
private lateinit var proxySwitch: SwitchCompat
private lateinit var proxySettingsLayout: ConstraintLayout
private lateinit var saveProxyButton: Button
private lateinit var accountSettingsConstraintLayout: ConstraintLayout
private lateinit var addAccountButton: Button
private lateinit var accountsRecyclerView: RecyclerView
private val askForWalletPasswordAndDisplayWalletKeys = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val password = result.data?.extras?.getString(Constants.EXTRA_WALLET_PASSWORD)
password?.let { displaySeedDialog(it) }
}
}
private lateinit var accountAdapter: AccountAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
displaySeedButton = findViewById(R.id.display_seed_button)
displayUtxosButton = findViewById(R.id.display_utxos_button)
displayEnotesButton = findViewById(R.id.display_utxos_button)
selectNodeButton = findViewById(R.id.select_node_button)
multiWalletModeSwitch = findViewById(R.id.enable_multiple_wallets_switch)
multiAccountModeSwitch = findViewById(R.id.enable_multiple_accounts_switch)
streetModeSwitch = findViewById(R.id.street_mode_switch)
allowFeeOverrideSwitch = findViewById(R.id.allow_fee_override_switch)
proxySwitch = findViewById(R.id.proxy_switch)
val proxySettingsLayout = findViewById<ConstraintLayout>(R.id.wallet_proxy_settings_layout)
proxySettingsLayout = findViewById(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)
accountSettingsConstraintLayout = findViewById(R.id.wallet_account_settings_layout)
addAccountButton = findViewById(R.id.add_account_button)
accountsRecyclerView = findViewById(R.id.accounts_recycler_view)
val cachedProxyAddress = ProxyService.instance?.proxyAddress ?: return
val cachedProxyPort = ProxyService.instance?.proxyPort ?: return
val cachedUsingProxy = ProxyService.instance?.usingProxy == true
val cachedUsingBundledTor = ProxyService.instance?.useBundledTor == true
val isMultiWalletMode = PreferenceUtils.isMultiWalletMode(applicationContext)
val isMultiAccountMode = PreferenceUtils.isMultiAccountMode(applicationContext)
val isStreetMode = PreferenceUtils.isStreetMode(applicationContext)
val isAllowFeeOverride = PreferenceUtils.isAllowFeeOverride(applicationContext)
val isUseProxy = PreferenceUtils.isUseProxy(applicationContext)
// walletProxyPortEditText.isEnabled = !cachedUsingBundledTor
// walletProxyAddressEditText.isEnabled = !cachedUsingBundledTor
proxySettingsLayout.visibility = View.VISIBLE
accountAdapter = AccountAdapter(emptyList(), null, isStreetMode, object : AccountAdapter.AccountAdapterListener {
override fun onAccountSelected(account: Account) {
walletService?.setAccount(account.index)
}
streetModeSwitch.isChecked = PreferenceUtils.isStreetMode(applicationContext)
allowFeeOverrideSwitch.isChecked = PreferenceUtils.isAllowFeeOverride(applicationContext)
useBundledTor.isChecked = cachedUsingBundledTor
proxySwitch.isChecked = cachedUsingProxy
updateProxy(cachedProxyAddress, cachedProxyPort)
override fun onAccountEditLabel(account: Account) {
val dialog = EditAccountLabelBottomSheetDialog(account.label) {
walletService?.setAccountLabel(account.index, it)
}
dialog.show(supportFragmentManager, "edit_account_dialog")
}
})
accountsRecyclerView.layoutManager = LinearLayoutManager(this)
accountsRecyclerView.adapter = accountAdapter
if (isMultiAccountMode) {
accountSettingsConstraintLayout.visibility = View.VISIBLE
}
multiWalletModeSwitch.isChecked = isMultiWalletMode
multiAccountModeSwitch.isChecked = isMultiAccountMode
streetModeSwitch.isChecked = isStreetMode
allowFeeOverrideSwitch.isChecked = isAllowFeeOverride
proxySwitch.isChecked = isUseProxy
walletProxyAddressEditText.setText(PreferenceUtils.getProxy(this) ?: "")
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() {
multiWalletModeSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PreferenceUtils.setMultiWalletMode(applicationContext, b)
}
multiAccountModeSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PreferenceUtils.setMultiAccountMode(applicationContext, b)
if (b) {
accountSettingsConstraintLayout.visibility = View.VISIBLE
} else {
accountSettingsConstraintLayout.visibility = View.GONE
}
}
streetModeSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PreferenceUtils.setStreetMode(applicationContext, b)
}
allowFeeOverrideSwitch.setOnCheckedChangeListener { _: CompoundButton?, b: Boolean ->
PreferenceUtils.setAllowFeeOverride(applicationContext, b)
}
proxySwitch.setOnCheckedChangeListener { _, b: Boolean ->
PreferenceUtils.setUseProxy(applicationContext, b)
}
selectNodeButton.setOnClickListener {
val nodes = PreferenceUtils.getOrSetDefaultNodes(this, DefaultNode.defaultNodes())
@ -153,33 +158,26 @@ class SettingsActivity : AppCompatActivity(), WalletServiceObserver {
dialog.show(supportFragmentManager, "node_selection_dialog")
}
useBundledTor.setOnCheckedChangeListener { _, isChecked ->
viewModel.setUseBundledTor(isChecked)
}
displaySeedButton.setOnClickListener {
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)
val passwordDialog = PasswordBottomSheetDialog(walletService!!.getWalletOrThrow().info.path, object : PasswordBottomSheetDialog.Listener {
override fun onCorrectPasswordSubmitted(self: PasswordBottomSheetDialog, password: String) {
self.dismiss()
displaySeedDialog()
}
})
passwordDialog.show(supportFragmentManager, "password_dialog")
}
displayUtxosButton.setOnClickListener {
displayEnotesButton.setOnClickListener {
startActivity(Intent(this, EnotesActivity::class.java))
}
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)
PreferenceUtils.setProxy(applicationContext, proxyAddress)
if (proxySwitch.isChecked) {
Toast.makeText(this, "Activating proxy", Toast.LENGTH_SHORT).show()
walletService?.setProxy(proxy)
walletService?.setProxy(proxyAddress)
} else {
if (walletService?.getProxy() != "") {
Toast.makeText(this, "Deactivating proxy", Toast.LENGTH_SHORT).show()
@ -187,70 +185,56 @@ class SettingsActivity : AppCompatActivity(), WalletServiceObserver {
}
}
}
addAccountButton.setOnClickListener {
Toast.makeText(this, "Creating new account, please wait..", Toast.LENGTH_SHORT).show()
walletService?.createAccount()
}
}
private fun bindObservers() {
viewModel.useProxy.observe(this) { useProxy ->
useBundledTor.isEnabled = useProxy
override fun onWalletServiceBound(walletService: WalletService) {
updateAccountAdapter()
}
// Utils.refreshProxy(walletProxyAddressEditText.text.toString(), walletProxyPortEditText.text.toString())
override fun onAccountCreated() {
runOnUiThread {
updateAccountAdapter()
Toast.makeText(this, "New account created", Toast.LENGTH_SHORT).show()
}
}
viewModel.useBundledTor.observe(this) { isChecked ->
// TODO: bundled tor support
override fun onAccountSet(index: Int) {
runOnUiThread {
updateAccountAdapter()
Toast.makeText(this, "Account changed to #${index}", Toast.LENGTH_SHORT).show()
}
}
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
val indicatorCircle =
findViewById<CircularProgressIndicator>(R.id.settings_tor_loading_progressindicator)
val torIcon = findViewById<ImageView>(R.id.settings_tor_icon)
override fun onAccountLabelChanged(index: Int, label: String) {
runOnUiThread {
updateAccountAdapter()
}
}
samouraiTorManager?.getTorStateLiveData()?.observe(this) { state ->
samouraiTorManager.getProxy()?.address()?.let { socketAddress ->
if (socketAddress.toString().isEmpty()) return@let
if (viewModel.useProxy.value == true && viewModel.useBundledTor.value == true) {
torIcon?.visibility = View.VISIBLE
indicatorCircle?.visibility = View.INVISIBLE
val proxyString = socketAddress.toString().substring(1)
val address = proxyString.split(":")[0]
val port = proxyString.split(":")[1]
updateProxy(address, port)
}
}
indicatorCircle?.isIndeterminate = state.progressIndicator == 0
indicatorCircle?.progress = state.progressIndicator
when (state.state) {
EnumTorState.OFF -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.INVISIBLE
}
EnumTorState.STARTING, EnumTorState.STOPPING -> {
torIcon?.visibility = View.INVISIBLE
indicatorCircle?.visibility = View.VISIBLE
}
else -> {}
private fun updateAccountAdapter() {
walletService?.getWallet()?.let { wallet ->
val accounts = (0 until wallet.getNumAccounts()).map {
val index = it
val label = wallet.getAccountLabel(index)
val balance = Balance(0, 0, 0, 0)
Account(index, label, balance)
}
accountAdapter.setAccounts(accounts)
accountAdapter.setSelectedAccount(accounts[wallet.getAccountIndex()])
}
}
private fun updateProxy(address: String, port: String) {
walletProxyPortEditText.setText(port)
walletProxyAddressEditText.setText(address)
// Utils.refreshProxy(address, port)
}
private fun displaySeedDialog(password: String) {
private fun displaySeedDialog() {
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)
val informationDialog = WalletKeysBottomSheetDialog(wallet, privateViewKey, restoreHeight)
informationDialog.show(supportFragmentManager, "information_seed_dialog")
}
@ -272,39 +256,3 @@ class SettingsActivity : AppCompatActivity(), WalletServiceObserver {
}
}
}
class SettingsViewModel : ViewModel() {
private val _useProxy = MutableLiveData(false)
val useProxy: LiveData<Boolean> = _useProxy
private val _useBundledTor = MutableLiveData(false)
val useBundledTor: LiveData<Boolean> = _useBundledTor
init {
_useProxy.value = ProxyService.instance?.usingProxy
_useBundledTor.value = ProxyService.instance?.useBundledTor
}
fun setUseProxy(use: Boolean) {
_useProxy.value = use
ProxyService.instance?.usingProxy = use
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (use && ProxyService.instance?.useBundledTor == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
fun setUseBundledTor(use: Boolean) {
_useBundledTor.value = use
ProxyService.instance?.useBundledTor = use
val samouraiTorManager = ProxyService.instance?.samouraiTorManager
if (use && ProxyService.instance?.usingProxy == true) {
samouraiTorManager?.start()
} else {
samouraiTorManager?.stop()
}
}
}

View File

@ -2,73 +2,45 @@ package net.mynero.wallet
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import net.mynero.wallet.fragment.dialog.PasswordBottomSheetDialog
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.UriData
import net.mynero.wallet.util.Helper
import net.mynero.wallet.util.PreferenceUtils
import net.mynero.wallet.util.acitivity.WalletOpeningActivity
import timber.log.Timber
import java.io.File
class StartActivity : AppCompatActivity() {
private var uriData: UriData? = null
private val startPasswordActivityForOpeningWallet = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val walletName = result.data?.extras?.getString(Constants.EXTRA_WALLET_NAME)
val walletPassword = result.data?.extras?.getString(Constants.EXTRA_WALLET_PASSWORD)
if (walletName != null && walletPassword != null) {
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()
}
}
}
class StartActivity : WalletOpeningActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent.data?.let { uriData = UriData.parse(it.toString()) }
// TODO: multiple wallets support
val walletName = Constants.DEFAULT_WALLET_NAME
val walletKeysFile = File(applicationInfo.dataDir, "$walletName.keys")
if (walletKeysFile.exists()) {
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))
setVisible(false)
val isMultiWalletMode = PreferenceUtils.isMultiWalletMode(this)
val walletPaths = WalletManager.instance.findWallets(Helper.getWalletRoot(this).absolutePath)
if (walletPaths.isEmpty()) {
Timber.d("No wallets found, launching onboarding activity")
val onboardingActivityIntent = Intent(this, OnboardingActivity::class.java)
onboardingActivityIntent.data = intent.data
onboardingActivityIntent.putExtra(Constants.EXTRA_PREVENT_GOING_BACK, true)
startActivity(onboardingActivityIntent)
finish()
} else if (isMultiWalletMode) {
Timber.d("${walletPaths.size} wallets found, launching wallet activity")
val walletActivityIntent = Intent(this, WalletActivity::class.java)
walletActivityIntent.data = intent.data
startActivity(walletActivityIntent)
finish()
return
}
}
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
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
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)
uriData!!.amount?.let { sendIntent.putExtra(Constants.EXTRA_SEND_AMOUNT, it) }
startActivity(sendIntent)
Timber.d("${walletPaths.size} wallets found but multi-wallet mode is not enabled, asking for password to open a wallet")
val walletPath = walletPaths.find { it.endsWith(Constants.DEFAULT_WALLET_NAME) } ?: walletPaths.first()
val passwordDialog = PasswordBottomSheetDialog(walletPath, object : PasswordBottomSheetDialog.Listener {
override fun onCorrectPasswordSubmitted(self: PasswordBottomSheetDialog, password: String) {
self.dismiss()
openWallet(walletPath, password)
}
})
passwordDialog.show(supportFragmentManager, "password_dialog")
}
finish()
}
}
}

View File

@ -1,36 +1,30 @@
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.ImageButton
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 net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.service.wallet.WalletServiceObserver
import net.mynero.wallet.model.Wallet
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 net.mynero.wallet.util.acitivity.MoneroActivity
import java.util.Calendar
import java.util.Date
import java.util.Objects
class TransactionActivity : AppCompatActivity(), WalletServiceObserver {
class TransactionActivity : MoneroActivity() {
private val viewModel: TransactionActivityViewModel by viewModels()
private var walletService: WalletService? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_transaction)
@ -49,19 +43,6 @@ class TransactionActivity : AppCompatActivity(), WalletServiceObserver {
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() {
@ -162,14 +143,25 @@ class TransactionActivity : AppCompatActivity(), WalletServiceObserver {
}
}
override fun onWalletServiceBound(walletService: WalletService) {
walletService.getWallet()?.let { wallet ->
updateState(wallet)
}
}
private fun getDateTime(time: Long): String {
return DateHelper.DATETIME_FORMATTER.format(Date(time * 1000))
}
override fun onWalletHistoryRefreshed(transactions: List<TransactionInfo>) {
private fun updateState(wallet: Wallet) {
val transactions = wallet.getHistory()
val newTransactionInfo = transactions.find { it.hash != null && it.hash == viewModel.txHash }
viewModel.updateTransactionInfo(newTransactionInfo)
}
override fun onWalletUpdated(wallet: Wallet) {
updateState(wallet)
}
}
internal class TransactionActivityViewModel : ViewModel() {

View File

@ -0,0 +1,48 @@
package net.mynero.wallet
import WalletAdapter
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.fragment.dialog.PasswordBottomSheetDialog
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Helper
import net.mynero.wallet.util.acitivity.WalletOpeningActivity
class WalletActivity : WalletOpeningActivity() {
private lateinit var createWalletButton: Button
private lateinit var walletRecyclerView: RecyclerView
private lateinit var adapter: WalletAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_wallet)
createWalletButton = findViewById(R.id.create_or_import_wallet)
walletRecyclerView = findViewById(R.id.wallet_list_recyclerview)
val walletPaths = WalletManager.instance.findWallets(Helper.getWalletRoot(this).absolutePath)
adapter = WalletAdapter(walletPaths, object : WalletAdapter.AccountAdapterListener {
override fun onWalletSelected(walletPath: String) {
val passwordDialog = PasswordBottomSheetDialog(walletPath, object : PasswordBottomSheetDialog.Listener {
override fun onCorrectPasswordSubmitted(self: PasswordBottomSheetDialog, password: String) {
self.dismiss()
openWallet(walletPath, password)
}
})
passwordDialog.show(supportFragmentManager, "password_dialog")
}
})
walletRecyclerView.layoutManager = LinearLayoutManager(this)
walletRecyclerView.adapter = adapter
createWalletButton.setOnClickListener {
val onboardingActivityIntent = Intent(this, OnboardingActivity::class.java)
onboardingActivityIntent.data = intent.data
startActivity(onboardingActivityIntent)
}
}
}

View File

@ -0,0 +1,84 @@
package net.mynero.wallet.adapter
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.R
import net.mynero.wallet.model.Account
class AccountAdapter(
private var accounts: List<Account>,
private var selectedAccount: Account?,
private var streetMode: Boolean,
private val listener: AccountAdapterListener
) : RecyclerView.Adapter<AccountAdapter.ViewHolder>() {
fun setAccounts(accounts: List<Account>) {
this.accounts = accounts
notifyDataSetChanged()
}
fun setSelectedAccount(selectedAccount: Account?) {
this.selectedAccount = selectedAccount
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.account_item, parent, false)
return ViewHolder(listener, view, streetMode)
}
override fun getItemCount(): Int {
return accounts.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(accounts[position], accounts[position] == selectedAccount)
}
interface AccountAdapterListener {
fun onAccountSelected(account: Account)
fun onAccountEditLabel(account: Account)
}
class ViewHolder(private val listener: AccountAdapterListener, view: View, private val streetMode: Boolean) : RecyclerView.ViewHolder(view) {
private lateinit var addressTextView: TextView
private lateinit var addressLabelTextView: TextView
private lateinit var addressAmountTextView: TextView
private lateinit var account: Account
fun bind(
account: Account,
isSelected: Boolean
) {
this.account = account
addressTextView = itemView.findViewById(R.id.account_item_account_textview)
addressLabelTextView = itemView.findViewById(R.id.address_label_textview)
addressAmountTextView = itemView.findViewById(R.id.address_amount_textview)
addressTextView.text = account.label
addressLabelTextView.text = account.index.toString()
addressAmountTextView.text = account.balance.total.toString()
if (isSelected) {
addressTextView.setTypeface(null, Typeface.BOLD)
} else {
addressTextView.setTypeface(null, Typeface.NORMAL)
}
itemView.setOnClickListener {
listener.onAccountSelected(account)
}
itemView.setOnLongClickListener {
listener.onAccountEditLabel(account)
return@setOnLongClickListener true
}
}
}
}

View File

@ -23,48 +23,44 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.R
import net.mynero.wallet.model.CoinsInfo
import net.mynero.wallet.model.Enote
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.util.Constants
class EnotesAdapter(
private var enotes: List<CoinsInfo>,
private var enotes: List<Enote>,
private val streetMode: Boolean,
private val listener: EnotesAdapterListener
) : RecyclerView.Adapter<EnotesAdapter.ViewHolder>() {
private val selectedEnotes: MutableMap<String, CoinsInfo> = HashMap()
private val selectedEnotes: MutableMap<Long, Enote> = HashMap()
private var editing = false
fun submitList(enotes: List<CoinsInfo>) {
this.enotes = enotes
notifyDataSetChanged()
fun submitList(enotes: List<Enote>) {
if (this.enotes != enotes) {
this.enotes = enotes
notifyDataSetChanged()
}
}
fun getSelectedEnotes(): Map<String, CoinsInfo> = selectedEnotes
fun getSelectedEnotes(): Map<Long, Enote> = selectedEnotes
fun deselectEnote(coinsInfo: CoinsInfo) {
selectedEnotes.remove(coinsInfo.pubKey)
if (selectedEnotes.size == 0) {
fun deselectEnote(enote: Enote) {
selectedEnotes.remove(enote.idx)
if (selectedEnotes.isEmpty()) {
editing = false
}
notifyDataSetChanged()
}
fun selectEnote(coinsInfo: CoinsInfo) {
fun selectEnote(enote: Enote) {
editing = true
selectedEnotes[coinsInfo.pubKey!!] = coinsInfo
selectedEnotes[enote.idx] = enote
notifyDataSetChanged()
}
operator fun contains(coinsInfo: CoinsInfo): Boolean {
return selectedEnotes.containsKey(coinsInfo.pubKey)
}
fun clear() {
selectedEnotes.clear()
editing = false
notifyDataSetChanged()
operator fun contains(enote: Enote): Boolean {
return selectedEnotes.containsKey(enote.idx)
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
@ -83,12 +79,12 @@ class EnotesAdapter(
}
interface EnotesAdapterListener {
fun onEnoteSelected(coinsInfo: CoinsInfo)
fun onEnoteSelected(enote: Enote)
}
class ViewHolder(private val listener: EnotesAdapterListener?, view: View, private val streetMode: Boolean) :
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 enote: Enote? = null
private var editing = false
init {
@ -98,12 +94,12 @@ class EnotesAdapter(
fun bind(
editing: Boolean,
coinsInfo: CoinsInfo,
selectedEnotes: Map<String, CoinsInfo>
enote: Enote,
selectedEnotes: Map<Long, Enote>
) {
this.editing = editing
this.coinsInfo = coinsInfo
val selected = selectedEnotes.containsKey(coinsInfo.pubKey)
this.enote = enote
val selected = selectedEnotes.containsKey(enote.idx)
val pubKeyTextView = itemView.findViewById<TextView>(R.id.utxo_pub_key_textview)
val amountTextView = itemView.findViewById<TextView>(R.id.utxo_amount_textview)
val addressTextView = itemView.findViewById<TextView>(R.id.utxo_address_textview)
@ -111,28 +107,28 @@ class EnotesAdapter(
val outpointTextView = itemView.findViewById<TextView>(R.id.utxo_outpoint_textview)
val balanceString =
if (streetMode) Constants.STREET_MODE_BALANCE else Wallet.getDisplayAmount(
coinsInfo.amount
enote.amount
)
amountTextView.text =
itemView.resources.getString(R.string.tx_amount_no_prefix, balanceString)
pubKeyTextView.text = coinsInfo.pubKey
addressTextView.text = coinsInfo.address
pubKeyTextView.text = enote.pubKey
addressTextView.text = enote.address
globalIdxTextView.text =
itemView.resources.getString(
R.string.global_index_text,
coinsInfo.globalOutputIndex
enote.globalOutputIndex
)
outpointTextView.text = itemView.resources.getString(
R.string.outpoint_text,
coinsInfo.hash + ":" + coinsInfo.localOutputIndex
enote.hash + ":" + enote.localOutputIndex
)
if (selected) {
itemView.backgroundTintList =
ContextCompat.getColorStateList(itemView.context, R.color.oled_colorSecondary)
} else if (coinsInfo.isFrozen) {
} else if (enote.isFrozen) {
itemView.backgroundTintList =
ContextCompat.getColorStateList(itemView.context, R.color.oled_frozen_utxo)
} else if (!coinsInfo.isUnlocked) {
} else if (!enote.isUnlocked) {
itemView.backgroundTintList =
ContextCompat.getColorStateList(itemView.context, R.color.oled_locked_utxo)
} else {
@ -146,17 +142,17 @@ class EnotesAdapter(
override fun onClick(view: View) {
if (!editing) return
val unlocked = coinsInfo?.isUnlocked == true
val unlocked = enote?.isUnlocked == true
if (unlocked) {
coinsInfo?.let { listener?.onEnoteSelected(it) }
enote?.let { listener.onEnoteSelected(it) }
}
}
override fun onLongClick(view: View): Boolean {
if (editing) return false
val unlocked = coinsInfo?.isUnlocked == true
val unlocked = enote?.isUnlocked == true
if (unlocked) {
coinsInfo?.let { listener?.onEnoteSelected(it) }
enote?.let { listener.onEnoteSelected(it) }
}
return unlocked
}

View File

@ -25,13 +25,12 @@ import net.mynero.wallet.R
import net.mynero.wallet.data.Subaddress
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?
private val listener: SubaddressAdapterListener
): RecyclerView.Adapter<SubaddressAdapter.ViewHolder>() {
fun submitAddresses(addresses: List<Subaddress>) {
@ -44,21 +43,17 @@ class SubaddressAdapter(
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.address_item, viewGroup, false)
return ViewHolder(view, listener)
}
// 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, streetMode)
}
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount(): Int {
return addresses.size
}
@ -68,19 +63,17 @@ class SubaddressAdapter(
fun onSubaddressEditLabel(subaddress: Subaddress)
}
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
class ViewHolder(view: View, val listener: SubaddressAdapterListener?) :
RecyclerView.ViewHolder(view) {
class ViewHolder(view: View, val listener: SubaddressAdapterListener) : RecyclerView.ViewHolder(view) {
private lateinit var addressTextView: TextView
private lateinit var addressLabelTextView: TextView
private lateinit var addressAmountTextView: TextView
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)
val addressAmountTextView =
itemView.findViewById<TextView>(R.id.address_amount_textview)
addressTextView = itemView.findViewById(R.id.address_item_address_textview)
addressLabelTextView = itemView.findViewById(R.id.address_label_textview)
addressAmountTextView = itemView.findViewById(R.id.address_amount_textview)
addressTextView.text = subaddress.address
if (isSelected) {
addressTextView.setTypeface(null, Typeface.BOLD)
@ -94,7 +87,6 @@ class SubaddressAdapter(
)
addressLabelTextView.text = label.ifEmpty { address }
val amount = subaddress.amount
Timber.e("amount = $amount")
if (amount > 0) {
if (streetMode) {
addressAmountTextView.text = itemView.context.getString(
@ -108,9 +100,9 @@ class SubaddressAdapter(
)
}
} else addressAmountTextView.text = ""
itemView.setOnClickListener { listener?.onSubaddressSelected(subaddress) }
itemView.setOnClickListener { listener.onSubaddressSelected(subaddress) }
itemView.setOnLongClickListener { _: View? ->
listener?.onSubaddressEditLabel(subaddress)
listener.onSubaddressEditLabel(subaddress)
true
}
}

View File

@ -15,7 +15,6 @@
*/
package net.mynero.wallet.adapter
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -28,19 +27,22 @@ import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.DateHelper
import net.mynero.wallet.util.Helper
import net.mynero.wallet.util.ThemeHelper
import timber.log.Timber
import java.util.Calendar
import java.util.Date
class TransactionInfoAdapter(
private var streetMode: Boolean,
val listener: TxInfoAdapterListener,
private val listener: TxInfoAdapterListener,
) : RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder>() {
private var localDataSet: List<TransactionInfo> = emptyList()
fun submitList(dataSet: List<TransactionInfo>) {
localDataSet = dataSet
notifyDataSetChanged()
if (localDataSet != dataSet) {
localDataSet = dataSet
notifyDataSetChanged()
}
}
fun submitStreetMode(newStreetMode: Boolean) {
@ -50,21 +52,17 @@ class TransactionInfoAdapter(
}
}
// 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.transaction_history_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 tx = localDataSet[position]
viewHolder.bind(tx, streetMode)
}
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount(): Int {
return localDataSet.size
}
@ -73,46 +71,42 @@ class TransactionInfoAdapter(
fun onClickTransaction(txInfo: TransactionInfo)
}
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder).
*/
class ViewHolder(val listener: TxInfoAdapterListener?, view: View) :
RecyclerView.ViewHolder(view) {
private val outboundColour: Int
private val inboundColour: Int
private val pendingColour: Int
private val failedColour: Int
private val selfColour: Int
private var amountTextView: TextView? = null
class ViewHolder(val listener: TxInfoAdapterListener?, view: View) : RecyclerView.ViewHolder(view) {
private val outboundColour: Int = ThemeHelper.getThemedColor(view.context, R.attr.negativeColor)
private val inboundColour: Int = ThemeHelper.getThemedColor(view.context, R.attr.positiveColor)
private val pendingColour: Int = ThemeHelper.getThemedColor(view.context, R.attr.neutralColor)
private val failedColour: Int = ThemeHelper.getThemedColor(view.context, R.attr.neutralColor)
private lateinit var amountTextView: TextView
private lateinit var confirmationsTextView: TextView
private lateinit var confirmationsProgressBar: CircularProgressIndicator
private lateinit var txFailedTextView: TextView
private lateinit var txDateTimeTextView: TextView
init {
inboundColour = ThemeHelper.getThemedColor(view.context, R.attr.positiveColor)
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, streetMode: Boolean) {
confirmationsTextView = itemView.findViewById(R.id.tvConfirmations)
confirmationsProgressBar = itemView.findViewById(R.id.pbConfirmations)
amountTextView = itemView.findViewById(R.id.tx_amount)
txFailedTextView = itemView.findViewById(R.id.tx_failed)
txDateTimeTextView = itemView.findViewById(R.id.tx_datetime)
val displayAmount =
if (streetMode) Constants.STREET_MODE_BALANCE else Helper.getDisplayAmount(
txInfo.amount,
Helper.DISPLAY_DIGITS_INFO
)
val confirmationsTextView = itemView.findViewById<TextView>(R.id.tvConfirmations)
val confirmationsProgressBar =
itemView.findViewById<CircularProgressIndicator>(R.id.pbConfirmations)
confirmationsProgressBar.max = TransactionInfo.CONFIRMATION
amountTextView = itemView.findViewById(R.id.tx_amount)
itemView.findViewById<View>(R.id.tx_failed).visibility = View.GONE
txFailedTextView.visibility = View.GONE
if (txInfo.isFailed) {
amountTextView?.text =
itemView.context.getString(R.string.tx_list_amount_negative, displayAmount)
itemView.findViewById<View>(R.id.tx_failed).visibility = View.VISIBLE
amountTextView.text = itemView.context.getString(R.string.tx_list_amount_negative, displayAmount)
txFailedTextView.visibility = View.VISIBLE
setTxColour(failedColour)
confirmationsTextView.visibility = View.GONE
confirmationsProgressBar.visibility = View.GONE
@ -138,9 +132,6 @@ class TransactionInfoAdapter(
}
} else {
setTxColour(outboundColour)
if (txInfo.amount == 0L) {
setTxColour(Color.CYAN)
}
if (!txInfo.isConfirmed) {
confirmationsProgressBar.visibility = View.VISIBLE
val confirmations = txInfo.confirmations.toInt()
@ -156,19 +147,18 @@ class TransactionInfoAdapter(
}
}
if (txInfo.direction == TransactionInfo.Direction.Direction_Out) {
amountTextView?.text =
amountTextView.text =
itemView.context.getString(R.string.tx_list_amount_negative, displayAmount)
} else {
amountTextView?.text =
amountTextView.text =
itemView.context.getString(R.string.tx_list_amount_positive, displayAmount)
}
(itemView.findViewById<View>(R.id.tx_datetime) as TextView).text =
getDateTime(txInfo.timestamp)
txDateTimeTextView.text = getDateTime(txInfo.timestamp)
itemView.setOnClickListener { _: View? -> listener?.onClickTransaction(txInfo) }
}
private fun setTxColour(clr: Int) {
amountTextView?.setTextColor(clr)
amountTextView.setTextColor(clr)
}
private fun getDateTime(time: Long): String {

View File

@ -0,0 +1,52 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import net.mynero.wallet.R
import kotlin.io.path.Path
import kotlin.io.path.name
class WalletAdapter(
private var walletPaths: List<String>,
private val listener: AccountAdapterListener
) : RecyclerView.Adapter<WalletAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.wallet_item, parent, false)
return ViewHolder(listener, view)
}
override fun getItemCount(): Int {
return walletPaths.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(walletPaths[position])
}
interface AccountAdapterListener {
fun onWalletSelected(walletPath: String)
}
class ViewHolder(private val listener: AccountAdapterListener, view: View) : RecyclerView.ViewHolder(view) {
private lateinit var walletNameTextView: TextView
private lateinit var walletPath: String
fun bind(
walletPath: String,
) {
this.walletPath = walletPath
walletNameTextView = itemView.findViewById(R.id.wallet_name_textview)
walletNameTextView.text = Path(walletPath).name
itemView.setOnClickListener {
listener.onWalletSelected(walletPath)
}
}
}
}

View File

@ -0,0 +1,48 @@
package net.mynero.wallet.fragment.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.mynero.wallet.R
import net.mynero.wallet.util.Helper.getClipBoardText
class EditAccountLabelBottomSheetDialog(
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_account_label, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
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))
}
saveLabelButton.setOnClickListener {
val label = labelEditText.text.toString()
onSave(label)
dismiss()
}
}
}

View File

@ -9,11 +9,7 @@ import android.widget.EditText
import android.widget.ImageButton
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.mynero.wallet.R
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Helper.getClipBoardText
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class EditAddressLabelBottomSheetDialog(
private val currentLabel: String,

View File

@ -0,0 +1,67 @@
package net.mynero.wallet.fragment.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import android.widget.Toast
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.mynero.wallet.R
import net.mynero.wallet.model.WalletManager
import net.mynero.wallet.util.Helper.getClipBoardText
class PasswordBottomSheetDialog(
private val walletPath: String,
private val listener: Listener,
) : BottomSheetDialogFragment() {
private lateinit var passwordEditText: EditText
private lateinit var unlockButton: Button
private lateinit var pastePasswordImageButton: ImageButton
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.activity_password, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
passwordEditText = view.findViewById(R.id.wallet_password_edittext)
unlockButton = view.findViewById(R.id.unlock_wallet_button)
pastePasswordImageButton = view.findViewById(R.id.paste_password_imagebutton)
bindListeners()
}
private fun bindListeners() {
unlockButton.setOnClickListener {
onUnlockClicked(passwordEditText.text.toString())
}
pastePasswordImageButton.setOnClickListener {
passwordEditText.setText(getClipBoardText(requireView().context))
}
}
private fun onUnlockClicked(walletPassword: String) {
if (checkPassword(walletPassword)) {
listener.onCorrectPasswordSubmitted(this, walletPassword)
} else {
Toast.makeText(context, R.string.bad_password, Toast.LENGTH_SHORT).show()
}
}
private fun checkPassword(walletPassword: String): Boolean {
return WalletManager.instance.verifyWalletPasswordOnly(
"$walletPath.keys",
walletPassword
)
}
interface Listener {
fun onCorrectPasswordSubmitted(self: PasswordBottomSheetDialog, password: String)
}
}

View File

@ -6,41 +6,85 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import net.mynero.wallet.R
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.util.Helper.clipBoardCopy
class WalletKeysBottomSheetDialog(
private val usesOffset: Boolean,
private val seed: String,
private val wallet: Wallet,
private val privateViewKey: String,
private val restoreHeight: Long,
) : BottomSheetDialogFragment() {
private val viewModel: WalletKeysBottomSheetDialogViewModel by viewModels()
private lateinit var encryptSeedSwitch: SwitchCompat
private lateinit var copyViewKeyImageButton: ImageButton
private lateinit var seedTextView: TextView
private lateinit var viewKeyTextView: TextView
private lateinit var restoreHeightTextView: TextView
private lateinit var walletSeedOffset: TextView
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.wallet_keys_bottom_sheet_dialog, null)
return inflater.inflate(R.layout.wallet_keys_bottom_sheet_dialog, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val copyViewKeyImageButton = view.findViewById<ImageButton>(R.id.copy_viewkey_imagebutton)
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)
if (usesOffset) {
view.findViewById<View>(R.id.wallet_seed_offset_textview).visibility = View.VISIBLE
}
informationTextView.text = seed
encryptSeedSwitch = view.findViewById(R.id.encrypt_seed_switch)
copyViewKeyImageButton = view.findViewById(R.id.copy_viewkey_imagebutton)
seedTextView = view.findViewById(R.id.seed_textview)
viewKeyTextView = view.findViewById(R.id.viewkey_textview)
restoreHeightTextView = view.findViewById(R.id.restore_height_textview)
walletSeedOffset = view.findViewById(R.id.wallet_seed_offset_textview)
viewKeyTextView.text = privateViewKey
restoreHeightTextView.text = "${restoreHeight}"
restoreHeightTextView.text = "$restoreHeight"
bindListeners()
bindObservers()
}
private fun bindListeners() {
encryptSeedSwitch.setOnCheckedChangeListener { _, checked ->
viewModel.setUseOffset(checked)
}
copyViewKeyImageButton.setOnClickListener {
clipBoardCopy(
context, "private view-key", privateViewKey
)
}
}
private fun bindObservers() {
viewModel.useOffset.observe(this) { useOffset ->
if (useOffset) {
seedTextView.text = wallet.getSeed(wallet.info.password)
walletSeedOffset.visibility = View.VISIBLE
} else {
seedTextView.text = wallet.getSeed("")
walletSeedOffset.visibility = View.INVISIBLE
}
}
}
}
class WalletKeysBottomSheetDialogViewModel : ViewModel() {
private val _useOffset = MutableLiveData(false)
val useOffset: LiveData<Boolean> = _useOffset
fun setUseOffset(useOffset: Boolean) {
_useOffset.value = useOffset
}
}

View File

@ -3,7 +3,7 @@ package net.mynero.wallet.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
fun <T1, T2, T3, T4, T5, T6, T7, S> combineLiveDatas(
source1: LiveData<T1>,
source2: LiveData<T2>,
source3: LiveData<T3>,
@ -11,9 +11,7 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
source5: LiveData<T5>,
source6: LiveData<T6>,
source7: LiveData<T7>,
source8: LiveData<T8>,
source9: LiveData<T9>?,
func: (T1?, T2?, T3?, T4?, T5?, T6?, T7?, T8?, T9?) -> S?
func: (T1?, T2?, T3?, T4?, T5?, T6?, T7?) -> S?
): LiveData<S> {
val result = MediatorLiveData<S>()
result.addSource(source1) {
@ -25,8 +23,6 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -39,8 +35,6 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -53,8 +47,6 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -67,8 +59,6 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -81,8 +71,6 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -95,8 +83,6 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value,
source9?.value
)?.run { result.value = this }
}
@ -109,41 +95,8 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, S> combineLiveDatas(
source5.value,
source6.value,
source7.value,
source8.value,
source9?.value
)?.run { result.value = this }
}
result.addSource(source8) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value,
source9?.value
)?.run { result.value = this }
}
source9?.let { src9 ->
result.addSource(src9) {
func(
source1.value,
source2.value,
source3.value,
source4.value,
source5.value,
source6.value,
source7.value,
source8.value,
src9.value
)?.run { result.value = this }
}
}
return result
}

View File

@ -0,0 +1,3 @@
package net.mynero.wallet.model
data class Account(val index: Int, val label: String, val balance: Balance)

View File

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

View File

@ -1,94 +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.model
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
class CoinsInfo : Parcelable, Comparable<CoinsInfo> {
var globalOutputIndex: Long
var isSpent = false
var keyImage: String? = null
var amount: Long = 0
var hash: String? = null
var pubKey: String? = null
var isUnlocked = false
var localOutputIndex: Long = 0
var isFrozen = false
var address: String? = null
constructor(
globalOutputIndex: Long,
spent: Boolean,
keyImage: String?,
amount: Long,
hash: String?,
pubKey: String?,
unlocked: Boolean,
localOutputIndex: Long,
frozen: Boolean,
address: String?
) {
this.globalOutputIndex = globalOutputIndex
isSpent = spent
this.keyImage = keyImage
this.amount = amount
this.hash = hash
this.pubKey = pubKey
isUnlocked = unlocked
this.localOutputIndex = localOutputIndex
isFrozen = frozen
this.address = address
}
private constructor(`in`: Parcel) {
globalOutputIndex = `in`.readLong()
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(parcel: Parcel, i: Int) {
parcel.writeLong(globalOutputIndex)
}
override fun compareTo(other: CoinsInfo): Int {
val b1 = amount
val b2 = other.amount
return if (b1 > b2) {
-1
} else if (b1 < b2) {
1
} else {
other.hash?.let { hash?.compareTo(it) } ?: 0
}
}
companion object {
@JvmField
val CREATOR: Creator<CoinsInfo?> = object : Creator<CoinsInfo?> {
override fun createFromParcel(`in`: Parcel): CoinsInfo {
return CoinsInfo(`in`)
}
override fun newArray(size: Int): Array<CoinsInfo?> {
return arrayOfNulls(size)
}
}
}
}

View File

@ -15,19 +15,16 @@
*/
package net.mynero.wallet.model
import android.util.Log
class Coins(private val handle: Long) {
var all: List<CoinsInfo> = ArrayList()
private set
fun refresh() {
val transactionInfos = refreshJ()
Log.d("Coins.kt", "refresh size=${transactionInfos.size}")
all = transactionInfos
}
external fun setFrozen(publicKey: String?, frozen: Boolean)
external fun refreshJ(): List<CoinsInfo>
external fun getCount(): Int
}
data class Enote(
val idx: Long,
val globalOutputIndex: Long,
val isSpent: Boolean,
val keyImage: String?,
val amount: Long,
val hash: String?,
val pubKey: String?,
val isUnlocked: Boolean,
val localOutputIndex: Long,
val isFrozen: Boolean,
val address: String?
)

View File

@ -1,8 +0,0 @@
package net.mynero.wallet.model
enum class EnumTorState {
STARTING,
ON,
STOPPING,
OFF
}

View File

@ -1,6 +0,0 @@
package net.mynero.wallet.model
class TorState {
var state: EnumTorState = EnumTorState.OFF
var progressIndicator: Int = 0
}

View File

@ -15,19 +15,10 @@
*/
package net.mynero.wallet.model
import android.util.Log
class TransactionHistory(private val handle: Long, var accountIndex: Int) {
class TransactionHistory(private val handle: Long) {
var all: List<TransactionInfo> = ArrayList()
private set
fun setAccountFor(wallet: Wallet) {
if (accountIndex != wallet.getAccountIndex()) {
accountIndex = wallet.getAccountIndex()
refreshWithNotes(wallet)
}
}
private fun loadNotes(wallet: Wallet) {
for (info in all) {
info.notes = wallet.getUserNote(info.hash)
@ -43,14 +34,6 @@ class TransactionHistory(private val handle: Long, var accountIndex: Int) {
private fun refresh() {
val transactionInfos = refreshJ()
Log.d("TransactionHistory.kt", "refresh size=${transactionInfos.size}")
val iterator = transactionInfos.iterator()
while (iterator.hasNext()) {
val info = iterator.next()
if (info.accountIndex != accountIndex) {
iterator.remove()
}
}
all = transactionInfos
}

View File

@ -15,30 +15,25 @@
*/
package net.mynero.wallet.model
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import net.mynero.wallet.data.Subaddress
class TransactionInfo : Parcelable, Comparable<TransactionInfo> {
var direction: Direction
var isPending: Boolean
var isFailed: Boolean
var amount: Long
var fee: Long
var blockheight: Long
var hash: String?
var timestamp: Long
var paymentId: String?
var accountIndex: Int
var addressIndex: Int
var confirmations: Long
var subaddressLabel: String?
var transfers: List<Transfer>? = listOf()
var txKey: String? = null
var notes: String? = null
data class TransactionInfo(
var direction: Direction,
var isPending: Boolean,
var isFailed: Boolean,
var amount: Long,
var fee: Long,
var blockheight: Long,
var hash: String?,
var timestamp: Long,
var paymentId: String?,
var accountIndex: Int,
var addressIndex: Int,
var confirmations: Long,
var subaddressLabel: String?,
var transfers: List<Transfer>? = listOf(),
var txKey: String? = null,
var notes: String? = null,
var address: String? = null
) {
constructor(
direction: Int,
@ -55,49 +50,22 @@ class TransactionInfo : Parcelable, Comparable<TransactionInfo> {
confirmations: Long,
subaddressLabel: String?,
transfers: List<Transfer>?
) {
this.direction = Direction.values()[direction]
this.isPending = isPending
this.isFailed = isFailed
this.amount = amount
this.fee = fee
this.blockheight = blockheight
this.hash = hash
this.timestamp = timestamp
this.paymentId = paymentId
this.accountIndex = accountIndex
this.addressIndex = addressIndex
this.confirmations = confirmations
this.subaddressLabel = subaddressLabel
this.transfers = transfers
}
private constructor(`in`: Parcel) {
direction = Direction.fromInteger(`in`.readInt())
isPending = `in`.readByte().toInt() != 0
isFailed = `in`.readByte().toInt() != 0
amount = `in`.readLong()
fee = `in`.readLong()
blockheight = `in`.readLong()
hash = `in`.readString()
timestamp = `in`.readLong()
paymentId = `in`.readString()
accountIndex = `in`.readInt()
addressIndex = `in`.readInt()
confirmations = `in`.readLong()
subaddressLabel = `in`.readString()
transfers?.toMutableList()?.let { transfers ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
`in`.readList(transfers, Transfer::class.java.classLoader, Transfer::class.java)
} else {
`in`.readList(transfers, Transfer::class.java.classLoader)
}
}
txKey = `in`.readString()
notes = `in`.readString()
address = `in`.readString()
}
) : this(
Direction.values()[direction],
isPending,
isFailed,
amount,
fee,
blockheight,
hash,
timestamp,
paymentId,
accountIndex,
addressIndex,
confirmations,
subaddressLabel,
transfers
)
val isConfirmed: Boolean
get() = confirmations >= CONFIRMATION
@ -106,44 +74,6 @@ class TransactionInfo : Parcelable, Comparable<TransactionInfo> {
return "$direction@$blockheight $amount"
}
override fun writeToParcel(out: Parcel, flags: Int) {
out.writeInt(direction.value)
out.writeByte((if (isPending) 1 else 0).toByte())
out.writeByte((if (isFailed) 1 else 0).toByte())
out.writeLong(amount)
out.writeLong(fee)
out.writeLong(blockheight)
out.writeString(hash)
out.writeLong(timestamp)
out.writeString(paymentId)
out.writeInt(accountIndex)
out.writeInt(addressIndex)
out.writeLong(confirmations)
out.writeString(subaddressLabel)
transfers?.let {
out.writeList(transfers)
}
out.writeString(txKey)
out.writeString(notes)
out.writeString(address)
}
override fun describeContents(): Int {
return 0
}
override fun compareTo(other: TransactionInfo): Int {
val b1 = timestamp
val b2 = other.timestamp
return if (b1 > b2) {
-1
} else if (b1 < b2) {
1
} else {
hash?.let { other.hash?.compareTo(it) } ?: 0
}
}
enum class Direction(val value: Int) {
Direction_In(0), Direction_Out(1);
@ -159,16 +89,5 @@ class TransactionInfo : Parcelable, Comparable<TransactionInfo> {
companion object {
const val CONFIRMATION = 10 // blocks
@JvmField
val CREATOR: Creator<TransactionInfo> = object : Creator<TransactionInfo> {
override fun createFromParcel(`in`: Parcel): TransactionInfo {
return TransactionInfo(`in`)
}
override fun newArray(size: Int): Array<TransactionInfo?> {
return arrayOfNulls(size)
}
}
}
}

View File

@ -15,40 +15,4 @@
*/
package net.mynero.wallet.model
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
class Transfer : Parcelable {
var amount: Long
var address: String?
constructor(amount: Long, address: String?) {
this.amount = amount
this.address = address
}
private constructor(`in`: Parcel) {
amount = `in`.readLong()
address = `in`.readString()
}
override fun writeToParcel(out: Parcel, flags: Int) {
out.writeLong(amount)
out.writeString(address)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Creator<Transfer> {
override fun createFromParcel(parcel: Parcel): Transfer {
return Transfer(parcel)
}
override fun newArray(size: Int): Array<Transfer?> {
return arrayOfNulls(size)
}
}
}
data class Transfer(val amount: Long, val address: String?)

View File

@ -19,40 +19,21 @@ 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 {
class Wallet(private val handle: Long, val info: WalletInfo, private var accountIndex: Int = 0) {
var isSynchronized = false
private var accountIndex = 0
private var handle: Long = 0
private var listenerHandle: Long = 0
private var pendingTransaction: PendingTransaction? = null
var history: TransactionHistory? = null
get() {
if (field == null) {
field = TransactionHistory(getHistoryJ(), accountIndex)
}
return field
}
private set
var coins: Coins? = null
get() {
if (field == null) {
field = Coins(getCoinsJ())
}
return field
}
private set
private var enotes: List<Enote>? = null
private var balances: Map<Int, Balance>? = null
private var history: Map<Int, List<TransactionInfo>>? = null
internal constructor(handle: Long) {
this.handle = handle
fun update() {
refreshEnotes()
refreshTransactionHistory()
}
internal constructor(handle: Long, accountIndex: Int) {
this.handle = handle
this.accountIndex = accountIndex
}
fun getAccountIndex(): Int {
return accountIndex
@ -61,9 +42,85 @@ class Wallet {
fun setAccountIndex(accountIndex: Int) {
Log.d("Wallet.kt", "setAccountIndex($accountIndex)")
this.accountIndex = accountIndex
history?.setAccountFor(this)
}
private external fun getEnotesJ(): List<Enote>
fun refreshEnotes() {
val enotes = getEnotesJ()
this.enotes = enotes
this.balances = calculateBalances(enotes)
}
fun getEnotes(): List<Enote> {
var enotes = enotes
if (enotes == null) {
enotes = getEnotesJ()
this.enotes = enotes
return enotes
} else {
return enotes
}
}
fun getBalances(): Map<Int, Balance> {
if (balances == null) {
refreshEnotes()
}
return balances!!
}
fun getBalance(): Balance = getBalances()[accountIndex] ?: Balance(0, 0, 0, 0)
fun refreshTransactionHistory() {
val transactionInfos = mutableMapOf<Int, MutableList<TransactionInfo>>()
getTransactionHistory().refreshJ().forEach {
transactionInfos.getOrPut(it.accountIndex) { mutableListOf() }.add(it)
}
this.history = transactionInfos
}
fun getHistories(): Map<Int, List<TransactionInfo>> {
if (history == null) {
refreshTransactionHistory()
}
return history!!
}
fun getHistory(): List<TransactionInfo> = getHistories()[accountIndex] ?: listOf()
private fun calculateBalances(enotes: List<Enote>): Map<Int, Balance> {
val addressToAccount = mutableMapOf<String, Int>()
for (accountIndex in 0 until getNumAccounts()) {
for (addressIndex in 0 until getNumSubaddresses(accountIndex)) {
addressToAccount[getSubaddress(accountIndex, addressIndex)] = accountIndex
}
}
val accountToBalance = mutableMapOf<Int, Balance>()
enotes.filter { !it.isSpent }.forEach { enote ->
enote.address?.let { address -> addressToAccount[address] }?.let { account ->
val balance = accountToBalance.getOrDefault(account, Balance(0, 0, 0, 0))
balance.total += enote.amount
if (enote.isFrozen) {
balance.frozen += enote.amount
} else if (enote.isUnlocked) {
balance.unlocked += enote.amount
} else {
balance.pending += enote.amount
}
accountToBalance[account] = balance
}
}
return accountToBalance
}
external fun freeze(idx: Long)
external fun thaw(idx: Long)
val name: String
get() = getPath()?.let { File(it).name }.toString()
@ -84,7 +141,7 @@ class Wallet {
private external fun statusWithErrorString(): Status
fun getTransactionHistory(): TransactionHistory {
return TransactionHistory(getHistoryJ(), accountIndex)
return TransactionHistory(getHistoryJ())
}
@Synchronized
@ -117,14 +174,14 @@ class Wallet {
fun getSubaddressObject(subAddressIndex: Int): Subaddress {
val subaddress = getSubaddressObject(accountIndex, subAddressIndex)
var amount: Long = 0
history?.let { history ->
for (info in history.all) {
if (info.addressIndex == subAddressIndex && info.direction == TransactionInfo.Direction.Direction_In) {
amount += info.amount
}
}
}
// history?.let { history ->
// for (info in history.all) {
// if (info.addressIndex == subAddressIndex && info.direction == TransactionInfo.Direction.Direction_In) {
// amount += info.amount
// }
// }
// }
//
subaddress.amount = amount
return subaddress
}
@ -308,23 +365,14 @@ class Wallet {
private external fun createSweepUnmixableTransactionJ(): Long
private external fun disposeTransaction(pendingTransaction: PendingTransaction?)
private external fun getHistoryJ(): Long
private external fun getCoinsJ(): Long
//virtual bool exportKeyImages(const std::string &filename) = 0;
//virtual bool importKeyImages(const std::string &filename) = 0;
//virtual TransactionHistory * history() const = 0;
fun refreshHistory() {
history?.refreshWithNotes(this)
}
fun refreshCoins() {
if (isSynchronized) {
coins?.refresh()
}
}
private external fun setListenerJ(listener: WalletListener): Long
fun setListener(listener: WalletListener) {
private external fun setListenerJ(listener: WalletListener?): Long
fun setListener(listener: WalletListener?) {
listenerHandle = setListenerJ(listener)
}
@ -334,23 +382,16 @@ class Wallet {
external fun getUserNote(txid: String?): String?
external fun getTxKey(txid: String?): String?
@JvmOverloads
external fun addAccount(label: String? = NEW_ACCOUNT_NAME)
var accountLabel: String?
external fun addAccount(label: String)
var accountLabel: String
get() = getAccountLabel(accountIndex)
//virtual std::string signMessage(const std::string &message) = 0;
set(label) {
setAccountLabel(accountIndex, label)
}
private fun getAccountLabel(accountIndex: Int): String {
var label = getSubaddressLabel(accountIndex, 0)
if (label == NEW_ACCOUNT_NAME) {
val address = getAddress(accountIndex)
val len = address.length
label = address.substring(0, 6) +
"\u2026" + address.substring(len - 6, len)
}
fun getAccountLabel(accountIndex: Int): String {
val label = getSubaddressLabel(accountIndex, 0)
return label
}
@ -359,13 +400,12 @@ class Wallet {
}
private external fun getSubaddressLabel(accountIndex: Int, addressIndex: Int): String
private fun setAccountLabel(accountIndex: Int, label: String?) {
fun setAccountLabel(accountIndex: Int, label: String?) {
setSubaddressLabel(accountIndex, 0, label)
}
fun setSubaddressLabel(addressIndex: Int, label: String?) {
setSubaddressLabel(accountIndex, addressIndex, label)
refreshHistory()
}
private external fun setSubaddressLabel(accountIndex: Int, addressIndex: Int, label: String?)
@ -420,7 +460,7 @@ class Wallet {
companion object {
const val SWEEP_ALL = Long.MAX_VALUE
private const val NEW_ACCOUNT_NAME = "Untitled account" // src/wallet/wallet2.cpp:941
const val NEW_ACCOUNT_NAME = "Untitled account" // src/wallet/wallet2.cpp:941
@JvmStatic
external fun getDisplayAmount(amount: Long): String
@ -438,12 +478,12 @@ class Wallet {
external fun isPaymentIdValid(payment_id: String): Boolean
fun isAddressValid(address: String): Boolean {
return WalletManager.instance?.networkType?.value?.let {
return WalletManager.instance.networkType.value.let {
isAddressValid(
address,
it
)
} == true
}
}
@JvmStatic
@ -455,4 +495,8 @@ class Wallet {
@JvmStatic
external fun getMaximumAllowedAmount(): Long
}
}
data class WalletInfo(val path: String, val name: String, val password: String) {
constructor(file: File, password: String) : this(file.absolutePath, file.name, password)
}

View File

@ -25,12 +25,10 @@ import java.util.Locale
class WalletManager {
val networkType = NetworkType.NetworkType_Mainnet
var proxy = ""
private set
fun createWallet(aFile: File, password: String, language: String, height: Long): Wallet {
val walletHandle = createWalletJ(aFile.absolutePath, password, language, networkType.value)
val wallet = Wallet(walletHandle)
val wallet = Wallet(walletHandle, WalletInfo(aFile, password))
if (wallet.status.isOk) {
// (Re-)Estimate restore height based on what we know
val oldHeight = wallet.getRestoreHeight()
@ -48,7 +46,7 @@ class WalletManager {
return wallet
}
//public native List<String> findWallets(String path); // this does not work - some error in boost
external fun findWallets(path: String): List<String> // this does not work - some error in boost
private external fun createWalletJ(
path: String,
password: String,
@ -69,7 +67,7 @@ class WalletManager {
language,
networkType.value
)
val wallet = Wallet(walletHandle)
val wallet = Wallet(walletHandle, WalletInfo(aFile, password))
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())
@ -87,7 +85,7 @@ class WalletManager {
//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)
val wallet = Wallet(walletHandle, WalletInfo(File(path), password))
return wallet
}
@ -102,7 +100,7 @@ class WalletManager {
mnemonic, offset,
networkType.value, restoreHeight
)
val wallet = Wallet(walletHandle)
val wallet = Wallet(walletHandle, WalletInfo(aFile, password))
return wallet
}
@ -121,7 +119,7 @@ class WalletManager {
mnemonic, offset,
networkType.value
)
val wallet = Wallet(walletHandle)
val wallet = Wallet(walletHandle, WalletInfo(aFile, password))
return wallet
}
@ -140,7 +138,7 @@ class WalletManager {
language, networkType.value, restoreHeight,
addressString, viewKeyString, spendKeyString
)
val wallet = Wallet(walletHandle)
val wallet = Wallet(walletHandle, WalletInfo(aFile, password))
return wallet
}
@ -198,12 +196,8 @@ class WalletManager {
external fun stopMining(): Boolean
external fun resolveOpenAlias(address: String?, dnssec_valid: Boolean): String?
fun setProxy(address: String): Boolean {
proxy = address
return setProxyJ(address)
}
private external fun setProxyJ(address: String?): Boolean
external fun setProxyJ(address: String?): Boolean
inner class WalletInfo(wallet: File) : Comparable<WalletInfo> {
private val path: File
private val name: String
@ -228,7 +222,10 @@ class WalletManager {
var LOGLEVEL_TRACE = 3
var LOGLEVEL_MAX = 4
val instance: WalletManager = WalletManager()
val instance: WalletManager
get() = synchronized(WalletManager::class.java) {
return@synchronized WalletManager()
}
fun addressPrefix(networkType: NetworkType): String {
return when (networkType) {

View File

@ -1,37 +0,0 @@
/*
* Copyright (C) 2006 The Android Open Source Project
* 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.service
import android.os.Looper
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()
}
override fun run() {
Looper.prepare();
val looper = Looper.myLooper()!!
onLooperCreated?.let { callback -> callback(looper) }
onLooperCreated = null
Looper.loop()
}
}

View File

@ -1,57 +0,0 @@
package net.mynero.wallet.service
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import net.mynero.wallet.MoneroApplication
import net.mynero.wallet.data.DefaultNode
import net.mynero.wallet.data.Node
import net.mynero.wallet.data.Node.Companion.fromJson
import net.mynero.wallet.util.Constants
import org.json.JSONException
import org.json.JSONObject
class PrefService(application: MoneroApplication) {
private val preferences = application.getSharedPreferences(
application.applicationInfo.packageName,
Context.MODE_PRIVATE
)
init {
instance = this
}
fun edit(): SharedPreferences.Editor {
return preferences.edit()
}
fun getString(key: String?, defaultValue: String): String? {
val value = preferences.getString(key, "")
if (value?.isEmpty() == true && defaultValue.isNotEmpty()) {
edit().putString(key, defaultValue)?.apply()
return defaultValue
}
return value
}
fun getBoolean(key: String?, defaultValue: Boolean): Boolean {
val containsKey = preferences.contains(key)
val value = preferences.getBoolean(key, false)
if (!value && defaultValue && !containsKey) {
edit().putBoolean(key, true)?.apply()
return true
}
return value
}
fun deleteProxy() {
Log.d("PrefService", "Deleting proxy...")
edit().putString(Constants.PREF_PROXY, "")?.apply()
}
companion object {
lateinit var instance: PrefService
private set
}
}

View File

@ -1,63 +0,0 @@
package net.mynero.wallet.service
import android.app.Application
import net.mynero.wallet.util.Constants
class ProxyService(application: Application) {
var samouraiTorManager: SamouraiTorManager? = null
var usingProxy: Boolean = false
get() = PrefService.instance.getBoolean(Constants.PREF_USES_PROXY, false)
set(enabled) {
PrefService.instance.edit().putBoolean(Constants.PREF_USES_PROXY, enabled).apply()
field = enabled
}
var useBundledTor: Boolean = false
get() = PrefService.instance.getBoolean(Constants.PREF_USE_BUNDLED_TOR, false)
set(enabled) {
PrefService.instance.edit().putBoolean(Constants.PREF_USE_BUNDLED_TOR, enabled).apply()
field = enabled
}
init {
instance = this
samouraiTorManager = SamouraiTorManager(application, TorKmpManager(application))
if (useBundledTor && usingProxy) {
samouraiTorManager?.start()
}
}
private fun hasProxySet(): Boolean {
val proxyString = proxy
return proxyString?.contains(":") == true
}
val proxy: String?
get() = PrefService.instance.getString(Constants.PREF_PROXY, "")
val proxyAddress: String
get() {
if (hasProxySet() && usingProxy) {
val proxyString = proxy
return proxyString?.split(":".toRegex())?.dropLastWhile { it.isEmpty() }
?.toTypedArray()
?.get(0) ?: ""
}
return ""
}
val proxyPort: String
get() {
if (hasProxySet() && usingProxy) {
val proxyString = proxy
return proxyString?.split(":".toRegex())?.dropLastWhile { it.isEmpty() }
?.toTypedArray()
?.get(1) ?: ""
}
return ""
}
companion object {
var instance: ProxyService? = null
private set
}
}

View File

@ -1,47 +0,0 @@
package net.mynero.wallet.service
import android.app.Application
import androidx.lifecycle.MutableLiveData
import net.mynero.wallet.model.TorState
import java.net.Proxy
class SamouraiTorManager(
private val appContext: Application?,
private val torKmpManager: TorKmpManager
) {
fun getTorStateLiveData(): MutableLiveData<TorState> {
return torKmpManager.torStateLiveData
}
fun getTorState(): TorState {
return torKmpManager.torState
}
fun isConnected(): Boolean {
return torKmpManager.isConnected()
}
fun isStarting(): Boolean {
return torKmpManager.isStarting()
}
fun stop() {
torKmpManager.torOperationManager.stopQuietly()
}
fun start() {
torKmpManager.torOperationManager.startQuietly()
}
fun getProxy(): Proxy? {
return torKmpManager.proxy
}
fun newIdentity() {
appContext?.let { torKmpManager.newIdentity(it) }
}
companion object {
private const val TAG = "SamouraiTorManager"
}
}

View File

@ -1,375 +0,0 @@
package net.mynero.wallet.service
import android.app.Application
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.matthewnelson.kmp.tor.KmpTorLoaderAndroid
import io.matthewnelson.kmp.tor.TorConfigProviderAndroid
import io.matthewnelson.kmp.tor.common.address.*
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Option.*
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Setting.*
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlInfoGet
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal
import io.matthewnelson.kmp.tor.controller.common.events.TorEvent
import io.matthewnelson.kmp.tor.manager.TorManager
import io.matthewnelson.kmp.tor.manager.TorServiceConfig
import io.matthewnelson.kmp.tor.manager.common.TorControlManager
import io.matthewnelson.kmp.tor.manager.common.TorOperationManager
import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent
import io.matthewnelson.kmp.tor.manager.common.state.isOff
import io.matthewnelson.kmp.tor.manager.common.state.isOn
import io.matthewnelson.kmp.tor.manager.common.state.isStarting
import io.matthewnelson.kmp.tor.manager.common.state.isStopping
import kotlinx.coroutines.*
import net.mynero.wallet.model.EnumTorState
import net.mynero.wallet.model.TorState
import java.net.InetSocketAddress
import java.net.Proxy
class TorKmpManager(application: Application) {
private val TAG = "TorListener"
private val providerAndroid by lazy {
object : TorConfigProviderAndroid(context = application) {
override fun provide(): TorConfig {
return TorConfig.Builder {
// Set multiple ports for all of the things
val dns = Ports.Dns()
put(dns.set(AorDorPort.Value(PortProxy(9252))))
put(dns.set(AorDorPort.Value(PortProxy(9253))))
val socks = Ports.Socks()
put(socks.set(AorDorPort.Value(PortProxy(9254))))
put(socks.set(AorDorPort.Value(PortProxy(9255))))
val http = Ports.HttpTunnel()
put(http.set(AorDorPort.Value(PortProxy(9258))))
put(http.set(AorDorPort.Value(PortProxy(9259))))
val trans = Ports.Trans()
put(trans.set(AorDorPort.Value(PortProxy(9262))))
put(trans.set(AorDorPort.Value(PortProxy(9263))))
// If a port (9263) is already taken (by ^^^^ trans port above)
// this will take its place and "overwrite" the trans port entry
// because port 9263 is taken.
put(socks.set(AorDorPort.Value(PortProxy(9263))))
// Set Flags
socks.setFlags(
setOf(
Ports.Socks.Flag.OnionTrafficOnly
)
).setIsolationFlags(
setOf(
Ports.IsolationFlag.IsolateClientAddr,
)
).set(AorDorPort.Value(PortProxy(9264)))
put(socks)
// reset our socks object to defaults
socks.setDefault()
// Not necessary, as if ControlPort is missing it will be
// automatically added for you; but for demonstration purposes...
// put(Ports.Control().set(AorDorPort.Auto))
// Use a UnixSocket instead of TCP for the ControlPort.
//
// A unix domain socket will always be preferred on Android
// if neither Ports.Control or UnixSockets.Control are provided.
put(UnixSockets.Control().set(FileSystemFile(
workDir.builder {
// Put the file in the "data" directory
// so that we avoid any directory permission
// issues.
//
// Note that DataDirectory is automatically added
// for you if it is not present in your provided
// config. If you set a custom Path for it, you
// should use it here.
addSegment(DataDirectory.DEFAULT_NAME)
addSegment(UnixSockets.Control.DEFAULT_NAME)
}
)))
// Use a UnixSocket instead of TCP for the SocksPort.
put(UnixSockets.Socks().set(FileSystemFile(
workDir.builder {
// Put the file in the "data" directory
// so that we avoid any directory permission
// issues.
//
// Note that DataDirectory is automatically added
// for you if it is not present in your provided
// config. If you set a custom Path for it, you
// should use it here.
addSegment(DataDirectory.DEFAULT_NAME)
addSegment(UnixSockets.Socks.DEFAULT_NAME)
}
)))
// For Android, disabling & reducing connection padding is
// advisable to minimize mobile data usage.
put(ConnectionPadding().set(AorTorF.False))
put(ConnectionPaddingReduced().set(TorF.True))
// Tor default is 24h. Reducing to 10 min helps mitigate
// unnecessary mobile data usage.
put(DormantClientTimeout().set(Time.Minutes(10)))
// Tor defaults this setting to false which would mean if
// Tor goes dormant, the next time it is started it will still
// be in the dormant state and will not bootstrap until being
// set to "active". This ensures that if it is a fresh start,
// dormancy will be cancelled automatically.
put(DormantCanceledByStartup().set(TorF.True))
// If planning to use v3 Client Authentication in a persistent
// manner (where private keys are saved to disk via the "Persist"
// flag), this is needed to be set.
put(ClientOnionAuthDir().set(FileSystemDir(
workDir.builder { addSegment(ClientOnionAuthDir.DEFAULT_NAME) }
)))
val hsPath = workDir.builder {
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
addSegment("test_service")
}
// Add Hidden services
put(
HiddenService()
.setPorts(
ports = setOf(
// Use a unix domain socket to communicate via IPC instead of over TCP
HiddenService.UnixSocket(
virtualPort = Port(80),
targetUnixSocket = hsPath.builder {
addSegment(HiddenService.UnixSocket.DEFAULT_UNIX_SOCKET_NAME)
}),
)
)
.setMaxStreams(maxStreams = HiddenService.MaxStreams(value = 2))
.setMaxStreamsCloseCircuit(value = TorF.True)
.set(FileSystemDir(path = hsPath))
)
put(HiddenService()
.setPorts(
ports = setOf(
HiddenService.Ports(
virtualPort = Port(80),
targetPort = Port(1030)
), // http
HiddenService.Ports(
virtualPort = Port(443),
targetPort = Port(1030)
) // https
)
)
.set(FileSystemDir(path =
workDir.builder {
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
addSegment("test_service_2")
}
))
)
}.build()
}
}
}
private val loaderAndroid by lazy {
KmpTorLoaderAndroid(provider = providerAndroid)
}
private val manager: TorManager by lazy {
TorManager.newInstance(
application = application,
loader = loaderAndroid,
requiredEvents = null
)
}
// only expose necessary interfaces
val torOperationManager: TorOperationManager get() = manager
val torControlManager: TorControlManager get() = manager
private val listener = TorListener()
val events: LiveData<String> get() = listener.eventLines
private val appScope by lazy {
CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
}
val torStateLiveData: MutableLiveData<TorState> = MutableLiveData()
var torState: TorState = TorState()
var proxy: Proxy? = null
init {
manager.debug(true)
manager.addListener(listener)
listener.addLine(TorServiceConfig.getMetaData(application).toString())
}
fun isConnected(): Boolean {
return manager.state.isOn()
}
fun isStarting(): Boolean {
return manager.state.isStarting()
}
fun newIdentity(appContext: Application) {
appScope.launch(Dispatchers.IO) {
var result = manager.signal(TorControlSignal.Signal.NewNym)
appScope.launch(Dispatchers.Main) {
if (result.isSuccess) {
val msg = "You have changed Tor identity"
listener.addLine(msg)
Toast.makeText(appContext, msg, Toast.LENGTH_SHORT).show()
} else if (result.isFailure) {
val msg = "Tor identity change failed"
listener.addLine(msg)
Toast.makeText(appContext, msg, Toast.LENGTH_SHORT).show()
}
}
}
}
private inner class TorListener : TorManagerEvent.Listener() {
private val _eventLines: MutableLiveData<String> = MutableLiveData("")
val eventLines: LiveData<String> = _eventLines
private val events: MutableList<String> = ArrayList(50)
fun addLine(line: String) {
synchronized(this) {
if (events.size > 49) {
events.removeAt(0)
}
events.add(line)
//Log.i(TAG, line)
try {
_eventLines.value = events.joinToString("\n")
} catch (e: Exception) {
_eventLines.postValue(events.joinToString("\n"))
}
}
}
override fun onEvent(event: TorManagerEvent) {
if (event is TorManagerEvent.State) {
val stateEvent: TorManagerEvent.State = event
val state = stateEvent.torState
torState.progressIndicator = state.bootstrap
val liveTorState = TorState()
liveTorState.progressIndicator = state.bootstrap
if (state.isOn()) {
torState.state = EnumTorState.ON
liveTorState.state = EnumTorState.ON
} else if (state.isStarting()) {
torState.state = EnumTorState.STARTING
liveTorState.state = EnumTorState.STARTING
} else if (state.isOff()) {
torState.state = EnumTorState.OFF
liveTorState.state = EnumTorState.OFF
} else if (state.isStopping()) {
torState.state = EnumTorState.STOPPING
liveTorState.state = EnumTorState.STOPPING
}
torStateLiveData.postValue(liveTorState)
}
addLine(event.toString())
super.onEvent(event)
}
override fun onEvent(event: TorEvent.Type.SingleLineEvent, output: String) {
addLine("$event - $output")
super.onEvent(event, output)
}
override fun onEvent(event: TorEvent.Type.MultiLineEvent, output: List<String>) {
addLine("multi-line event: $event. See Logs.")
// these events are many many many lines and should be moved
// off the main thread if ever needed to be dealt with.
val enabled = false
if (enabled) {
appScope.launch(Dispatchers.IO) {
Log.d(TAG, "-------------- multi-line event START: $event --------------")
for (line in output) {
Log.d(TAG, line)
}
Log.d(TAG, "--------------- multi-line event END: $event ---------------")
}
}
super.onEvent(event, output)
}
override fun managerEventError(t: Throwable) {
t.printStackTrace()
}
override fun managerEventAddressInfo(info: TorManagerEvent.AddressInfo) {
if (info.isNull) {
// Tear down HttpClient
} else {
info.socksInfoToProxyAddressOrNull()?.firstOrNull()?.let { proxyAddress ->
@Suppress("UNUSED_VARIABLE")
val socket =
InetSocketAddress(proxyAddress.address.value, proxyAddress.port.value)
proxy = Proxy(Proxy.Type.SOCKS, socket)
}
}
}
override fun managerEventStartUpCompleteForTorInstance() {
// Do one-time things after we're bootstrapped
appScope.launch {
torControlManager.onionAddNew(
type = OnionAddress.PrivateKey.Type.ED25519_V3,
hsPorts = setOf(HiddenService.Ports(virtualPort = Port(443))),
flags = null,
maxStreams = null,
).onSuccess { hsEntry ->
addLine(
"New HiddenService: " +
"\n - Address: https://${hsEntry.address.canonicalHostname()}" +
"\n - PrivateKey: ${hsEntry.privateKey}"
)
torControlManager.onionDel(hsEntry.address).onSuccess {
addLine("Aaaaaaaaand it's gone...")
}.onFailure { t ->
t.printStackTrace()
}
}.onFailure { t ->
t.printStackTrace()
}
delay(20_000L)
torControlManager.infoGet(TorControlInfoGet.KeyWord.Uptime()).onSuccess { uptime ->
addLine("Uptime - $uptime")
}.onFailure { t ->
t.printStackTrace()
}
}
}
}
}

View File

@ -0,0 +1,18 @@
package net.mynero.wallet.service.wallet
import android.os.Looper
class MoneroHandlerThread(private val 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()
}
override fun run() {
Looper.prepare()
val looper = Looper.myLooper()!!
onLooperCreated(looper)
Looper.loop()
}
}

View File

@ -7,23 +7,20 @@ 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.Enote
import net.mynero.wallet.model.PendingTransaction
import net.mynero.wallet.model.TransactionInfo
import net.mynero.wallet.model.Wallet
import net.mynero.wallet.model.Wallet.Companion.NEW_ACCOUNT_NAME
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.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
private const val EXTRA_WALLET_NAME = "wallet_name"
@ -34,6 +31,8 @@ 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_ACCOUNT_LABEL = "account_label"
private const val EXTRA_ACCOUNT_INDEX = "account_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"
@ -45,6 +44,10 @@ private const val EXTRA_ENOTES = "enotes"
class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
companion object {
val RUNNING: AtomicBoolean = AtomicBoolean(false)
}
private var daemonHeight: AtomicLong = AtomicLong(0)
// height of the wallet when it was opened
private var walletBeginSyncHeight: AtomicLong = AtomicLong(0)
@ -66,14 +69,16 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
}
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)
handleOpenWallet(walletName, walletPassword, daemonAddress, daemonUsername, daemonPassword, daemonTrusted, proxy)
}
WalletServiceCommand.CLOSE_WALLET -> {
handleCloseWallet()
}
WalletServiceCommand.SET_DAEMON_ADDRESS -> {
val daemonAddress = data.getString(EXTRA_DAEMON_ADDRESS)!!
@ -86,14 +91,11 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
WalletServiceCommand.FETCH_BLOCKCHAIN_HEIGHT -> {
handleFetchDaemonHeight()
}
WalletServiceCommand.REFRESH_WALLET_TRANSACTION_HISTORY -> {
handleRefreshTransactionHistory()
}
WalletServiceCommand.GENERATE_NEW_SUBADDRESS -> {
WalletServiceCommand.GENERATE_NEW_ADDRESS -> {
val subaddressLabel = data.getString(EXTRA_SUBADDRESS_LABEL)!!
handleGenerateNewSubaddress(subaddressLabel)
}
WalletServiceCommand.SET_SUBADDRESS_LABEL -> {
WalletServiceCommand.SET_ADDRESS_LABEL -> {
val subaddressIndex = data.getInt(EXTRA_SUBADDRESS_INDEX)
val subaddressLabel = data.getString(EXTRA_SUBADDRESS_LABEL)!!
handleSetSubaddressLabel(subaddressIndex, subaddressLabel)
@ -129,22 +131,32 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
val proxy = data.getString(EXTRA_PROXY)!!
handleSetProxy(proxy)
}
WalletServiceCommand.REFRESH_ENOTES -> {
handleRefreshEnotes()
}
WalletServiceCommand.FREEZE_ENOTES -> {
val enotes = data.getStringArrayList(EXTRA_ENOTES)!!
val enotes = data.getLongArray(EXTRA_ENOTES)!!
handleFreezeEnotes(enotes)
}
WalletServiceCommand.THAW_ENOTES -> {
val enotes = data.getStringArrayList(EXTRA_ENOTES)!!
val enotes = data.getLongArray(EXTRA_ENOTES)!!
handleThawEnotes(enotes)
}
WalletServiceCommand.CREATE_ACCOUNT -> {
val label = data.getString(EXTRA_ACCOUNT_LABEL)
handleCreateAccount(label)
}
WalletServiceCommand.SET_ACCOUNT -> {
val index = data.getInt(EXTRA_ACCOUNT_INDEX)
handleSetAccount(index)
}
WalletServiceCommand.SET_ACCOUNT_LABEL -> {
val index = data.getInt(EXTRA_ACCOUNT_INDEX)
val label = data.getString(EXTRA_ACCOUNT_LABEL)!!
handleSetAccountLabel(index, label)
}
}
}
private fun handleOpenWallet(
walletFile: File,
walletName: String,
walletPassword: String,
daemonAddress: String,
daemonUsername: String,
@ -152,25 +164,27 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
daemonTrusted: Boolean,
proxy: String
) {
WalletManager.instance.setProxy(proxy)
WalletManager.instance.setProxyJ(proxy)
WalletManager.instance.setDaemonAddressJ(daemonAddress)
val wallet = WalletManager.instance.openWallet(walletFile.absolutePath, walletPassword)
val wallet = WalletManager.instance.openWallet(walletName, 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())
updateWallet(wallet)
this@WalletService.walletRef.set(wallet)
forEachObserver {
it.onWalletOpened(wallet)
}
handleRefreshTransactionHistory()
handleRefreshEnotes()
handleFetchDaemonHeight() // TODO: investigate why there is a double free in Monero code when this is moved after startRefresh
wallet.setListener(this@WalletService)
wallet.startRefresh()
handleFetchDaemonHeight()
}
private fun handleCloseWallet() {
walletRef.getAndSet(null)?.let { wallet ->
wallet.setListener(null)
wallet.close()
}
}
private fun setDaemonAddress(
@ -180,20 +194,14 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
daemonTrusted: Boolean,
proxy: String
) {
WalletManager.instance.setProxy(proxy)
WalletManager.instance.setProxyJ(proxy)
WalletManager.instance.setDaemonAddressJ(daemonAddress)
walletRef.get()?.let { wallet ->
wallet.initJ(daemonAddress, 0, daemonUsername, daemonPassword, proxy)
wallet.setTrustedDaemon(daemonTrusted)
updateWallet(wallet)
wallet.setListener(this@WalletService)
wallet.startRefresh()
forEachObserver {
it.onWalletOpened(wallet)
}
handleRefreshTransactionHistory()
handleRefreshEnotes()
}
handleFetchDaemonHeight()
}
@ -208,16 +216,11 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
}
}
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()
val wallet = getWalletOrThrow()
wallet.addSubaddress(wallet.getAccountIndex(), label)
wallet.store()
updateWallet(wallet)
forEachObserver {
it.onSubaddressesUpdated()
}
@ -282,32 +285,28 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
val amountWithBasicFee = amount + basicFeeEstimate
val selectedUtxos = ArrayList<String>()
val seenTxs = ArrayList<String>()
val utxos: List<CoinsInfo> = ArrayList(walletRef.get()!!.coins?.all!!)
val enotes: List<Enote> = ArrayList(walletRef.get()!!.getEnotes())
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
for (enote in enotes) {
// TODO: filter account
if (!enote.isSpent && enote.isUnlocked && !enote.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) }
enote.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) }
if (amountSelected <= amountWithBasicFee && !seenTxs.contains(enote.hash)) {
enote.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
enote.hash?.let { seenTxs.add(it) }
amountSelected += enote.amount
}
}
}
}
if (amountSelected < amountWithBasicFee && !sendAll) {
Log.e("WS", "amountSelected = $amountSelected, amountWithBasicFee = $amountWithBasicFee")
throw Exception("insufficient wallet balance")
}
return selectedUtxos
@ -326,7 +325,7 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
}
private fun handleSetProxy(value: String) {
val success = WalletManager.instance.setProxy(value)
val success = WalletManager.instance.setProxyJ(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)
@ -338,47 +337,60 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
}
}
private fun calculateBalanceAndNotifyAboutRefreshedEnotes(enotes: List<CoinsInfo>) {
var total = 0L
var unlocked = 0L
var frozen = 0L
var pending = 0L
private fun handleFreezeEnotes(enotes: LongArray) {
val wallet = walletRef.get()!!
enotes.forEach {
wallet.freeze(it)
}
wallet.store("")
updateWallet(wallet)
}
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
private fun handleThawEnotes(enotes: LongArray) {
val wallet = walletRef.get()!!
enotes.forEach {
wallet.thaw(it)
}
wallet.store("")
updateWallet(wallet)
}
private fun handleCreateAccount(
label: String?
) {
getWallet()?.let { wallet ->
wallet.addAccount(label ?: NEW_ACCOUNT_NAME)
wallet.store("")
forEachObserver {
it.onAccountCreated()
}
}
}
forEachObserver {
it.onEnotesRefreshed(enotes, Balance(total, unlocked, frozen, pending))
private fun handleSetAccount(
index: Int
) {
getWallet()?.let { wallet ->
wallet.setAccountIndex(index)
updateWallet(wallet)
forEachObserver {
it.onAccountSet(index)
}
}
}
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)
private fun handleSetAccountLabel(
index: Int,
label: String
) {
getWallet()?.let { wallet ->
wallet.setAccountLabel(index, label)
wallet.store("")
updateWallet(wallet)
forEachObserver {
it.onAccountLabelChanged(index, label)
}
}
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)
}
}
@ -387,10 +399,12 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
private lateinit var handler: WalletHandler
override fun onBind(intent: Intent?): IBinder {
Timber.d("A component is binding")
return mBinder
}
override fun onCreate() {
Timber.d("Wallet Service is being created")
// ArrayBlockingQueue is used as a oneshot channel to receive a Looper from MoneroHandlerThread
val oneShotQueue = ArrayBlockingQueue<Looper>(1)
val thread = MoneroHandlerThread { looper ->
@ -400,9 +414,11 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
val looper = oneShotQueue.take()
handler = WalletHandler(looper)
RUNNING.set(true)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("Wallet Service is being started")
if (intent != null) {
return START_STICKY
} else {
@ -411,7 +427,14 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
}
}
override fun onDestroy() {
Timber.d("Wallet Service is being destroyed")
closeWallet()
RUNNING.set(false)
}
override fun onDestroy(owner: LifecycleOwner) {
Timber.d("An observer ${owner.javaClass.simpleName} is being destroyed")
synchronized(observers) {
observers.remove(owner)
}
@ -425,47 +448,44 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
}
}
private fun updateWallet(wallet: Wallet) {
wallet.update()
forEachObserver {
it.onWalletUpdated(wallet)
}
}
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)
}
// Monero heights are fucked up. Wallet and blockchain heights are always +1 to real height, but this callback receives real height
daemonHeight.updateAndGet { if (it > 0 && it < (height + 1)) (height + 1) else it }
val wallet = getWalletOrThrow()
refreshTransactionsHistory()
wallet.coins!!.refresh()
if (wallet.isSynchronized) {
wallet.store()
}
// TODO: optimize to call this only on the last block of the batch
updateWallet(wallet)
}
override fun updated() {
Timber.d("Updated callback")
forEachObserver {
it.onUpdated()
}
updateWallet(getWalletOrThrow())
}
override fun refreshed() {
Timber.d("Refreshed callback")
forEachObserver {
it.onRefreshed()
}
if (daemonHeight.get() <= 1) {
Timber.d("Fetching blockchain height")
fetchBlockchainHeight()
@ -477,9 +497,11 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
wallet.isSynchronized = true
wallet.store()
}
updateWallet(wallet)
}
fun addObserver(observer: WalletServiceObserver) {
Timber.d("An observer ${observer.javaClass.simpleName} is being added")
observer.lifecycle.addObserver(this)
synchronized(observers) {
observers.add(observer)
@ -520,6 +542,7 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
daemonTrusted: Boolean,
proxy: String
) {
closeWallet()
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.OPEN_WALLET.value()
message.data.putString(EXTRA_WALLET_NAME, walletName)
@ -532,6 +555,12 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
handler.sendMessage(message)
}
fun closeWallet() {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.CLOSE_WALLET.value()
handler.sendMessage(message)
}
fun setDaemonAddress(
daemonAddress: String,
daemonUsername: String,
@ -555,22 +584,16 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
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.arg1 = WalletServiceCommand.GENERATE_NEW_ADDRESS.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.arg1 = WalletServiceCommand.SET_ADDRESS_LABEL.value()
message.data.putInt(EXTRA_SUBADDRESS_INDEX, index)
message.data.putString(EXTRA_SUBADDRESS_LABEL, label)
handler.sendMessage(message)
@ -608,56 +631,72 @@ class WalletService : Service(), WalletListener, DefaultLifecycleObserver {
handler.sendMessage(message)
}
fun refreshEnotes() {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.REFRESH_ENOTES.value()
handler.sendMessage(message)
}
fun freezeEnote(enotes: List<String>) {
fun freezeEnote(enotes: List<Long>) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.FREEZE_ENOTES.value()
message.data.putStringArrayList(EXTRA_ENOTES, ArrayList(enotes))
message.data.putLongArray(EXTRA_ENOTES, enotes.toLongArray())
handler.sendMessage(message)
}
fun thawEnote(enotes: List<String>) {
fun thawEnote(enotes: List<Long>) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.THAW_ENOTES.value()
message.data.putStringArrayList(EXTRA_ENOTES, ArrayList(enotes))
message.data.putLongArray(EXTRA_ENOTES, enotes.toLongArray())
handler.sendMessage(message)
}
fun createAccount(label: String? = null) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.CREATE_ACCOUNT.value()
message.data.putString(EXTRA_ACCOUNT_LABEL, label)
handler.sendMessage(message)
}
fun setAccount(index: Int) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.SET_ACCOUNT.value()
message.data.putInt(EXTRA_ACCOUNT_INDEX, index)
handler.sendMessage(message)
}
fun setAccountLabel(index: Int, label: String) {
val message = handler.obtainMessage()
message.arg1 = WalletServiceCommand.SET_ACCOUNT_LABEL.value()
message.data.putInt(EXTRA_ACCOUNT_INDEX, index)
message.data.putString(EXTRA_ACCOUNT_LABEL, label)
handler.sendMessage(message)
}
}
interface WalletServiceObserver : LifecycleOwner {
fun onWalletOpened(wallet: Wallet) {}
fun onWalletHistoryRefreshed(transactions: List<TransactionInfo>) {}
fun onWalletUpdated(wallet: Wallet) {}
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) {}
fun onAccountCreated() {}
fun onAccountSet(index: Int) {}
fun onAccountLabelChanged(index: Int, label: String) {}
}
enum class WalletServiceCommand {
UNKNOWN,
UNKNOWN, // to catch bugs when command is set to 0 by accident
OPEN_WALLET,
CLOSE_WALLET,
SET_DAEMON_ADDRESS,
FETCH_BLOCKCHAIN_HEIGHT,
REFRESH_WALLET_TRANSACTION_HISTORY,
GENERATE_NEW_SUBADDRESS,
SET_SUBADDRESS_LABEL,
GENERATE_NEW_ADDRESS,
SET_ADDRESS_LABEL,
CREATE_SWEEP_TX,
CREATE_TX,
SEND_TX,
SET_PROXY,
REFRESH_ENOTES,
FREEZE_ENOTES,
THAW_ENOTES;
THAW_ENOTES,
CREATE_ACCOUNT,
SET_ACCOUNT,
SET_ACCOUNT_LABEL;
fun value() = ordinal

View File

@ -7,26 +7,25 @@ object Constants {
const val DEFAULT_WALLET_NAME = "xmr_wallet"
const val MNEMONIC_LANGUAGE = "English"
const val STREET_MODE_BALANCE = "#.############"
const val PREF_USES_PROXY = "pref_uses_tor"
const val PREF_USE_PROXY = "pref_use_proxy"
const val PREF_PROXY = "pref_proxy"
// _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_NODE = "pref_node"
const val PREF_NODES = "pref_nodes"
const val PREF_STREET_MODE = "pref_street_mode"
const val PREF_ALLOW_FEE_OVERRIDE = "pref_allow_fee_override"
const val PREF_USE_BUNDLED_TOR = "pref_use_bundled_tor"
const val PREF_ENABLE_MULTIPLE_WALLETS = "pref_enable_multiple_wallets"
const val PREF_ENABLE_MULTIPLE_ACCOUNTS = "pref_enable_multiple_accounts"
const val URI_PREFIX = "monero:"
const val URI_ARG_AMOUNT = "tx_amount"
const val URI_ARG_AMOUNT2 = "amount"
const val NAV_ARG_TXINFO = "nav_arg_txinfo"
const val STREET_MODE_BALANCE = "#.############"
const val EXTRA_PREVENT_GOING_BACK = "prevent_going_back"
const val EXTRA_WALLET_NAME = "wallet_name"
const val EXTRA_WALLET_PATH = "wallet_name"
const val EXTRA_WALLET_PASSWORD = "wallet_password"
const val EXTRA_SEND_ADDRESS = "send_address"
const val EXTRA_SEND_AMOUNT = "send_amount"

View File

@ -66,7 +66,7 @@ object Helper {
return getStorage(context, WALLET_DIR)
}
fun getStorage(context: Context, folderName: String?): File {
fun getStorage(context: Context, folderName: String): File {
val dir = File(context.filesDir, folderName)
if (!dir.exists()) {
Log.i("Helper.kt", "Creating ${dir.absolutePath}")
@ -116,13 +116,6 @@ object Helper {
}
}
fun getWalletFile(context: Context, aWalletName: String?): File {
val walletDir = getWalletRoot(context)
val f = File(walletDir, aWalletName)
Log.d("Helper.kt", "wallet=${f.absolutePath} size= ${f.length()}")
return f
}
fun showKeyboard(act: Activity) {
val imm = act.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val focus = act.currentFocus

View File

@ -26,6 +26,14 @@ object PreferenceUtils {
fun setUseProxy(context: Context, value: Boolean) = setBoolean(context, Constants.PREF_USE_PROXY, value)
fun isMultiWalletMode(context: Context): Boolean = getBoolean(context, Constants.PREF_ENABLE_MULTIPLE_WALLETS, false)
fun setMultiWalletMode(context: Context, value: Boolean) = setBoolean(context, Constants.PREF_ENABLE_MULTIPLE_WALLETS, value)
fun isMultiAccountMode(context: Context): Boolean = getBoolean(context, Constants.PREF_ENABLE_MULTIPLE_ACCOUNTS, false)
fun setMultiAccountMode(context: Context, value: Boolean) = setBoolean(context, Constants.PREF_ENABLE_MULTIPLE_ACCOUNTS, 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)
@ -104,11 +112,11 @@ object PreferenceUtils {
}
fun setNode(context: Context, node: Node): Result<Unit> = node.toJson().map { nodeJson ->
setString(context, Constants.PREF_NODE_3, nodeJson.toString())
setString(context, Constants.PREF_NODE, nodeJson.toString())
}
private fun getNode(context: Context): Result<Node?> {
val nodeJsonString = getString(context, Constants.PREF_NODE_3) ?: return Result.success(null)
val nodeJsonString = getString(context, Constants.PREF_NODE) ?: return Result.success(null)
val nodeJson = kotlin.runCatching { JSONObject(nodeJsonString) }
return nodeJson.fold(
{

View File

@ -0,0 +1,40 @@
package net.mynero.wallet.util
import android.graphics.Bitmap
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.journeyapps.barcodescanner.BarcodeEncoder
import timber.log.Timber
import java.nio.charset.StandardCharsets
import java.util.EnumMap
object QrCodeHelper {
fun generateQrCode(text: String, width: Int, height: Int, backgroundColor: Int): Bitmap? {
if (width <= 0 || height <= 0) return null
val hints: MutableMap<EncodeHintType, Any?> = EnumMap(com.google.zxing.EncodeHintType::class.java)
hints[EncodeHintType.CHARACTER_SET] = StandardCharsets.UTF_8
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M
try {
val barcodeEncoder = BarcodeEncoder()
val bitMatrix = barcodeEncoder.encode(text, BarcodeFormat.QR_CODE, width, height, hints)
val pixels = IntArray(bitMatrix.width * bitMatrix.height)
for (i in 0 until bitMatrix.height) {
for (j in 0 until bitMatrix.width) {
if (bitMatrix[j, i]) {
pixels[i * width + j] = -0x1
} else {
pixels[i * height + j] = backgroundColor
}
}
}
return Bitmap.createBitmap(pixels, 0, bitMatrix.width, bitMatrix.width, bitMatrix.height, Bitmap.Config.ARGB_8888)
} catch (e: WriterException) {
Timber.e(e, "Failed to generate QR code")
}
return null
}
}

View File

@ -0,0 +1,38 @@
package net.mynero.wallet.util.acitivity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import timber.log.Timber
open class LoggingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("Activity ${javaClass.simpleName} is being created")
}
override fun onStart() {
super.onStart()
Timber.d("Activity ${javaClass.simpleName} is being started")
}
override fun onResume() {
super.onResume()
Timber.d("Activity ${javaClass.simpleName} is being resumed")
}
override fun onPause() {
super.onPause()
Timber.d("Activity ${javaClass.simpleName} is being paused")
}
override fun onStop() {
super.onStop()
Timber.d("Activity ${javaClass.simpleName} is being stopped")
}
override fun onDestroy() {
super.onDestroy()
Timber.d("Activity ${javaClass.simpleName} is being destroyed")
}
}

View File

@ -0,0 +1,42 @@
package net.mynero.wallet.util.acitivity
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import net.mynero.wallet.service.wallet.WalletService
import net.mynero.wallet.service.wallet.WalletServiceObserver
import timber.log.Timber
abstract class MoneroActivity : LoggingActivity(), WalletServiceObserver {
protected var walletService: WalletService? = null
private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
Timber.d("Wallet Service connected for activity ${this@MoneroActivity.javaClass.simpleName}")
val walletService = (service as WalletService.WalletServiceBinder).service
this@MoneroActivity.walletService = walletService
walletService.addObserver(this@MoneroActivity)
onWalletServiceBound(walletService)
}
override fun onServiceDisconnected(className: ComponentName) {
Timber.i("Wallet Service disconnected for activity ${this@MoneroActivity.javaClass.simpleName}")
walletService = null
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!WalletService.RUNNING.get()) {
Timber.i("Finishing activity because wallet service is not running yet")
finish()
} else {
bindService(Intent(applicationContext, WalletService::class.java), connection, 0)
}
}
open fun onWalletServiceBound(walletService: WalletService) {}
}

View File

@ -0,0 +1,57 @@
package net.mynero.wallet.util.acitivity
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import net.mynero.wallet.HomeActivity
import net.mynero.wallet.SendActivity
import net.mynero.wallet.data.DefaultNode
import net.mynero.wallet.service.wallet.WalletService
import net.mynero.wallet.util.Constants
import net.mynero.wallet.util.PreferenceUtils
import net.mynero.wallet.util.UriData
import timber.log.Timber
open class WalletOpeningActivity : LoggingActivity() {
fun openWallet(walletPath: String, walletPassword: String) {
val uriData = intent.data?.let { UriData.parse(it.toString()) }
val walletServiceIntent = Intent(applicationContext, WalletService::class.java)
startService(walletServiceIntent)
bindService(walletServiceIntent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Timber.d("Wallet Service connected")
val walletService = (service as WalletService.WalletServiceBinder).service
val node = PreferenceUtils.getOrSetDefaultNode(this@WalletOpeningActivity, DefaultNode.defaultNode())
walletService.openWallet(walletPath, walletPassword, node.address, node.username ?: "", node.password ?: "", node.trusted, PreferenceUtils.getProxyIfEnabled(applicationContext) ?: "")
val homeActivityIntent = Intent(this@WalletOpeningActivity, HomeActivity::class.java)
if (uriData == null) {
// the app was NOT started with a monero uri payment data, proceed to the home activity
Timber.d("Uri payment data not present, launching home activity")
startActivity(homeActivityIntent)
finish()
} 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
Timber.d("Uri payment data present, launching home and send activities")
homeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
startActivity(homeActivityIntent)
val sendIntent = Intent(this@WalletOpeningActivity, SendActivity::class.java)
sendIntent.putExtra(Constants.EXTRA_SEND_ADDRESS, uriData.address)
sendIntent.putExtra(Constants.EXTRA_SEND_AMOUNT, uriData.amount)
startActivity(sendIntent)
finish()
}
}
override fun onServiceDisconnected(name: ComponentName?) {
Timber.i("Wallet Service disconnected")
}
}, 0)
}
}

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<TextView
android:id="@+id/account_item_account_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:singleLine="true"
android:text="Account"
android:textColor="@color/oled_addressListColor"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/address_label_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/address_label_textview"
style="@style/MoneroText.Subaddress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:text="Label"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/address_item_address_textview" />
<TextView
android:id="@+id/address_amount_textview"
android:layout_width="0dp"
android:layout_height="0dp"
android:ellipsize="middle"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:text="Amount"
android:textAlignment="viewEnd"
android:textColor="@color/oled_positiveColor"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/address_label_textview"
app:layout_constraintTop_toBottomOf="@id/address_item_address_textview" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,6 +6,21 @@
android:layout_height="match_parent"
tools:context="net.mynero.wallet.HomeActivity">
<TextView
android:id="@+id/wallet_and_account_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:textAlignment="center"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="wallet / account" />
<ProgressBar
android:id="@+id/sync_progress_bar"
style="?android:attr/progressBarStyleHorizontal"
@ -16,7 +31,7 @@
android:progressDrawable="@drawable/sync_progress_bar_drawable"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/wallet_and_account_textview" />
<TextView
android:id="@+id/sync_progress_text"
@ -40,12 +55,12 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginTop="4dp"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/settings_imageview"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sync_progress_text"
tools:text="100.000000000000" />
<TextView

View File

@ -25,36 +25,31 @@
android:text="@string/create_wallet"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/wallet_password_edittext"
app:layout_constraintEnd_toStartOf="@id/onboarding_tor_loading_progressindicator"
app:layout_constraintBottom_toTopOf="@id/wallet_name_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/onboarding_tor_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/tor"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/onboarding_tor_loading_progressindicator"
app:layout_constraintEnd_toEndOf="@id/onboarding_tor_loading_progressindicator"
app:layout_constraintStart_toStartOf="@id/onboarding_tor_loading_progressindicator"
app:layout_constraintTop_toTopOf="@id/onboarding_tor_loading_progressindicator" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/onboarding_tor_loading_progressindicator"
android:layout_width="32dp"
android:layout_height="32dp"
android:indeterminate="true"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/create_wallet_textview"
<EditText
android:id="@+id/wallet_name_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/edittext_bg"
android:hint="Wallet name"
android:inputType="text"
android:minHeight="48dp"
app:layout_constraintBottom_toTopOf="@id/wallet_password_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/create_wallet_textview" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/create_wallet_textview"
tools:visibility="visible" />
<EditText
android:id="@+id/wallet_password_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/edittext_bg"
android:hint="@string/password_non_optional"
android:inputType="textPassword"
@ -62,23 +57,24 @@
app:layout_constraintBottom_toTopOf="@id/wallet_password_confirm_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/create_wallet_textview"
app:layout_constraintTop_toBottomOf="@id/wallet_name_edittext"
tools:visibility="visible" />
<EditText
android:id="@+id/wallet_password_confirm_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="8dp"
android:background="@drawable/edittext_bg"
android:hint="@string/password_confirm"
android:inputType="textPassword"
android:minHeight="48dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/advanced_settings_dropdown_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wallet_password_edittext"
tools:visibility="gone" />
tools:visibility="visible" />
<TextView
android:id="@+id/advanced_settings_dropdown_textview"
@ -117,26 +113,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/advanced_settings_dropdown_textview">
<TextView
android:id="@+id/disable_xmrchan_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/option_hide_xmrchan"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@id/show_xmrchan_switch"
app:layout_constraintEnd_toStartOf="@id/show_xmrchan_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/show_xmrchan_switch" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/show_xmrchan_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:minHeight="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/select_node_button"
android:layout_width="match_parent"
@ -149,9 +125,8 @@
android:singleLine="true"
app:layout_constraintBottom_toTopOf="@id/wallet_seed_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/show_xmrchan_switch"
tools:ignore="SpeakableTextPresentCheck"
tools:text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />
@ -222,7 +197,8 @@
app:layout_constraintBottom_toTopOf="@id/seed_offset_checkbox"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wallet_seed_edittext" />
app:layout_constraintTop_toBottomOf="@id/wallet_seed_edittext"
tools:visibility="visible" />
<CheckBox
android:id="@+id/seed_offset_checkbox"
@ -233,7 +209,8 @@
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wallet_restore_height_edittext" />
app:layout_constraintTop_toBottomOf="@id/wallet_restore_height_edittext"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
@ -264,17 +241,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/bundled_tor_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:text="@string/use_bundled_tor"
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_address_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tor_onboarding_switch" />
<EditText
android:id="@+id/wallet_proxy_address_edittext"
android:layout_width="0dp"
@ -282,24 +248,11 @@
android:background="@drawable/edittext_bg"
android:hint="@string/wallet_proxy_address_hint"
android:minHeight="48dp"
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_port_edittext"
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_address_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bundled_tor_checkbox" />
app:layout_constraintTop_toBottomOf="@id/tor_onboarding_switch" />
<EditText
android:id="@+id/wallet_proxy_port_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@drawable/edittext_bg"
android:hint="@string/wallet_proxy_port_hint"
android:inputType="number"
android:minHeight="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wallet_proxy_address_edittext" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -39,7 +39,7 @@
android:layout_width="256dp"
android:layout_height="256dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_fingerprint"
android:src="@drawable/ic_monero"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -19,38 +19,16 @@
android:text="@string/settings"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/settings_tor_loading_progressindicator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/settings_tor_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="24dp"
android:src="@drawable/tor"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/settings_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/settings_textview" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/settings_tor_loading_progressindicator"
android:layout_width="32dp"
android:layout_height="32dp"
android:indeterminate="true"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/settings_tor_icon"
app:layout_constraintEnd_toEndOf="@id/settings_tor_icon"
app:layout_constraintStart_toStartOf="@id/settings_tor_icon"
app:layout_constraintTop_toTopOf="@id/settings_tor_icon" />
<TextView
android:id="@+id/wallet_settings_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="@string/wallet"
android:textSize="24sp"
@ -116,6 +94,51 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/display_utxos_button" />
<TextView
android:id="@+id/enable_multiple_wallets_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="Enable multi-wallet mode"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@id/enable_multiple_wallets_switch"
app:layout_constraintEnd_toStartOf="@id/enable_multiple_wallets_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/enable_multiple_wallets_switch" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/enable_multiple_wallets_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:minWidth="48dp"
android:minHeight="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appearance_settings_textview" />
<TextView
android:id="@+id/enable_multiple_accounts_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="Enable multi-account mode"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@id/enable_multiple_accounts_switch"
app:layout_constraintEnd_toStartOf="@id/enable_multiple_accounts_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/enable_multiple_accounts_switch" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/enable_multiple_accounts_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/enable_multiple_wallets_switch" />
<TextView
android:id="@+id/street_mode_label_textview"
android:layout_width="0dp"
@ -132,12 +155,11 @@
android:id="@+id/street_mode_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:minWidth="48dp"
android:minHeight="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appearance_settings_textview" />
app:layout_constraintTop_toBottomOf="@id/enable_multiple_accounts_switch" />
<TextView
android:id="@+id/allow_fee_override_label_textview"
@ -162,6 +184,59 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/street_mode_switch" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/wallet_account_settings_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/allow_fee_override_switch">
<TextView
android:id="@+id/account_settings_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:text="Accounts"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/add_account_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:background="@drawable/button_bg"
android:text="@string/add_account"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/account_settings_textview" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/accounts_recycler_view"
android:layout_width="0dp"
android:layout_height="128dp"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:background="@drawable/round_bg"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/add_account_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/network_settings_textview"
android:layout_width="match_parent"
@ -174,7 +249,7 @@
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/allow_fee_override_switch" />
app:layout_constraintTop_toBottomOf="@+id/wallet_account_settings_layout" />
<Button
android:id="@+id/select_node_button"
@ -196,7 +271,7 @@
tools:text="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />
<TextView
android:id="@+id/tor_textview"
android:id="@+id/proxy_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
@ -227,20 +302,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/proxy_switch">
<CheckBox
android:id="@+id/bundled_tor_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:minHeight="48dp"
android:text="@string/use_bundled_tor"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_address_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/wallet_proxy_address_edittext"
android:layout_width="0dp"
@ -248,28 +309,12 @@
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:background="@drawable/edittext_bg"
android:hint="@string/wallet_proxy_address_hint"
android:hint="@string/proxy_address_hint"
android:minHeight="48dp"
app:layout_constraintBottom_toTopOf="@id/wallet_proxy_port_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bundled_tor_checkbox" />
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/wallet_proxy_port_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:background="@drawable/edittext_bg"
android:hint="@string/wallet_proxy_port_hint"
android:inputType="number"
android:minHeight="48dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wallet_proxy_address_edittext" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
tools:context="net.mynero.wallet.ReceiveActivity">
<Button
android:id="@+id/create_or_import_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="8dp"
android:background="@drawable/button_bg"
android:text="Create / Import wallet"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/recv_monero_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="Mysu"
android:textAlignment="center"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/monero_qr_imageview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/monero_qr_imageview"
android:layout_width="256dp"
android:layout_height="256dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_monero"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recv_monero_textview" />
<TextView
android:id="@+id/address_list_label_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:singleLine="true"
android:text="Wallets"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/wallet_list_recyclerview"
app:layout_constraintStart_toStartOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/wallet_list_recyclerview"
android:layout_width="match_parent"
android:layout_height="256dp"
android:layout_marginBottom="16dp"
android:background="@drawable/round_bg"
android:clipToPadding="true"
app:layout_constraintBottom_toTopOf="@+id/create_or_import_wallet"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/oled_dialogBackgroundColor">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/enter_password_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="32dp"
android:text="@string/edit_address_label"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/wallet_password_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/wallet_password_edittext"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="32dp"
android:background="@drawable/edittext_bg"
android:hint="@string/label"
android:inputType="text"
app:layout_constraintBottom_toTopOf="@id/unlock_wallet_button"
app:layout_constraintEnd_toStartOf="@id/paste_password_imagebutton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/enter_password_textview" />
<ImageButton
android:id="@+id/paste_password_imagebutton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="24dp"
android:background="@android:color/transparent"
android:minWidth="48dp"
android:minHeight="48dp"
android:src="@drawable/ic_content_paste_24dp"
app:layout_constraintBottom_toBottomOf="@id/wallet_password_edittext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/wallet_password_edittext"
app:layout_constraintTop_toTopOf="@id/wallet_password_edittext" />
<Button
android:id="@+id/unlock_wallet_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="16dp"
android:background="@drawable/button_bg"
android:text="@string/save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wallet_password_edittext" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<ImageView
android:id="@+id/monero_qr_imageview2"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:src="@drawable/ic_monero"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/wallet_name_textview"
style="@style/MoneroText.Subaddress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="middle"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:text="Label"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/monero_qr_imageview2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -39,6 +39,26 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recv_monero_textview" />
<TextView
android:id="@+id/encrypt_seed_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Use wallet password as seed offset"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@id/encrypt_seed_switch"
app:layout_constraintEnd_toStartOf="@id/encrypt_seed_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/encrypt_seed_switch" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/encrypt_seed_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
android:minHeight="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/wallet_seed_label_textview" />
<TextView
android:id="@+id/wallet_seed_desc_textview"
android:layout_width="0dp"
@ -47,10 +67,10 @@
android:text="@string/wallet_seed_desc"
android:textColor="#f00"
android:textSize="12sp"
app:layout_constraintBottom_toTopOf="@id/information_textview"
app:layout_constraintBottom_toTopOf="@id/seed_textview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/wallet_seed_label_textview"
app:layout_constraintTop_toBottomOf="@id/encrypt_seed_switch"
app:layout_constraintVertical_bias="0.0" />
<TextView
@ -64,10 +84,11 @@
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/information_textview" />
app:layout_constraintTop_toBottomOf="@id/seed_textview"
tools:visibility="visible" />
<TextView
android:id="@+id/information_textview"
android:id="@+id/seed_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"

View File

@ -37,6 +37,7 @@
<string name="option_hide_xmrchan">Show Monerochan</string>
<string name="option_allow_fee_override">Manual fee selection</string>
<string name="display_recovery_phrase">Display wallet keys</string>
<string name="add_account">Add account</string>
<string name="tor_switch_label">SOCKS Proxy</string>
<string name="connection_failed">Failed to connect. Retrying…</string>
<string name="address">87BqQYkugEzh6Tuyotm2uc3DzJzKM6MuZaC161e6u1TsQxxPmXVPHpdNRyK47JY4d1hhbe25YVz4e9vTXCLDxvHkRXEAeBC</string>
@ -84,6 +85,7 @@
<string name="tx_amount_no_prefix2">999.99999999999</string>
<string name="wallet_proxy_address_hint">127.0.0.1</string>
<string name="wallet_proxy_port_hint">9050</string>
<string name="proxy_address_hint">127.0.0.1:9050</string>
<string name="no_history_loading">Loading your wallet…</string>
<string name="no_history_nget_some_monero_in_here">No transactions to display.\nAcquire coins by doing jobs, selling products, mining it, or buying some peer-to-peer.</string>
<string name="node_button_text">Node: %1$s</string>