mirror of
https://codeberg.org/anoncontributorxmr/mysu.git
synced 2025-01-17 14:46:32 -07:00
multi account and multi wallet support
This commit is contained in:
parent
cf8b1e9ce0
commit
a099040556
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -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
|
||||
|
@ -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"
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
@ -120,28 +100,44 @@ class EnotesActivity : AppCompatActivity(), WalletServiceObserver {
|
||||
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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ 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
|
||||
@ -29,21 +28,19 @@ 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 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
|
||||
@ -64,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, qrCodeBackgroundColor())
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(className: ComponentName) {
|
||||
walletService = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindListeners() {
|
||||
@ -121,6 +103,10 @@ class ReceiveActivity : AppCompatActivity(), WalletServiceObserver {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWalletServiceBound(walletService: WalletService) {
|
||||
viewModel.refreshSubaddresses(walletService, qrCodeBackgroundColor())
|
||||
}
|
||||
|
||||
override fun onSubaddressesUpdated() {
|
||||
walletService?.let { walletService ->
|
||||
viewModel.refreshSubaddresses(walletService, qrCodeBackgroundColor())
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
48
app/src/main/java/net/mynero/wallet/WalletActivity.kt
Normal file
48
app/src/main/java/net/mynero/wallet/WalletActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ 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>,
|
||||
@ -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(
|
||||
|
@ -27,6 +27,7 @@ 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
|
||||
|
||||
@ -38,8 +39,10 @@ class TransactionInfoAdapter(
|
||||
private var localDataSet: List<TransactionInfo> = emptyList()
|
||||
|
||||
fun submitList(dataSet: List<TransactionInfo>) {
|
||||
localDataSet = dataSet
|
||||
notifyDataSetChanged()
|
||||
if (localDataSet != dataSet) {
|
||||
localDataSet = dataSet
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun submitStreetMode(newStreetMode: Boolean) {
|
||||
|
52
app/src/main/java/net/mynero/wallet/adapter/WalletAdapter.kt
Normal file
52
app/src/main/java/net/mynero/wallet/adapter/WalletAdapter.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
3
app/src/main/java/net/mynero/wallet/model/Account.kt
Normal file
3
app/src/main/java/net/mynero/wallet/model/Account.kt
Normal file
@ -0,0 +1,3 @@
|
||||
package net.mynero.wallet.model
|
||||
|
||||
data class Account(val index: Int, val label: String, val balance: Balance)
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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?
|
||||
)
|
@ -1,8 +0,0 @@
|
||||
package net.mynero.wallet.model
|
||||
|
||||
enum class EnumTorState {
|
||||
STARTING,
|
||||
ON,
|
||||
STOPPING,
|
||||
OFF
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package net.mynero.wallet.model
|
||||
|
||||
class TorState {
|
||||
var state: EnumTorState = EnumTorState.OFF
|
||||
var progressIndicator: Int = 0
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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?)
|
||||
|
@ -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)
|
||||
}
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -7,25 +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_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"
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
57
app/src/main/res/layout/account_item.xml
Normal file
57
app/src/main/res/layout/account_item.xml
Normal 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>
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
74
app/src/main/res/layout/activity_wallet.xml
Normal file
74
app/src/main/res/layout/activity_wallet.xml
Normal 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>
|
@ -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>
|
39
app/src/main/res/layout/wallet_item.xml
Normal file
39
app/src/main/res/layout/wallet_item.xml
Normal 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>
|
@ -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"
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user