mirror of
https://github.com/m2049r/xmrwallet
synced 2025-09-02 15:53:04 +02:00
Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cdb29bbc2e | ||
![]() |
7b96baeca7 | ||
![]() |
0712efec78 | ||
![]() |
341df6c6a3 | ||
![]() |
ab8fb82c1b | ||
![]() |
22d9173cea | ||
![]() |
05720e63ab | ||
![]() |
cdc2b23257 | ||
![]() |
9f0e89719c | ||
![]() |
190726e61c | ||
![]() |
729fafdb48 | ||
![]() |
b7ae23ac64 | ||
![]() |
27885f2c86 | ||
![]() |
ce9046c7b5 | ||
![]() |
506e6ce017 | ||
![]() |
f07b439731 | ||
![]() |
ac70ba8424 | ||
![]() |
84ec1ef418 | ||
![]() |
b576a9de3d | ||
![]() |
148faa00e4 | ||
![]() |
e82b471c14 | ||
![]() |
9ed92e5117 | ||
![]() |
303b3aa354 | ||
![]() |
d801a50962 | ||
![]() |
9cd8a75dc6 | ||
![]() |
002dfd5d58 | ||
![]() |
54e54b2a8a | ||
![]() |
aa768596a4 | ||
![]() |
c4958f6c54 | ||
![]() |
2c2a5314d4 | ||
![]() |
669516c60b |
@@ -171,6 +171,10 @@ add_library(wallet-crypto STATIC IMPORTED)
|
||||
set_target_properties(wallet-crypto PROPERTIES IMPORTED_LOCATION
|
||||
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libwallet-crypto.a)
|
||||
|
||||
add_library(cryptonote_format_utils_basic STATIC IMPORTED)
|
||||
set_target_properties(cryptonote_format_utils_basic PROPERTIES IMPORTED_LOCATION
|
||||
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcryptonote_format_utils_basic.a)
|
||||
|
||||
#############
|
||||
# System
|
||||
#############
|
||||
@@ -193,6 +197,7 @@ target_link_libraries( monerujo
|
||||
wallet
|
||||
cryptonote_core
|
||||
cryptonote_basic
|
||||
cryptonote_format_utils_basic
|
||||
mnemonics
|
||||
ringct
|
||||
ringct_basic
|
||||
|
@@ -1,15 +1,15 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion '30.0.3'
|
||||
ndkVersion '17.2.4988734'
|
||||
defaultConfig {
|
||||
applicationId "com.m2049r.xmrwallet"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 1005
|
||||
versionName "2.0.5 'Puginarug'"
|
||||
targetSdkVersion 31
|
||||
versionCode 1303
|
||||
versionName "2.3.3 'Baldaŭ'"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
@@ -113,42 +113,46 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def getId(name) {
|
||||
def Properties props = new Properties()
|
||||
static def getId(name) {
|
||||
Properties props = new Properties()
|
||||
props.load(new FileInputStream(new File('monerujo.id')))
|
||||
return props[name]
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core:1.3.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.core:core:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation "com.squareup.okhttp3:okhttp:4.9.0"
|
||||
implementation "io.github.rburgst:okhttp-digest:2.5"
|
||||
implementation "com.jakewharton.timber:timber:4.7.1"
|
||||
implementation "com.squareup.okhttp3:okhttp:4.9.3"
|
||||
implementation "io.github.rburgst:okhttp-digest:2.6"
|
||||
implementation "com.jakewharton.timber:timber:5.0.1"
|
||||
|
||||
implementation 'com.nulab-inc:zxcvbn:1.3.0'
|
||||
implementation 'info.guardianproject.netcipher:netcipher:2.1.0'
|
||||
//implementation 'info.guardianproject.netcipher:netcipher-okhttp3:2.1.0'
|
||||
implementation fileTree(dir: 'libs/classes', include: ['*.jar'])
|
||||
implementation 'com.nulab-inc:zxcvbn:1.5.2'
|
||||
|
||||
implementation 'dnsjava:dnsjava:2.1.9'
|
||||
implementation 'org.jitsi:dnssecjava:1.2.0'
|
||||
implementation 'org.slf4j:slf4j-nop:1.7.30'
|
||||
implementation 'org.slf4j:slf4j-nop:1.7.36'
|
||||
implementation 'com.github.brnunes:swipeablerecyclerview:1.0.2'
|
||||
|
||||
implementation 'com.github.aelstad:keccakj:1.1.0'
|
||||
//noinspection GradleDependency
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.mockito:mockito-all:1.10.19"
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.3"
|
||||
testImplementation 'org.json:json:20211205'
|
||||
testImplementation 'net.jodah:concurrentunit:0.4.6'
|
||||
|
||||
testImplementation "junit:junit:$rootProject.ext.junitVersion"
|
||||
testImplementation "org.mockito:mockito-all:$rootProject.ext.mockitoVersion"
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
|
||||
testImplementation 'org.json:json:20180813'
|
||||
testImplementation 'net.jodah:concurrentunit:0.4.4'
|
||||
|
||||
compileOnly 'org.projectlombok:lombok:1.18.16'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.16'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.22'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.22'
|
||||
}
|
||||
|
@@ -4,28 +4,48 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="org.torproject.android.intent.action.START" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="org.torproject.android.intent.action.STATUS" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="org.torproject.android.REQUEST_HS_PORT" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="org.torproject.android.REQUEST_V3_ONION_SERVICE" />
|
||||
</intent>
|
||||
|
||||
<package android:name="org.torproject.android" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:preserveLegacyExternalStorage="true"
|
||||
android:name=".XmrWalletApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:preserveLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/MyMaterialTheme"
|
||||
android:theme="@style/MyMaterialThemeClassic"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|uiMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
@@ -34,27 +54,28 @@
|
||||
android:configChanges="orientation|keyboardHidden|uiMode"
|
||||
android:label="@string/wallet_activity_name"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="behind"/>
|
||||
android:screenOrientation="behind" />
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:configChanges="orientation|keyboardHidden|uiMode"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="locked">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data android:scheme="monero" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data android:scheme="bitcoin" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -87,4 +108,5 @@
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
</manifest>
|
@@ -229,7 +229,7 @@ std::vector<std::string> java2cpp(JNIEnv *env, jobject arrayList) {
|
||||
return result;
|
||||
}
|
||||
|
||||
jobject cpp2java(JNIEnv *env, const std::vector<std::string>& vector) {
|
||||
jobject cpp2java(JNIEnv *env, const std::vector<std::string> &vector) {
|
||||
|
||||
jmethodID java_util_ArrayList_ = env->GetMethodID(class_ArrayList, "<init>", "(I)V");
|
||||
jmethodID java_util_ArrayList_add = env->GetMethodID(class_ArrayList, "add",
|
||||
@@ -301,12 +301,13 @@ Java_com_m2049r_xmrwallet_model_WalletManager_openWalletJ(JNIEnv *env, jobject i
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_WalletManager_recoveryWalletJ(JNIEnv *env, jobject instance,
|
||||
jstring path, jstring password,
|
||||
jstring mnemonic,
|
||||
jstring mnemonic, jstring offset,
|
||||
jint networkType,
|
||||
jlong restoreHeight) {
|
||||
const char *_path = env->GetStringUTFChars(path, nullptr);
|
||||
const char *_password = env->GetStringUTFChars(password, nullptr);
|
||||
const char *_mnemonic = env->GetStringUTFChars(mnemonic, nullptr);
|
||||
const char *_offset = env->GetStringUTFChars(offset, nullptr);
|
||||
Monero::NetworkType _networkType = static_cast<Monero::NetworkType>(networkType);
|
||||
|
||||
Bitmonero::Wallet *wallet =
|
||||
@@ -315,11 +316,14 @@ Java_com_m2049r_xmrwallet_model_WalletManager_recoveryWalletJ(JNIEnv *env, jobje
|
||||
std::string(_password),
|
||||
std::string(_mnemonic),
|
||||
_networkType,
|
||||
(uint64_t) restoreHeight);
|
||||
(uint64_t) restoreHeight,
|
||||
1, // kdf_rounds
|
||||
std::string(_offset));
|
||||
|
||||
env->ReleaseStringUTFChars(path, _path);
|
||||
env->ReleaseStringUTFChars(password, _password);
|
||||
env->ReleaseStringUTFChars(mnemonic, _mnemonic);
|
||||
env->ReleaseStringUTFChars(offset, _offset);
|
||||
return reinterpret_cast<jlong>(wallet);
|
||||
}
|
||||
|
||||
@@ -531,6 +535,17 @@ Java_com_m2049r_xmrwallet_model_WalletManager_resolveOpenAlias(JNIEnv *env, jobj
|
||||
return env->NewStringUTF(resolvedAlias.c_str());
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_WalletManager_setProxy(JNIEnv *env, jobject instance,
|
||||
jstring address) {
|
||||
const char *_address = env->GetStringUTFChars(address, nullptr);
|
||||
bool rc =
|
||||
Bitmonero::WalletManagerFactory::getWalletManager()->setProxy(std::string(_address));
|
||||
env->ReleaseStringUTFChars(address, _address);
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
//TODO static std::tuple<bool, std::string, std::string, std::string, std::string> checkUpdates(const std::string &software, const std::string &subdir);
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
@@ -559,9 +574,12 @@ Java_com_m2049r_xmrwallet_model_WalletManager_closeJ(JNIEnv *env, jobject instan
|
||||
/**********************************/
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_Wallet_getSeed(JNIEnv *env, jobject instance) {
|
||||
Java_com_m2049r_xmrwallet_model_Wallet_getSeed(JNIEnv *env, jobject instance, jstring seedOffset) {
|
||||
const char *_seedOffset = env->GetStringUTFChars(seedOffset, nullptr);
|
||||
Bitmonero::Wallet *wallet = getHandle<Bitmonero::Wallet>(env, instance);
|
||||
return env->NewStringUTF(wallet->seed().c_str());
|
||||
jstring seed = env->NewStringUTF(wallet->seed(std::string(_seedOffset)).c_str());
|
||||
env->ReleaseStringUTFChars(seedOffset, _seedOffset);
|
||||
return seed;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
@@ -727,6 +745,16 @@ Java_com_m2049r_xmrwallet_model_Wallet_getConnectionStatusJ(JNIEnv *env, jobject
|
||||
//TODO virtual void setTrustedDaemon(bool arg) = 0;
|
||||
//TODO virtual bool trustedDaemon() const = 0;
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_Wallet_setProxy(JNIEnv *env, jobject instance,
|
||||
jstring address) {
|
||||
const char *_address = env->GetStringUTFChars(address, nullptr);
|
||||
Bitmonero::Wallet *wallet = getHandle<Bitmonero::Wallet>(env, instance);
|
||||
bool rc = wallet->setProxy(std::string(_address));
|
||||
env->ReleaseStringUTFChars(address, _address);
|
||||
return rc;
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_Wallet_getBalance(JNIEnv *env, jobject instance,
|
||||
jint accountIndex) {
|
||||
@@ -914,7 +942,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_refreshAsync(JNIEnv *env, jobject instanc
|
||||
|
||||
//virtual void rescanBlockchainAsync() = 0;
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_Wallet_rescanBlockchainAsync(JNIEnv *env, jobject instance) {
|
||||
Java_com_m2049r_xmrwallet_model_Wallet_rescanBlockchainAsyncJ(JNIEnv *env, jobject instance) {
|
||||
Bitmonero::Wallet *wallet = getHandle<Bitmonero::Wallet>(env, instance);
|
||||
wallet->rescanBlockchainAsync();
|
||||
}
|
||||
@@ -1241,7 +1269,7 @@ jobject newTransactionInfo(JNIEnv *env, Bitmonero::TransactionInfo *info) {
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
jobject cpp2java(JNIEnv *env, const std::vector<Bitmonero::TransactionInfo *>& vector) {
|
||||
jobject cpp2java(JNIEnv *env, const std::vector<Bitmonero::TransactionInfo *> &vector) {
|
||||
|
||||
jmethodID java_util_ArrayList_ = env->GetMethodID(class_ArrayList, "<init>", "(I)V");
|
||||
jmethodID java_util_ArrayList_add = env->GetMethodID(class_ArrayList, "add",
|
||||
|
@@ -77,7 +77,7 @@ public class Dispatcher implements PeerRetriever.OnGetPeers {
|
||||
final NodeInfo nodeInfo = retrievedPeer.getNodeInfo();
|
||||
Timber.d("Retrieved %s", nodeInfo);
|
||||
if ((nodeInfo.isValid() || nodeInfo.isFavourite())) {
|
||||
nodeInfo.setName();
|
||||
nodeInfo.setDefaultName();
|
||||
rpcNodes.add(nodeInfo);
|
||||
Timber.d("RPC: %s", nodeInfo);
|
||||
// the following is not totally correct but it works (otherwise we need to
|
||||
|
@@ -27,6 +27,7 @@ import android.nfc.NfcAdapter;
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.tech.Ndef;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -180,7 +181,7 @@ public class BaseActivity extends SecureActivity
|
||||
return;
|
||||
nfcPendingIntent = PendingIntent.getActivity(this, 0,
|
||||
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||
0);
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
}
|
||||
|
||||
private void processNfcIntent(Intent intent) {
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,6 @@ import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -59,7 +58,6 @@ import java.text.NumberFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import timber.log.Timber;
|
||||
@@ -219,8 +217,8 @@ public class NodeFragment extends Fragment
|
||||
activityCallback.setNode(nodeItem); // this marks it as selected & saves it as well
|
||||
nodeItem.setSelecting(false);
|
||||
try {
|
||||
Objects.requireNonNull(getActivity()).runOnUiThread(() -> nodesAdapter.allowClick(true));
|
||||
} catch (NullPointerException ex) {
|
||||
requireActivity().runOnUiThread(() -> nodesAdapter.allowClick(true));
|
||||
} catch (IllegalStateException ex) {
|
||||
// it's ok
|
||||
}
|
||||
});
|
||||
@@ -403,16 +401,12 @@ public class NodeFragment extends Fragment
|
||||
etNodeHost.setError(getString(R.string.node_host_empty));
|
||||
return false;
|
||||
}
|
||||
final boolean setHostSuccess = Helper.runWithNetwork(new Helper.Action() {
|
||||
@Override
|
||||
public boolean run() {
|
||||
try {
|
||||
nodeInfo.setHost(host);
|
||||
return true;
|
||||
} catch (UnknownHostException ex) {
|
||||
etNodeHost.setError(getString(R.string.node_host_unresolved));
|
||||
return false;
|
||||
}
|
||||
final boolean setHostSuccess = Helper.runWithNetwork(() -> {
|
||||
try {
|
||||
nodeInfo.setHost(host);
|
||||
return true;
|
||||
} catch (UnknownHostException ex) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!setHostSuccess) {
|
||||
@@ -421,14 +415,7 @@ public class NodeFragment extends Fragment
|
||||
}
|
||||
etNodeHost.setError(null);
|
||||
nodeInfo.setRpcPort(port);
|
||||
// setName() may trigger reverse DNS
|
||||
Helper.runWithNetwork(new Helper.Action() {
|
||||
@Override
|
||||
public boolean run() {
|
||||
nodeInfo.setName(etNodeName.getEditText().getText().toString().trim());
|
||||
return true;
|
||||
}
|
||||
});
|
||||
nodeInfo.setName(etNodeName.getEditText().getText().toString().trim());
|
||||
nodeInfo.setUsername(etNodeUser.getEditText().getText().toString().trim());
|
||||
nodeInfo.setPassword(etNodePass.getEditText().getText().toString()); // no trim for pw
|
||||
return true;
|
||||
@@ -532,20 +519,10 @@ public class NodeFragment extends Fragment
|
||||
@Override
|
||||
public void onShow(final DialogInterface dialog) {
|
||||
Button testButton = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
testButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
test();
|
||||
}
|
||||
});
|
||||
testButton.setOnClickListener(view -> test());
|
||||
|
||||
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
apply();
|
||||
}
|
||||
});
|
||||
button.setOnClickListener(view -> apply());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -553,15 +530,13 @@ public class NodeFragment extends Fragment
|
||||
editDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
etNodePass.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
editDialog.getButton(DialogInterface.BUTTON_NEUTRAL).requestFocus();
|
||||
test();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
etNodePass.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
editDialog.getButton(DialogInterface.BUTTON_NEUTRAL).requestFocus();
|
||||
test();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -589,6 +564,7 @@ public class NodeFragment extends Fragment
|
||||
} else {
|
||||
nodesAdapter.setNodes();
|
||||
}
|
||||
nodesAdapter.notifyItemChanged(nodeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -46,10 +46,7 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.ShareActionProvider;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.view.MenuItemCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
@@ -89,6 +86,7 @@ public class ReceiveFragment extends Fragment {
|
||||
private ImageView ivQrCodeFull;
|
||||
private EditText etDummy;
|
||||
private ImageButton bCopyAddress;
|
||||
private MenuItem shareItem;
|
||||
|
||||
private Wallet wallet = null;
|
||||
private boolean isMyWallet = false;
|
||||
@@ -128,6 +126,7 @@ public class ReceiveFragment extends Fragment {
|
||||
evAmount.setOnNewAmountListener(xmr -> {
|
||||
Timber.d("new amount = %s", xmr);
|
||||
generateQr();
|
||||
if (shareRequested && (xmr != null)) share();
|
||||
});
|
||||
|
||||
evAmount.setOnFailedExchangeListener(() -> {
|
||||
@@ -211,39 +210,38 @@ public class ReceiveFragment extends Fragment {
|
||||
setSharedElementEnterTransition(transform);
|
||||
}
|
||||
|
||||
private ShareActionProvider shareActionProvider;
|
||||
private boolean shareRequested = false;
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.receive_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
// Locate MenuItem with ShareActionProvider
|
||||
MenuItem item = menu.findItem(R.id.menu_item_share);
|
||||
|
||||
// Fetch and store ShareActionProvider
|
||||
shareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(item);
|
||||
|
||||
shareActionProvider.setOnShareTargetSelectedListener(new ShareActionProvider.OnShareTargetSelectedListener() {
|
||||
@Override
|
||||
public boolean onShareTargetSelected(ShareActionProvider shareActionProvider, Intent intent) {
|
||||
saveQrCode(); // save it only if we need it
|
||||
return false;
|
||||
shareItem = menu.findItem(R.id.menu_item_share);
|
||||
shareItem.setOnMenuItemClickListener(item -> {
|
||||
if (shareRequested) return true;
|
||||
shareRequested = true;
|
||||
if (!qrValid) {
|
||||
evAmount.doExchange();
|
||||
} else {
|
||||
share();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void setShareIntent() {
|
||||
if (shareActionProvider != null) {
|
||||
if (qrValid) {
|
||||
shareActionProvider.setShareIntent(getShareIntent());
|
||||
} else {
|
||||
shareActionProvider.setShareIntent(null);
|
||||
}
|
||||
private void share() {
|
||||
shareRequested = false;
|
||||
if (saveQrCode()) {
|
||||
final Intent sendIntent = getSendIntent();
|
||||
if (sendIntent != null)
|
||||
startActivity(Intent.createChooser(sendIntent, null));
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.message_qr_failed), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveQrCode() {
|
||||
private boolean saveQrCode() {
|
||||
if (!qrValid) throw new IllegalStateException("trying to save null qr code!");
|
||||
|
||||
File cachePath = new File(getActivity().getCacheDir(), "images");
|
||||
@@ -255,33 +253,35 @@ public class ReceiveFragment extends Fragment {
|
||||
Bitmap qrBitmap = ((BitmapDrawable) ivQrCode.getDrawable()).getBitmap();
|
||||
qrBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
stream.close();
|
||||
return true;
|
||||
} catch (IOException ex) {
|
||||
Timber.e(ex);
|
||||
// make sure we don't share an old qr code
|
||||
if (!png.delete()) throw new IllegalStateException("cannot delete old qr code");
|
||||
// if we manage to delete it, the URI points to nothing and the user gets a toast with the error
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Intent getShareIntent() {
|
||||
File imagePath = new File(getActivity().getCacheDir(), "images");
|
||||
private Intent getSendIntent() {
|
||||
File imagePath = new File(requireActivity().getCacheDir(), "images");
|
||||
File png = new File(imagePath, "QR.png");
|
||||
Uri contentUri = FileProvider.getUriForFile(getActivity(),
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider", png);
|
||||
Uri contentUri = FileProvider.getUriForFile(requireActivity(), BuildConfig.APPLICATION_ID + ".fileprovider", png);
|
||||
if (contentUri != null) {
|
||||
Intent shareIntent = new Intent();
|
||||
shareIntent.setAction(Intent.ACTION_SEND);
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // temp permission for receiving app to read this file
|
||||
shareIntent.setDataAndType(contentUri, getActivity().getContentResolver().getType(contentUri));
|
||||
shareIntent.setTypeAndNormalize("image/png");
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, bcData.getUriString());
|
||||
if (bcData != null)
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, bcData.getUriString());
|
||||
return shareIntent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void copyAddress() {
|
||||
Helper.clipBoardCopy(Objects.requireNonNull(getActivity()), getString(R.string.label_copy_address), subaddress.getAddress());
|
||||
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_address), subaddress.getAddress());
|
||||
Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@@ -291,7 +291,6 @@ public class ReceiveFragment extends Fragment {
|
||||
if (qrValid) {
|
||||
ivQrCode.setImageBitmap(null);
|
||||
qrValid = false;
|
||||
setShareIntent();
|
||||
if (isLoaded)
|
||||
tvQrCode.setVisibility(View.VISIBLE);
|
||||
}
|
||||
@@ -300,7 +299,6 @@ public class ReceiveFragment extends Fragment {
|
||||
void setQR(Bitmap qr) {
|
||||
ivQrCode.setImageBitmap(qr);
|
||||
qrValid = true;
|
||||
setShareIntent();
|
||||
tvQrCode.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@@ -462,8 +460,8 @@ public class ReceiveFragment extends Fragment {
|
||||
subaddress = newSubaddress;
|
||||
final Context context = getContext();
|
||||
Spanned label = Html.fromHtml(context.getString(R.string.receive_subaddress,
|
||||
Integer.toHexString(ContextCompat.getColor(context, R.color.monerujoGreen) & 0xFFFFFF),
|
||||
Integer.toHexString(ContextCompat.getColor(context, R.color.monerujoBackground) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF),
|
||||
subaddress.getDisplayLabel(), subaddress.getAddress()));
|
||||
tvAddress.setText(label);
|
||||
generateQr();
|
||||
|
@@ -66,11 +66,7 @@ public abstract class SecureActivity extends AppCompatActivity {
|
||||
|
||||
Locale locale = LocaleHelper.getPreferredLocale(this);
|
||||
if (locale != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
config.setLocale(locale);
|
||||
} else {
|
||||
config.locale = locale;
|
||||
}
|
||||
config.setLocale(locale);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
125
app/src/main/java/com/m2049r/xmrwallet/SettingsFragment.java
Normal file
125
app/src/main/java/com/m2049r/xmrwallet/SettingsFragment.java
Normal file
@@ -0,0 +1,125 @@
|
||||
package com.m2049r.xmrwallet;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.m2049r.xmrwallet.dialog.AboutFragment;
|
||||
import com.m2049r.xmrwallet.dialog.CreditsFragment;
|
||||
import com.m2049r.xmrwallet.dialog.PrivacyFragment;
|
||||
import com.m2049r.xmrwallet.util.DayNightMode;
|
||||
import com.m2049r.xmrwallet.util.LocaleHelper;
|
||||
import com.m2049r.xmrwallet.util.NightmodeHelper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragmentCompat
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||
|
||||
findPreference(getString(R.string.about_info)).setOnPreferenceClickListener(preference -> {
|
||||
AboutFragment.display(getParentFragmentManager());
|
||||
return true;
|
||||
});
|
||||
findPreference(getString(R.string.privacy_info)).setOnPreferenceClickListener(preference -> {
|
||||
PrivacyFragment.display(getParentFragmentManager());
|
||||
return true;
|
||||
});
|
||||
findPreference(getString(R.string.credits_info)).setOnPreferenceClickListener(preference -> {
|
||||
CreditsFragment.display(getParentFragmentManager());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(getString(R.string.preferred_locale))) {
|
||||
activity.recreate();
|
||||
} else if (key.equals(getString(R.string.preferred_nightmode))) {
|
||||
NightmodeHelper.setNightMode(DayNightMode.valueOf(sharedPreferences.getString(key, "AUTO")));
|
||||
} else if (key.equals(getString(R.string.preferred_theme))) {
|
||||
ThemeHelper.setTheme((Activity) activity, sharedPreferences.getString(key, "Classic"));
|
||||
activity.recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private SettingsFragment.Listener activity;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof SettingsFragment.Listener) {
|
||||
activity = (SettingsFragment.Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context + " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume()");
|
||||
activity.setSubtitle(getString(R.string.menu_settings));
|
||||
activity.setToolbarButton(Toolbar.BUTTON_BACK);
|
||||
populateLanguages();
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void setSubtitle(String title);
|
||||
|
||||
void recreate();
|
||||
|
||||
void setTheme(@StyleRes final int resId);
|
||||
}
|
||||
|
||||
public void populateLanguages() {
|
||||
ListPreference language = findPreference(getString(R.string.preferred_locale));
|
||||
assert language != null;
|
||||
|
||||
final ArrayList<Locale> availableLocales = LocaleHelper.getAvailableLocales(requireContext());
|
||||
Collections.sort(availableLocales, (locale1, locale2) -> {
|
||||
String localeString1 = LocaleHelper.getDisplayName(locale1, true);
|
||||
String localeString2 = LocaleHelper.getDisplayName(locale2, true);
|
||||
return localeString1.compareTo(localeString2);
|
||||
});
|
||||
|
||||
String[] localeDisplayNames = new String[1 + availableLocales.size()];
|
||||
localeDisplayNames[0] = getString(R.string.language_system_default);
|
||||
for (int i = 1; i < localeDisplayNames.length; i++) {
|
||||
localeDisplayNames[i] = LocaleHelper.getDisplayName(availableLocales.get(i - 1), true);
|
||||
}
|
||||
language.setEntries(localeDisplayNames);
|
||||
|
||||
String[] languageTags = new String[1 + availableLocales.size()];
|
||||
languageTags[0] = "";
|
||||
for (int i = 1; i < languageTags.length; i++) {
|
||||
languageTags[i] = availableLocales.get(i - 1).toLanguageTag();
|
||||
}
|
||||
language.setEntryValues(languageTags);
|
||||
}
|
||||
}
|
@@ -67,6 +67,8 @@ public class SubaddressFragment extends Fragment implements SubaddressInfoAdapte
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void showSubaddress(View view, final int subaddressIndex);
|
||||
|
||||
void saveWallet();
|
||||
}
|
||||
|
||||
public interface ProgressListener {
|
||||
@@ -217,7 +219,9 @@ public class SubaddressFragment extends Fragment implements SubaddressInfoAdapte
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
if (params.length != 0) return false;
|
||||
wallet.getNewSubaddress();
|
||||
wallet.store();
|
||||
if (activityCallback != null) {
|
||||
activityCallback.saveWallet();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -35,7 +35,6 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.transition.MaterialContainerTransform;
|
||||
@@ -220,8 +219,8 @@ public class TxFragment extends Fragment {
|
||||
final Context ctx = getContext();
|
||||
Spanned label = Html.fromHtml(ctx.getString(R.string.tx_account_formatted,
|
||||
info.accountIndex, info.addressIndex,
|
||||
Integer.toHexString(ContextCompat.getColor(ctx, R.color.monerujoGreen) & 0xFFFFFF),
|
||||
Integer.toHexString(ContextCompat.getColor(ctx, R.color.monerujoBackground) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),
|
||||
subaddress.getDisplayLabel()));
|
||||
tvAccount.setText(label);
|
||||
tvAccount.setOnClickListener(v -> activityCallback.showSubaddress(v, info.addressIndex));
|
||||
@@ -266,13 +265,13 @@ public class TxFragment extends Fragment {
|
||||
if (info.isFailed) {
|
||||
tvTxAmount.setText(getString(R.string.tx_list_amount_failed, Wallet.getDisplayAmount(info.amount)));
|
||||
tvTxFee.setText(getString(R.string.tx_list_failed_text));
|
||||
setTxColour(ContextCompat.getColor(getContext(), R.color.tx_failed));
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
|
||||
} else if (info.isPending) {
|
||||
setTxColour(ContextCompat.getColor(getContext(), R.color.tx_pending));
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
|
||||
} else if (info.direction == TransactionInfo.Direction.Direction_In) {
|
||||
setTxColour(ContextCompat.getColor(getContext(), R.color.tx_plus));
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor));
|
||||
} else {
|
||||
setTxColour(ContextCompat.getColor(getContext(), R.color.tx_minus));
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.negativeColor));
|
||||
}
|
||||
Set<String> destinations = new HashSet<>();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
@@ -356,6 +356,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
Timber.d("onCreate()");
|
||||
ThemeHelper.setPreferred(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
// activity restarted
|
||||
@@ -385,7 +386,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||
case Toolbar.BUTTON_CLOSE:
|
||||
finish();
|
||||
break;
|
||||
case Toolbar.BUTTON_CREDITS:
|
||||
case Toolbar.BUTTON_SETTINGS:
|
||||
Toast.makeText(WalletActivity.this, getString(R.string.label_credits), Toast.LENGTH_SHORT).show();
|
||||
case Toolbar.BUTTON_NONE:
|
||||
default:
|
||||
@@ -505,6 +506,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||
Timber.d("onResume()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveWallet() {
|
||||
if (mIsBound) { // no point in talking to unbound service
|
||||
Intent intent = new Intent(getApplicationContext(), WalletService.class);
|
||||
@@ -578,7 +580,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||
try {
|
||||
final WalletFragment walletFragment = getWalletFragment();
|
||||
if (wallet.isSynchronized()) {
|
||||
Timber.d("onRefreshed() synced");
|
||||
releaseWakeLock(RELEASE_WAKE_LOCK_DELAY); // the idea is to stay awake until synced
|
||||
if (!synced) { // first sync
|
||||
onProgress(-1);
|
||||
@@ -610,9 +611,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||
@Override
|
||||
public void onWalletStored(final boolean success) {
|
||||
runOnUiThread(() -> {
|
||||
if (success) {
|
||||
Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_unloaded), Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
if (!success) {
|
||||
Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_unload_failed), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
@@ -942,6 +941,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Timber.d("onRequestPermissionsResult()");
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == Helper.PERMISSIONS_REQUEST_CAMERA) { // If request is cancelled, the result arrays are empty.
|
||||
if (grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
|
@@ -52,13 +52,13 @@ import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
@@ -112,7 +112,8 @@ public class WalletFragment extends Fragment
|
||||
llBalance = view.findViewById(R.id.llBalance);
|
||||
flExchange = view.findViewById(R.id.flExchange);
|
||||
((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable().
|
||||
setColorFilter(getResources().getColor(R.color.progress_circle),
|
||||
setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tvProgress = view.findViewById(R.id.tvProgress);
|
||||
@@ -128,7 +129,7 @@ public class WalletFragment extends Fragment
|
||||
currencies.add(Helper.BASE_CRYPTO);
|
||||
if (Helper.SHOW_EXCHANGERATES)
|
||||
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(Objects.requireNonNull(getContext()), R.layout.item_spinner_balance, currencies);
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(requireContext(), R.layout.item_spinner_balance, currencies);
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
sCurrency.setAdapter(spinnerAdapter);
|
||||
|
||||
@@ -224,11 +225,13 @@ public class WalletFragment extends Fragment
|
||||
}
|
||||
|
||||
void showUnconfirmed(double unconfirmedAmount) {
|
||||
if (!activityCallback.isStreetMode()) {
|
||||
if (activityCallback.isStreetMode() || unconfirmedAmount == 0) {
|
||||
tvUnconfirmedAmount.setText(null);
|
||||
tvUnconfirmedAmount.setVisibility(View.GONE);
|
||||
} else {
|
||||
String unconfirmed = Helper.getFormattedAmount(unconfirmedAmount, true);
|
||||
tvUnconfirmedAmount.setText(getResources().getString(R.string.xmr_unconfirmed_amount, unconfirmed));
|
||||
} else {
|
||||
tvUnconfirmedAmount.setText(null);
|
||||
tvUnconfirmedAmount.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,13 +349,18 @@ public class WalletFragment extends Fragment
|
||||
// if account index has changed scroll to top?
|
||||
private int accountIndex = 0;
|
||||
|
||||
public void onRefreshed(final Wallet wallet, final boolean full) {
|
||||
public void onRefreshed(final Wallet wallet, boolean full) {
|
||||
Timber.d("onRefreshed(%b)", full);
|
||||
|
||||
if (adapter.needsTransactionUpdateOnNewBlock()) {
|
||||
wallet.refreshHistory();
|
||||
full = true;
|
||||
}
|
||||
if (full) {
|
||||
List<TransactionInfo> list = new ArrayList<>();
|
||||
final long streetHeight = activityCallback.getStreetModeHeight();
|
||||
Timber.d("StreetHeight=%d", streetHeight);
|
||||
wallet.refreshHistory();
|
||||
for (TransactionInfo info : wallet.getHistory().getAll()) {
|
||||
Timber.d("TxHeight=%d, Label=%s", info.blockheight, info.subaddressLabel);
|
||||
if ((info.isPending || (info.blockheight >= streetHeight))
|
||||
@@ -561,7 +569,7 @@ public class WalletFragment extends Fragment
|
||||
//TODO figure out why gunther disappears on return from send although he is still set
|
||||
if (enable) {
|
||||
if (streetGunther == null)
|
||||
streetGunther = ContextCompat.getDrawable(Objects.requireNonNull(getContext()), R.drawable.ic_gunther_streetmode);
|
||||
streetGunther = ContextCompat.getDrawable(requireContext(), R.drawable.ic_gunther_streetmode);
|
||||
ivStreetGunther.setImageDrawable(streetGunther);
|
||||
} else
|
||||
ivStreetGunther.setImageDrawable(null);
|
||||
|
@@ -21,22 +21,29 @@ import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
|
||||
import com.m2049r.xmrwallet.BuildConfig;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.util.LocaleHelper;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
import com.m2049r.xmrwallet.util.NightmodeHelper;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class XmrWalletApplication extends Application {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
FragmentManager.enableNewStateManager(false);
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(new Timber.DebugTree());
|
||||
}
|
||||
|
||||
NightmodeHelper.setPreferredNightmode(this);
|
||||
|
||||
NetCipherHelper.createInstance(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,7 +52,7 @@ public class XmrWalletApplication extends Application {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration configuration) {
|
||||
public void onConfigurationChanged(@NonNull Configuration configuration) {
|
||||
super.onConfigurationChanged(configuration);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
LocaleHelper.updateSystemDefaultLocale(configuration.getLocales().get(0));
|
||||
|
@@ -28,7 +28,11 @@ public enum DefaultNodes {
|
||||
SUPPORTXMR("node.supportxmr.com:18081"),
|
||||
HASHVAULT("nodes.hashvault.pro:18081"),
|
||||
MONEROWORLD("node.moneroworld.com:18089"),
|
||||
XMRTW("opennode.xmr-tw.org:18089");
|
||||
XMRTW("opennode.xmr-tw.org:18089"),
|
||||
MONERUJO_ONION("monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion:18081/mainnet/monerujo.onion"),
|
||||
Criminales78("56wl7y2ebhamkkiza4b7il4mrzwtyvpdym7bm2bkg3jrei2je646k3qd.onion:18089/mainnet/Criminales78.onion"),
|
||||
xmrfail("mxcd4577fldb3ppzy7obmmhnu3tf57gbcbd4qhwr2kxyjj2qi3dnbfqd.onion:18081/mainnet/xmrfail.onion"),
|
||||
boldsuck("6dsdenp6vjkvqzy4wzsnzn6wixkdzihx3khiumyzieauxuxslmcaeiad.onion:18081/mainnet/boldsuck.onion");
|
||||
|
||||
@Getter
|
||||
private final String uri;
|
||||
|
@@ -18,6 +18,7 @@ package com.m2049r.xmrwallet.data;
|
||||
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.OnionHelper;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.InetAddress;
|
||||
@@ -35,11 +36,63 @@ public class Node {
|
||||
static public final String STAGENET = "stagenet";
|
||||
static public final String TESTNET = "testnet";
|
||||
|
||||
static class Address {
|
||||
final private InetAddress inet;
|
||||
final private String onion;
|
||||
|
||||
public boolean isOnion() {
|
||||
return onion != null;
|
||||
}
|
||||
|
||||
public String getHostName() {
|
||||
if (inet != null) {
|
||||
return inet.getHostName();
|
||||
} else {
|
||||
return onion;
|
||||
}
|
||||
}
|
||||
|
||||
public String getHostAddress() {
|
||||
if (inet != null) {
|
||||
return inet.getHostAddress();
|
||||
} else {
|
||||
return onion;
|
||||
}
|
||||
}
|
||||
|
||||
private Address(InetAddress address, String onion) {
|
||||
this.inet = address;
|
||||
this.onion = onion;
|
||||
}
|
||||
|
||||
static Address of(InetAddress address) {
|
||||
return new Address(address, null);
|
||||
}
|
||||
|
||||
static Address of(String host) throws UnknownHostException {
|
||||
if (OnionHelper.isOnionHost(host)) {
|
||||
return new Address(null, host);
|
||||
} else {
|
||||
return new Address(InetAddress.getByName(host), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getHostAddress().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (other instanceof Address) && (getHostAddress().equals(((Address) other).getHostAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
private String name = null;
|
||||
@Getter
|
||||
final private NetworkType networkType;
|
||||
InetAddress hostAddress;
|
||||
Address hostAddress;
|
||||
@Getter
|
||||
private String host;
|
||||
@Getter
|
||||
@@ -74,6 +127,10 @@ public class Node {
|
||||
&& (networkType == anotherNode.networkType));
|
||||
}
|
||||
|
||||
public boolean isOnion() {
|
||||
return hostAddress.isOnion();
|
||||
}
|
||||
|
||||
static public Node fromString(String nodeString) {
|
||||
try {
|
||||
return new Node(nodeString);
|
||||
@@ -205,7 +262,7 @@ public class Node {
|
||||
// constructor used for created nodes from retrieved peer lists
|
||||
public Node(InetSocketAddress socketAddress) {
|
||||
this();
|
||||
this.hostAddress = socketAddress.getAddress();
|
||||
this.hostAddress = Address.of(socketAddress.getAddress());
|
||||
this.host = socketAddress.getHostString();
|
||||
this.rpcPort = 0; // unknown
|
||||
this.levinPort = socketAddress.getPort();
|
||||
@@ -225,17 +282,25 @@ public class Node {
|
||||
if ((host == null) || (host.isEmpty()))
|
||||
throw new UnknownHostException("loopback not supported (yet?)");
|
||||
this.host = host;
|
||||
this.hostAddress = InetAddress.getByName(host);
|
||||
this.hostAddress = Address.of(host);
|
||||
}
|
||||
|
||||
public void setName() {
|
||||
if (name == null)
|
||||
this.name = hostAddress.getHostName();
|
||||
public void setDefaultName() {
|
||||
if (name != null) return;
|
||||
String nodeName = hostAddress.getHostName();
|
||||
if (hostAddress.isOnion()) {
|
||||
nodeName = nodeName.substring(0, nodeName.length() - ".onion".length());
|
||||
if (nodeName.length() > 16) {
|
||||
nodeName = nodeName.substring(0, 8) + "…" + nodeName.substring(nodeName.length() - 6);
|
||||
}
|
||||
nodeName = nodeName + ".onion";
|
||||
}
|
||||
this.name = nodeName;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
if ((name == null) || (name.isEmpty()))
|
||||
this.name = hostAddress.getHostName();
|
||||
setDefaultName();
|
||||
else
|
||||
this.name = name;
|
||||
}
|
||||
|
@@ -16,14 +16,19 @@
|
||||
|
||||
package com.m2049r.xmrwallet.data;
|
||||
|
||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
|
||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
|
||||
import com.burgstaller.okhttp.digest.CachingAuthenticator;
|
||||
import com.burgstaller.okhttp.digest.Credentials;
|
||||
import com.burgstaller.okhttp.digest.DigestAuthenticator;
|
||||
import android.content.Context;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.m2049r.levin.scanner.LevinPeer;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper.Request;
|
||||
import com.m2049r.xmrwallet.util.NodePinger;
|
||||
import com.m2049r.xmrwallet.util.OkHttpHelper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -32,17 +37,12 @@ import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Calendar;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import timber.log.Timber;
|
||||
@@ -94,7 +94,7 @@ public class NodeInfo extends Node {
|
||||
synchronized public SocketAddress getLevinSocketAddress() {
|
||||
if (levinSocketAddress == null) {
|
||||
// use default peer port if not set - very few peers use nonstandard port
|
||||
levinSocketAddress = new InetSocketAddress(hostAddress, getDefaultLevinPort());
|
||||
levinSocketAddress = new InetSocketAddress(hostAddress.getHostAddress(), getDefaultLevinPort());
|
||||
}
|
||||
return levinSocketAddress;
|
||||
}
|
||||
@@ -180,7 +180,7 @@ public class NodeInfo extends Node {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final int HTTP_TIMEOUT = OkHttpHelper.HTTP_TIMEOUT;
|
||||
private static final int HTTP_TIMEOUT = 1000; //ms
|
||||
public static final double PING_GOOD = HTTP_TIMEOUT / 3.0; //ms
|
||||
public static final double PING_MEDIUM = 2 * PING_GOOD; //ms
|
||||
public static final double PING_BAD = HTTP_TIMEOUT;
|
||||
@@ -196,32 +196,29 @@ public class NodeInfo extends Node {
|
||||
return result;
|
||||
}
|
||||
|
||||
private Request rpcServiceRequest(int port) {
|
||||
final HttpUrl url = new HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host(getHost())
|
||||
.port(port)
|
||||
.addPathSegment("json_rpc")
|
||||
.build();
|
||||
final String json = "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}";
|
||||
return new Request(url, json, getUsername(), getPassword());
|
||||
}
|
||||
|
||||
private boolean testRpcService(int port) {
|
||||
Timber.d("Testing %s", toNodeString());
|
||||
clear();
|
||||
if (hostAddress.isOnion() && !NetCipherHelper.isTor()) {
|
||||
tested = true; // sortof
|
||||
responseCode = 418; // I'm a teapot - or I need an Onion - who knows
|
||||
return false; // autofail
|
||||
}
|
||||
try {
|
||||
OkHttpClient client = OkHttpHelper.getEagerClient();
|
||||
if (!getUsername().isEmpty()) {
|
||||
final DigestAuthenticator authenticator =
|
||||
new DigestAuthenticator(new Credentials(getUsername(), getPassword()));
|
||||
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
|
||||
client = client.newBuilder()
|
||||
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
|
||||
.addInterceptor(new AuthenticationCacheInterceptor(authCache))
|
||||
.build();
|
||||
}
|
||||
HttpUrl url = new HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host(getHostAddress())
|
||||
.port(port)
|
||||
.addPathSegment("json_rpc")
|
||||
.build();
|
||||
final RequestBody reqBody = RequestBody
|
||||
.create(MediaType.parse("application/json"),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}");
|
||||
Request request = OkHttpHelper.getPostRequest(url, reqBody);
|
||||
long ta = System.nanoTime();
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
try (Response response = rpcServiceRequest(port).execute()) {
|
||||
Timber.d("%s: %s", response.code(), response.request().url());
|
||||
responseTime = (System.nanoTime() - ta) / 1000000.0;
|
||||
responseCode = response.code();
|
||||
if (response.isSuccessful()) {
|
||||
@@ -243,7 +240,7 @@ public class NodeInfo extends Node {
|
||||
}
|
||||
}
|
||||
} catch (IOException | JSONException ex) {
|
||||
Timber.d(ex);
|
||||
Timber.d("EX: %s", ex.getMessage()); //TODO: do something here (show error?)
|
||||
} finally {
|
||||
tested = true;
|
||||
}
|
||||
@@ -264,4 +261,43 @@ public class NodeInfo extends Node {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static public final int STALE_NODE_HOURS = 2;
|
||||
|
||||
public void showInfo(TextView view, String info, boolean isError) {
|
||||
final Context ctx = view.getContext();
|
||||
final Spanned text = Html.fromHtml(ctx.getString(R.string.status,
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),
|
||||
(hostAddress.isOnion() ? " .onion " : ""), " " + info));
|
||||
view.setText(text);
|
||||
if (isError)
|
||||
view.setTextColor(ThemeHelper.getThemedColor(ctx, R.attr.colorError));
|
||||
else
|
||||
view.setTextColor(ThemeHelper.getThemedColor(ctx, android.R.attr.textColorSecondary));
|
||||
}
|
||||
|
||||
public void showInfo(TextView view) {
|
||||
if (!isTested()) {
|
||||
showInfo(view, "", false);
|
||||
return;
|
||||
}
|
||||
final Context ctx = view.getContext();
|
||||
final long now = Calendar.getInstance().getTimeInMillis() / 1000;
|
||||
final long secs = (now - timestamp);
|
||||
final long mins = secs / 60;
|
||||
final long hours = mins / 60;
|
||||
final long days = hours / 24;
|
||||
String info;
|
||||
if (mins < 2) {
|
||||
info = ctx.getString(R.string.node_updated_now, secs);
|
||||
} else if (hours < 2) {
|
||||
info = ctx.getString(R.string.node_updated_mins, mins);
|
||||
} else if (days < 2) {
|
||||
info = ctx.getString(R.string.node_updated_hours, hours);
|
||||
} else {
|
||||
info = ctx.getString(R.string.node_updated_days, days);
|
||||
}
|
||||
showInfo(view, info, hours >= STALE_NODE_HOURS);
|
||||
}
|
||||
}
|
||||
|
@@ -17,13 +17,17 @@
|
||||
package com.m2049r.xmrwallet.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -31,15 +35,20 @@ import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
public class HelpFragment extends DialogFragment {
|
||||
static final String TAG = "HelpFragment";
|
||||
private static final String HELP_ID = "HELP_ID";
|
||||
private static final String TOR_BUTTON = "TOR";
|
||||
|
||||
public static HelpFragment newInstance(int helpResourceId) {
|
||||
HelpFragment fragment = new HelpFragment();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(HELP_ID, helpResourceId);
|
||||
// a hack for the tor button
|
||||
if (helpResourceId == R.string.help_tor)
|
||||
bundle.putInt(TOR_BUTTON, 7);
|
||||
fragment.setArguments(bundle);
|
||||
return fragment;
|
||||
}
|
||||
@@ -54,27 +63,53 @@ public class HelpFragment extends DialogFragment {
|
||||
HelpFragment.newInstance(helpResourceId).show(ft, TAG);
|
||||
}
|
||||
|
||||
private Spanned getHtml(String html, double textSize) {
|
||||
final Html.ImageGetter imageGetter = source -> {
|
||||
final int imageId = getResources().getIdentifier(source.replace("/", ""), "drawable", requireActivity().getPackageName());
|
||||
// Don't die if we don't find the image - use a heart instead
|
||||
final Drawable drawable = ContextCompat.getDrawable(requireActivity(), imageId > 0 ? imageId : R.drawable.ic_favorite_24dp);
|
||||
final double f = textSize / drawable.getIntrinsicHeight();
|
||||
drawable.setBounds(0, 0, (int) (f * drawable.getIntrinsicWidth()), (int) textSize);
|
||||
return drawable;
|
||||
};
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY, imageGetter, null);
|
||||
} else {
|
||||
return Html.fromHtml(html, imageGetter, null);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_help, null);
|
||||
|
||||
int helpId = 0;
|
||||
boolean torButton = false;
|
||||
Bundle arguments = getArguments();
|
||||
if (arguments != null) {
|
||||
helpId = arguments.getInt(HELP_ID);
|
||||
torButton = arguments.getInt(TOR_BUTTON) > 0;
|
||||
}
|
||||
final TextView helpTv = view.findViewById(R.id.tvHelp);
|
||||
if (helpId > 0)
|
||||
((TextView) view.findViewById(R.id.tvHelp)).setText(Html.fromHtml(getString(helpId)));
|
||||
helpTv.setText(getHtml(getString(helpId), helpTv.getTextSize()));
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.help_ok,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
MaterialAlertDialogBuilder builder =
|
||||
new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setView(view);
|
||||
if (torButton) {
|
||||
builder.setNegativeButton(R.string.help_nok,
|
||||
(dialog, id) -> dialog.dismiss())
|
||||
.setPositiveButton(R.string.help_getorbot,
|
||||
(dialog, id) -> {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
NetCipherHelper.getInstance().installOrbot(requireActivity());
|
||||
});
|
||||
} else {
|
||||
builder.setNegativeButton(R.string.help_ok,
|
||||
(dialog, id) -> dialog.dismiss());
|
||||
}
|
||||
return builder.create();
|
||||
}
|
||||
}
|
@@ -22,6 +22,7 @@ import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Patterns;
|
||||
import android.view.KeyEvent;
|
||||
@@ -91,6 +92,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
private TextInputLayout etAddress;
|
||||
private TextInputLayout etNotes;
|
||||
private TextView tvXmrTo;
|
||||
private TextView tvTor;
|
||||
private Map<Crypto, ImageButton> ibCrypto;
|
||||
final private Set<Crypto> possibleCryptos = new HashSet<>();
|
||||
private Crypto selectedCrypto = null;
|
||||
@@ -117,11 +119,12 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_send_address, container, false);
|
||||
|
||||
if (Helper.ALLOW_SHIFT) {
|
||||
tvXmrTo = view.findViewById(R.id.tvXmrTo);
|
||||
ibCrypto = new HashMap<>();
|
||||
for (Crypto crypto : Crypto.values()) {
|
||||
final ImageButton button = view.findViewById(crypto.getButtonId());
|
||||
tvTor = view.findViewById(R.id.tvTor);
|
||||
tvXmrTo = view.findViewById(R.id.tvXmrTo);
|
||||
ibCrypto = new HashMap<>();
|
||||
for (Crypto crypto : Crypto.values()) {
|
||||
final ImageButton button = view.findViewById(crypto.getButtonId());
|
||||
if (Helper.ALLOW_SHIFT || (crypto == Crypto.XMR)) {
|
||||
ibCrypto.put(crypto, button);
|
||||
button.setOnClickListener(v -> {
|
||||
if (possibleCryptos.contains(crypto)) {
|
||||
@@ -137,14 +140,21 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
} else {
|
||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
|
||||
tvXmrTo.setVisibility(View.VISIBLE);
|
||||
tvTor.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
button.setImageResource(crypto.getIconDisabledId());
|
||||
button.setImageAlpha(128);
|
||||
button.setEnabled(false);
|
||||
}
|
||||
updateCryptoButtons(true);
|
||||
} else {
|
||||
view.findViewById(R.id.llExchange).setVisibility(View.GONE);
|
||||
}
|
||||
if (!Helper.ALLOW_SHIFT) {
|
||||
tvTor.setVisibility(View.VISIBLE);
|
||||
}
|
||||
updateCryptoButtons(true);
|
||||
|
||||
etAddress = view.findViewById(R.id.etAddress);
|
||||
etAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
etAddress.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@@ -349,7 +359,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
|
||||
private boolean checkAddress() {
|
||||
boolean ok = checkAddressNoError();
|
||||
if (!ok) {
|
||||
if (possibleCryptos.isEmpty()) {
|
||||
etAddress.setError(getString(R.string.send_address_invalid));
|
||||
} else {
|
||||
etAddress.setError(null);
|
||||
@@ -455,6 +465,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
Timber.d("BUT ONLY XMR SUPPORTED");
|
||||
barcodeData = null;
|
||||
sendListener.setBarcodeData(barcodeData);
|
||||
return;
|
||||
}
|
||||
if (barcodeData.address != null) {
|
||||
etAddress.getEditText().setText(barcodeData.address);
|
||||
|
@@ -35,7 +35,6 @@ import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.OkHttpHelper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.widget.ExchangeOtherEditText;
|
||||
import com.m2049r.xmrwallet.widget.SendProgressView;
|
||||
@@ -90,7 +89,6 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
Timber.i(maxBtc + "/" + minBtc);
|
||||
@@ -256,8 +254,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
if (xmrToApi == null) {
|
||||
synchronized (this) {
|
||||
if (xmrToApi == null) {
|
||||
xmrToApi = new SideShiftApiImpl(OkHttpHelper.getOkHttpClient(),
|
||||
ServiceHelper.getXmrToBaseUrl());
|
||||
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -37,7 +37,6 @@ import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.OkHttpHelper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.widget.SendProgressView;
|
||||
|
||||
@@ -359,40 +358,22 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||
}
|
||||
|
||||
private RequestQuote xmrtoQuote = null;
|
||||
private int stageARetries = 0;
|
||||
private final int RETRIES = 3;
|
||||
private double stageAPrice = 0;
|
||||
|
||||
private void processStageA(final RequestQuote requestQuote) {
|
||||
Timber.d("processCreateOrder %s", requestQuote.getId());
|
||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
// verify the BTC amount is correct (price can change and we can only specify XMR amount)
|
||||
// verify the BTC amount is correct
|
||||
if (requestQuote.getBtcAmount() != txDataBtc.getBtcAmount()) {
|
||||
if (--stageARetries <= 0) {
|
||||
Timber.d("Failed to get quote");
|
||||
getView().post(() ->
|
||||
showStageError(ShiftError.Error.SERVICE.toString(),
|
||||
getString(R.string.shift_noquote),
|
||||
getString(R.string.shift_checkamount)));
|
||||
return; // just stop for now
|
||||
}
|
||||
if (stageAPrice == requestQuote.getPrice()) {
|
||||
// same price but different BTC amount - something else is wrong (e.g. too many decimals)
|
||||
Timber.d("Price unchanged");
|
||||
getView().post(() ->
|
||||
showStageError(ShiftError.Error.SERVICE.toString(),
|
||||
getString(R.string.shift_noquote),
|
||||
getString(R.string.shift_checkamount)));
|
||||
return; // just stop for now
|
||||
}
|
||||
stageAPrice = requestQuote.getPrice();
|
||||
// recalc XMR and try again
|
||||
txDataBtc.setAmount(txDataBtc.getBtcAmount() / requestQuote.getPrice());
|
||||
getView().post(this::stageAOneShot);
|
||||
return; // stageA will run in the main thread
|
||||
Timber.d("Failed to get quote");
|
||||
getView().post(() -> showStageError(ShiftError.Error.SERVICE.toString(),
|
||||
getString(R.string.shift_noquote),
|
||||
getString(R.string.shift_checkamount)));
|
||||
return; // just stop for now
|
||||
}
|
||||
xmrtoQuote = requestQuote;
|
||||
txDataBtc.setAmount(xmrtoQuote.getXmrAmount());
|
||||
getView().post(() -> {
|
||||
// show data from the actual quote as that is what is used to
|
||||
NumberFormat df = NumberFormat.getInstance(Locale.US);
|
||||
df.setMaximumFractionDigits(12);
|
||||
final String btcAmount = df.format(xmrtoQuote.getBtcAmount());
|
||||
@@ -438,18 +419,12 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||
}
|
||||
|
||||
private void stageA() {
|
||||
stageARetries = RETRIES;
|
||||
stageAOneShot();
|
||||
}
|
||||
|
||||
private void stageAOneShot() {
|
||||
if (!isResumed) return;
|
||||
Timber.d("Request Quote");
|
||||
xmrtoQuote = null;
|
||||
xmrtoOrder = null;
|
||||
showProgress(1, getString(R.string.label_send_progress_xmrto_create));
|
||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
stageAPrice = 0;
|
||||
|
||||
ShiftCallback<RequestQuote> callback = new ShiftCallback<RequestQuote>() {
|
||||
@Override
|
||||
@@ -473,7 +448,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||
}
|
||||
};
|
||||
|
||||
getXmrToApi().requestQuote(txDataBtc.getAmountAsDouble(), callback);
|
||||
getXmrToApi().requestQuote(txDataBtc.getBtcAmount(), callback);
|
||||
}
|
||||
|
||||
private CreateOrder xmrtoOrder = null;
|
||||
@@ -567,8 +542,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||
if (xmrToApi == null) {
|
||||
synchronized (this) {
|
||||
if (xmrToApi == null) {
|
||||
xmrToApi = new SideShiftApiImpl(OkHttpHelper.getOkHttpClient(),
|
||||
ServiceHelper.getXmrToBaseUrl());
|
||||
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -39,8 +39,8 @@ import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.OkHttpHelper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
@@ -213,19 +213,27 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||
if (status.isError()) {
|
||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, status.toString()));
|
||||
statusResource = R.drawable.ic_error_red_24dp;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(0xff8b0000, android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), android.R.attr.colorError),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} else if (status.isSent() || status.isPaid()) {
|
||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, btcData.getBtcSymbol()));
|
||||
statusResource = R.drawable.ic_success_green_24dp;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(0xFF417505, android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
statusResource = R.drawable.ic_success;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} else if (status.isWaiting()) {
|
||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid));
|
||||
statusResource = R.drawable.ic_pending_orange_24dp;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(0xFFFF6105, android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
statusResource = R.drawable.ic_pending;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} else if (status.isPending()) {
|
||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid));
|
||||
statusResource = R.drawable.ic_pending_orange_24dp;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(0xFFFF6105, android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
statusResource = R.drawable.ic_pending;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} else {
|
||||
throw new IllegalStateException("status is broken: " + status.toString());
|
||||
}
|
||||
@@ -245,8 +253,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||
if (xmrToApi == null) {
|
||||
synchronized (this) {
|
||||
if (xmrToApi == null) {
|
||||
xmrToApi = new SideShiftApiImpl(OkHttpHelper.getOkHttpClient(),
|
||||
ServiceHelper.getXmrToBaseUrl());
|
||||
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -55,7 +55,6 @@ import com.m2049r.xmrwallet.widget.DotBar;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
@@ -202,14 +201,14 @@ public class SendFragment extends Fragment
|
||||
CharSequence nextLabel = pagerAdapter.getPageTitle(position + 1);
|
||||
bNext.setText(nextLabel);
|
||||
if (nextLabel != null) {
|
||||
bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_navigate_next_white_24dp, 0);
|
||||
bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_navigate_next, 0);
|
||||
} else {
|
||||
bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
CharSequence prevLabel = pagerAdapter.getPageTitle(position - 1);
|
||||
bPrev.setText(prevLabel);
|
||||
if (prevLabel != null) {
|
||||
bPrev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_navigate_prev_white_24dp, 0, 0, 0);
|
||||
bPrev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_navigate_prev, 0, 0, 0);
|
||||
} else {
|
||||
bPrev.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
|
@@ -25,26 +25,22 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.dialog.HelpFragment;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHolder> {
|
||||
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
public interface OnInteractionListener {
|
||||
void onInteraction(View view, NodeInfo item);
|
||||
|
||||
@@ -54,14 +50,16 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||
private final List<NodeInfo> nodeItems = new ArrayList<>();
|
||||
private final OnInteractionListener listener;
|
||||
|
||||
private final Context context;
|
||||
private final FragmentActivity activity;
|
||||
|
||||
public NodeInfoAdapter(Context context, OnInteractionListener listener) {
|
||||
this.context = context;
|
||||
public NodeInfoAdapter(FragmentActivity activity, OnInteractionListener listener) {
|
||||
this.activity = activity;
|
||||
this.listener = listener;
|
||||
Calendar cal = Calendar.getInstance();
|
||||
TimeZone tz = cal.getTimeZone(); //get the local time zone.
|
||||
TS_FORMATTER.setTimeZone(tz);
|
||||
}
|
||||
|
||||
public void notifyItemChanged(NodeInfo nodeInfo) {
|
||||
final int pos = nodeItems.indexOf(nodeInfo);
|
||||
if (pos >= 0) notifyItemChanged(pos);
|
||||
}
|
||||
|
||||
private static class NodeDiff extends DiffCallback<NodeInfo> {
|
||||
@@ -142,7 +140,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||
final ImageButton ibBookmark;
|
||||
final View pbBookmark;
|
||||
final TextView tvName;
|
||||
final TextView tvIp;
|
||||
final TextView tvInfo;
|
||||
final ImageView ivPing;
|
||||
NodeInfo nodeItem;
|
||||
|
||||
@@ -151,7 +149,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||
ibBookmark = itemView.findViewById(R.id.ibBookmark);
|
||||
pbBookmark = itemView.findViewById(R.id.pbBookmark);
|
||||
tvName = itemView.findViewById(R.id.tvName);
|
||||
tvIp = itemView.findViewById(R.id.tvAddress);
|
||||
tvInfo = itemView.findViewById(R.id.tvInfo);
|
||||
ivPing = itemView.findViewById(R.id.ivPing);
|
||||
ibBookmark.setOnClickListener(v -> {
|
||||
nodeItem.toggleFavourite();
|
||||
@@ -179,13 +177,12 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||
ivPing.setImageResource(getPingIcon(nodeItem));
|
||||
if (nodeItem.isTested()) {
|
||||
if (nodeItem.isValid()) {
|
||||
Helper.showTimeDifference(tvIp, nodeItem.getTimestamp());
|
||||
nodeItem.showInfo(tvInfo);
|
||||
} else {
|
||||
tvIp.setText(getResponseErrorText(context, nodeItem.getResponseCode()));
|
||||
tvIp.setTextColor(ThemeHelper.getThemedColor(context, R.attr.colorError));
|
||||
nodeItem.showInfo(tvInfo, getResponseErrorText(activity, nodeItem.getResponseCode()), true);
|
||||
}
|
||||
} else {
|
||||
tvIp.setText(context.getResources().getString(R.string.node_testing, nodeItem.getHostAddress()));
|
||||
nodeItem.showInfo(tvInfo);
|
||||
}
|
||||
itemView.setSelected(nodeItem.isSelected());
|
||||
itemView.setClickable(itemsClickable);
|
||||
@@ -201,6 +198,16 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||
int position = getAdapterPosition(); // gets item position
|
||||
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
||||
final NodeInfo node = nodeItems.get(position);
|
||||
if (node.isOnion()) {
|
||||
switch (NetCipherHelper.getStatus()) {
|
||||
case NOT_INSTALLED:
|
||||
HelpFragment.display(activity.getSupportFragmentManager(), R.string.help_tor);
|
||||
return;
|
||||
case DISABLED:
|
||||
HelpFragment.display(activity.getSupportFragmentManager(), R.string.help_tor_enable);
|
||||
return;
|
||||
}
|
||||
}
|
||||
node.setSelecting(true);
|
||||
allowClick(false);
|
||||
listener.onInteraction(view, node);
|
||||
@@ -222,21 +229,21 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||
|
||||
static public int getPingIcon(NodeInfo nodeInfo) {
|
||||
if (nodeInfo.isUnauthorized()) {
|
||||
return R.drawable.ic_wifi_lock_black_24dp;
|
||||
return R.drawable.ic_wifi_lock;
|
||||
}
|
||||
if (nodeInfo.isValid()) {
|
||||
final double ping = nodeInfo.getResponseTime();
|
||||
if (ping < NodeInfo.PING_GOOD) {
|
||||
return R.drawable.ic_signal_wifi_4_bar_24dp;
|
||||
return R.drawable.ic_wifi_4_bar;
|
||||
} else if (ping < NodeInfo.PING_MEDIUM) {
|
||||
return R.drawable.ic_signal_wifi_3_bar_24dp;
|
||||
return R.drawable.ic_wifi_3_bar;
|
||||
} else if (ping < NodeInfo.PING_BAD) {
|
||||
return R.drawable.ic_signal_wifi_2_bar_24dp;
|
||||
return R.drawable.ic_wifi_2_bar;
|
||||
} else {
|
||||
return R.drawable.ic_signal_wifi_1_bar_24dp;
|
||||
return R.drawable.ic_wifi_1_bar;
|
||||
}
|
||||
} else {
|
||||
return R.drawable.ic_signal_wifi_off_24dp;
|
||||
return R.drawable.ic_wifi_off;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +252,8 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||
return ctx.getResources().getString(R.string.node_general_error);
|
||||
} else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
return ctx.getResources().getString(R.string.node_auth_error);
|
||||
} else if (responseCode == 418) {
|
||||
return ctx.getResources().getString(R.string.node_tor_error);
|
||||
} else {
|
||||
return ctx.getResources().getString(R.string.node_test_error, responseCode);
|
||||
}
|
||||
|
@@ -30,11 +30,13 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
import com.m2049r.xmrwallet.data.UserNotes;
|
||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
@@ -48,7 +50,7 @@ import java.util.TimeZone;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
|
||||
private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
private final static SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
private final int outboundColour;
|
||||
private final int inboundColour;
|
||||
@@ -66,10 +68,10 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
|
||||
public TransactionInfoAdapter(Context context, OnInteractionListener listener) {
|
||||
this.context = context;
|
||||
inboundColour = ContextCompat.getColor(context, R.color.tx_plus);
|
||||
outboundColour = ContextCompat.getColor(context, R.color.tx_minus);
|
||||
pendingColour = ContextCompat.getColor(context, R.color.tx_pending);
|
||||
failedColour = ContextCompat.getColor(context, R.color.tx_failed);
|
||||
inboundColour = ThemeHelper.getThemedColor(context, R.attr.positiveColor);
|
||||
outboundColour = ThemeHelper.getThemedColor(context, R.attr.negativeColor);
|
||||
pendingColour = ThemeHelper.getThemedColor(context, R.attr.neutralColor);
|
||||
failedColour = ThemeHelper.getThemedColor(context, R.attr.neutralColor);
|
||||
infoItems = new ArrayList<>();
|
||||
this.listener = listener;
|
||||
Calendar cal = Calendar.getInstance();
|
||||
@@ -77,6 +79,10 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
DATETIME_FORMATTER.setTimeZone(tz);
|
||||
}
|
||||
|
||||
public boolean needsTransactionUpdateOnNewBlock() {
|
||||
return (infoItems.size() > 0) && !infoItems.get(0).isConfirmed();
|
||||
}
|
||||
|
||||
private static class TransactionInfoDiff extends DiffCallback<TransactionInfo> {
|
||||
|
||||
public TransactionInfoDiff(List<TransactionInfo> oldList, List<TransactionInfo> newList) {
|
||||
@@ -95,6 +101,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
return (oldItem.direction == newItem.direction)
|
||||
&& (oldItem.isPending == newItem.isPending)
|
||||
&& (oldItem.isFailed == newItem.isFailed)
|
||||
&& ((oldItem.confirmations == newItem.confirmations) || (oldItem.isConfirmed()))
|
||||
&& (oldItem.subaddressLabel.equals(newItem.subaddressLabel))
|
||||
&& (Objects.equals(oldItem.notes, newItem.notes));
|
||||
}
|
||||
@@ -146,18 +153,23 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
final ImageView ivTxType;
|
||||
final TextView tvAmount;
|
||||
final TextView tvFee;
|
||||
final TextView tvFailed;
|
||||
final TextView tvPaymentId;
|
||||
final TextView tvDateTime;
|
||||
final CircularProgressIndicator pbConfirmations;
|
||||
final TextView tvConfirmations;
|
||||
TransactionInfo infoItem;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ivTxType = itemView.findViewById(R.id.ivTxType);
|
||||
tvAmount = itemView.findViewById(R.id.tx_amount);
|
||||
tvFee = itemView.findViewById(R.id.tx_fee);
|
||||
tvFailed = itemView.findViewById(R.id.tx_failed);
|
||||
tvPaymentId = itemView.findViewById(R.id.tx_paymentid);
|
||||
tvDateTime = itemView.findViewById(R.id.tx_datetime);
|
||||
pbConfirmations = itemView.findViewById(R.id.pbConfirmations);
|
||||
pbConfirmations.setMax(TransactionInfo.CONFIRMATION);
|
||||
tvConfirmations = itemView.findViewById(R.id.tvConfirmations);
|
||||
}
|
||||
|
||||
private String getDateTime(long time) {
|
||||
@@ -182,7 +194,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
ivTxType.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
ivTxType.setVisibility(View.GONE); // gives us more space for the amount
|
||||
ivTxType.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
String displayAmount = Helper.getDisplayAmount(infoItem.amount, Helper.DISPLAY_DIGITS_INFO);
|
||||
@@ -192,25 +204,39 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
tvAmount.setText(context.getString(R.string.tx_list_amount_positive, displayAmount));
|
||||
}
|
||||
|
||||
if ((infoItem.fee > 0)) {
|
||||
String fee = Helper.getDisplayAmount(infoItem.fee, Helper.DISPLAY_DIGITS_INFO);
|
||||
tvFee.setText(context.getString(R.string.tx_list_fee, fee));
|
||||
tvFee.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
tvFee.setText("");
|
||||
tvFee.setVisibility(View.GONE);
|
||||
}
|
||||
tvFailed.setVisibility(View.GONE);
|
||||
if (infoItem.isFailed) {
|
||||
this.tvAmount.setText(context.getString(R.string.tx_list_amount_failed, displayAmount));
|
||||
this.tvFee.setText(context.getString(R.string.tx_list_failed_text));
|
||||
tvFee.setVisibility(View.VISIBLE);
|
||||
tvFailed.setVisibility(View.VISIBLE);
|
||||
setTxColour(failedColour);
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
} else if (infoItem.isPending) {
|
||||
setTxColour(pendingColour);
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
pbConfirmations.setIndeterminate(true);
|
||||
pbConfirmations.setVisibility(View.VISIBLE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
} else if (infoItem.direction == TransactionInfo.Direction.Direction_In) {
|
||||
setTxColour(inboundColour);
|
||||
if (!infoItem.isConfirmed()) {
|
||||
pbConfirmations.setVisibility(View.VISIBLE);
|
||||
final int confirmations = (int) infoItem.confirmations;
|
||||
pbConfirmations.setProgressCompat(confirmations, true);
|
||||
final String confCount = Integer.toString(confirmations);
|
||||
tvConfirmations.setText(confCount);
|
||||
if (confCount.length() == 1) // we only have space for character in the progress circle
|
||||
tvConfirmations.setVisibility(View.VISIBLE);
|
||||
else
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
} else {
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
setTxColour(outboundColour);
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
String tag = null;
|
||||
@@ -228,8 +254,8 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
tvPaymentId.setText(info);
|
||||
} else {
|
||||
Spanned label = Html.fromHtml(context.getString(R.string.tx_details_notes,
|
||||
Integer.toHexString(ContextCompat.getColor(context, R.color.monerujoGreen) & 0xFFFFFF),
|
||||
Integer.toHexString(ContextCompat.getColor(context, R.color.monerujoBackground) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF),
|
||||
tag, info.isEmpty() ? "" : (" " + info)));
|
||||
tvPaymentId.setText(label);
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@ public class TransactionHistory {
|
||||
this.accountIndex = accountIndex;
|
||||
}
|
||||
|
||||
public void loadNotes(Wallet wallet) {
|
||||
private void loadNotes(Wallet wallet) {
|
||||
for (TransactionInfo info : transactions) {
|
||||
info.notes = wallet.getUserNote(info.hash);
|
||||
}
|
||||
@@ -61,30 +61,22 @@ public class TransactionHistory {
|
||||
|
||||
private List<TransactionInfo> transactions = new ArrayList<>();
|
||||
|
||||
public void refreshWithNotes(Wallet wallet) {
|
||||
void refreshWithNotes(Wallet wallet) {
|
||||
refresh();
|
||||
loadNotes(wallet);
|
||||
}
|
||||
|
||||
// public void refresh() {
|
||||
// transactions = refreshJ();
|
||||
// }
|
||||
|
||||
public void refresh() {
|
||||
private void refresh() {
|
||||
List<TransactionInfo> transactionInfos = refreshJ();
|
||||
Timber.d("refreshed %d", transactionInfos.size());
|
||||
Timber.d("refresh size=%d", transactionInfos.size());
|
||||
for (Iterator<TransactionInfo> iterator = transactionInfos.iterator(); iterator.hasNext(); ) {
|
||||
TransactionInfo info = iterator.next();
|
||||
if (info.accountIndex != accountIndex) {
|
||||
iterator.remove();
|
||||
Timber.d("removed %s", info.hash);
|
||||
} else {
|
||||
Timber.d("kept %s", info.hash);
|
||||
}
|
||||
}
|
||||
transactions = transactionInfos;
|
||||
}
|
||||
|
||||
private native List<TransactionInfo> refreshJ();
|
||||
|
||||
}
|
||||
|
@@ -29,6 +29,8 @@ import lombok.RequiredArgsConstructor;
|
||||
// this is not the TransactionInfo from the API as that is owned by the TransactionHistory
|
||||
// this is a POJO for the TransactionInfoAdapter
|
||||
public class TransactionInfo implements Parcelable, Comparable<TransactionInfo> {
|
||||
public static final int CONFIRMATION = 10; // blocks
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum Direction {
|
||||
Direction_In(0),
|
||||
@@ -98,6 +100,10 @@ public class TransactionInfo implements Parcelable, Comparable<TransactionInfo>
|
||||
this.transfers = transfers;
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmations >= CONFIRMATION;
|
||||
}
|
||||
|
||||
public String getDisplayLabel() {
|
||||
if (subaddressLabel.isEmpty() || (Subaddress.DEFAULT_LABEL_FORMATTER.matcher(subaddressLabel).matches()))
|
||||
return ("#" + addressIndex);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user