mirror of https://github.com/m2049r/xmrwallet.git
Merge branch 'm2049r:master' into arabic-translate
This commit is contained in:
commit
4e8d30e2e1
|
@ -8,12 +8,8 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/app/build
|
/app/build
|
||||||
/app/release
|
/app/release
|
||||||
/app/alpha
|
/app/alpha*
|
||||||
/app/prod
|
/app/prod*
|
||||||
/app/alphaMainnet
|
|
||||||
/app/prodMainnet
|
|
||||||
/app/alphaStagenet
|
|
||||||
/app/prodStagenet
|
|
||||||
/app/.cxx
|
/app/.cxx
|
||||||
/monerujo.id
|
/monerujo.id
|
||||||
/external-libs/VERSION
|
/external-libs/VERSION
|
||||||
|
|
|
@ -4,12 +4,12 @@ android {
|
||||||
ndkVersion '17.2.4988734'
|
ndkVersion '17.2.4988734'
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.m2049r.xmrwallet"
|
applicationId "com.m2049r.xmrwallet"
|
||||||
buildToolsVersion = '34.0.0'
|
buildToolsVersion = '35.0.0'
|
||||||
compileSdk 34
|
compileSdk 35
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 35
|
||||||
versionCode 3311
|
versionCode 4104
|
||||||
versionName "3.3.11 'Argentina'"
|
versionName "4.1.4 'Exolix'"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
|
@ -24,7 +24,7 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions 'type', 'net'
|
flavorDimensions = ['type', 'net']
|
||||||
productFlavors {
|
productFlavors {
|
||||||
mainnet {
|
mainnet {
|
||||||
dimension 'net'
|
dimension 'net'
|
||||||
|
@ -58,7 +58,7 @@ android {
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
}
|
}
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
variant.buildConfigField "String", "ID_A", "\"" + getId("ID_A") + "\""
|
variant.buildConfigField "String", "ID_F", "\"" + getId("ID_F") + "\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,10 +132,10 @@ static def getId(name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
|
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
|
||||||
|
|
||||||
implementation 'androidx.core:core:1.12.0'
|
implementation 'androidx.core:core:1.13.1'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
@ -143,7 +143,7 @@ dependencies {
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.preference:preference:1.2.1'
|
implementation 'androidx.preference:preference:1.2.1'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
|
|
||||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
android:name="android.hardware.camera"
|
android:name="android.hardware.camera"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||||
|
@ -12,6 +17,7 @@
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
|
@ -98,7 +104,12 @@
|
||||||
android:name=".service.WalletService"
|
android:name=".service.WalletService"
|
||||||
android:description="@string/service_description"
|
android:description="@string/service_description"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="Monero Wallet Service" />
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:label="Monero Wallet Service">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="Keeps app in sync with the blockchain" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) 2017 m2049r
|
* Copyright (c) 2017-2024 m2049r
|
||||||
* <p>
|
* <p>
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -18,8 +18,6 @@
|
||||||
#include "monerujo.h"
|
#include "monerujo.h"
|
||||||
#include "wallet2_api.h"
|
#include "wallet2_api.h"
|
||||||
|
|
||||||
//TODO explicit casting jlong, jint, jboolean to avoid warnings
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C"
|
extern "C"
|
||||||
{
|
{
|
||||||
|
@ -34,7 +32,6 @@ extern "C"
|
||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG,__VA_ARGS__)
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG,__VA_ARGS__)
|
||||||
|
|
||||||
static JavaVM *cachedJVM;
|
static JavaVM *cachedJVM;
|
||||||
static jclass class_String;
|
|
||||||
static jclass class_ArrayList;
|
static jclass class_ArrayList;
|
||||||
static jclass class_WalletListener;
|
static jclass class_WalletListener;
|
||||||
static jclass class_CoinsInfo;
|
static jclass class_CoinsInfo;
|
||||||
|
@ -42,17 +39,11 @@ static jclass class_TransactionInfo;
|
||||||
static jclass class_Transfer;
|
static jclass class_Transfer;
|
||||||
static jclass class_Ledger;
|
static jclass class_Ledger;
|
||||||
static jclass class_WalletStatus;
|
static jclass class_WalletStatus;
|
||||||
|
static jclass class_BluetoothService;
|
||||||
|
static jclass class_SidekickService;
|
||||||
|
|
||||||
std::mutex _listenerMutex;
|
std::mutex _listenerMutex;
|
||||||
|
|
||||||
//void jstringToString(JNIEnv *env, std::string &str, jstring jstr) {
|
|
||||||
// if (!jstr) return;
|
|
||||||
// const int len = env->GetStringUTFLength(jstr);
|
|
||||||
// const char *chars = env->GetStringUTFChars(jstr, nullptr);
|
|
||||||
// str.assign(chars, len);
|
|
||||||
// env->ReleaseStringUTFChars(jstr, chars);
|
|
||||||
//}
|
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
|
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
|
||||||
cachedJVM = jvm;
|
cachedJVM = jvm;
|
||||||
LOGI("JNI_OnLoad");
|
LOGI("JNI_OnLoad");
|
||||||
|
@ -62,8 +53,6 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
|
||||||
}
|
}
|
||||||
//LOGI("JNI_OnLoad ok");
|
//LOGI("JNI_OnLoad ok");
|
||||||
|
|
||||||
class_String = static_cast<jclass>(jenv->NewGlobalRef(
|
|
||||||
jenv->FindClass("java/lang/String")));
|
|
||||||
class_ArrayList = static_cast<jclass>(jenv->NewGlobalRef(
|
class_ArrayList = static_cast<jclass>(jenv->NewGlobalRef(
|
||||||
jenv->FindClass("java/util/ArrayList")));
|
jenv->FindClass("java/util/ArrayList")));
|
||||||
class_CoinsInfo = static_cast<jclass>(jenv->NewGlobalRef(
|
class_CoinsInfo = static_cast<jclass>(jenv->NewGlobalRef(
|
||||||
|
@ -78,6 +67,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
|
||||||
jenv->FindClass("com/m2049r/xmrwallet/ledger/Ledger")));
|
jenv->FindClass("com/m2049r/xmrwallet/ledger/Ledger")));
|
||||||
class_WalletStatus = static_cast<jclass>(jenv->NewGlobalRef(
|
class_WalletStatus = static_cast<jclass>(jenv->NewGlobalRef(
|
||||||
jenv->FindClass("com/m2049r/xmrwallet/model/Wallet$Status")));
|
jenv->FindClass("com/m2049r/xmrwallet/model/Wallet$Status")));
|
||||||
|
class_BluetoothService = static_cast<jclass>(jenv->NewGlobalRef(
|
||||||
|
jenv->FindClass("com/m2049r/xmrwallet/service/BluetoothService")));
|
||||||
return JNI_VERSION_1_6;
|
return JNI_VERSION_1_6;
|
||||||
}
|
}
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -1686,6 +1677,79 @@ int LedgerFind(char *buffer, size_t len) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// SidekickWallet Stuff
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief BtExchange - exchange data with Monerujo Device
|
||||||
|
* @param request - buffer for data to send
|
||||||
|
* @param request_len - length of data to send
|
||||||
|
* @param response - buffer for received data
|
||||||
|
* @param max_resp_len - size of receive buffer
|
||||||
|
*
|
||||||
|
* @return length of received data in response or -1 if error, -2 if response buffer too small
|
||||||
|
*/
|
||||||
|
int BtExchange(
|
||||||
|
unsigned char *request,
|
||||||
|
unsigned int request_len,
|
||||||
|
unsigned char *response,
|
||||||
|
unsigned int max_resp_len) {
|
||||||
|
JNIEnv *jenv;
|
||||||
|
int envStat = attachJVM(&jenv);
|
||||||
|
if (envStat == JNI_ERR) return -16;
|
||||||
|
|
||||||
|
jmethodID exchangeMethod = jenv->GetStaticMethodID(class_BluetoothService, "Exchange",
|
||||||
|
"([B)[B");
|
||||||
|
|
||||||
|
auto reqLen = static_cast<jsize>(request_len);
|
||||||
|
jbyteArray reqData = jenv->NewByteArray(reqLen);
|
||||||
|
jenv->SetByteArrayRegion(reqData, 0, reqLen, (jbyte *) request);
|
||||||
|
LOGD("BtExchange cmd: 0x%02x with %u bytes", request[0], reqLen);
|
||||||
|
auto dataRecv = (jbyteArray)
|
||||||
|
jenv->CallStaticObjectMethod(class_BluetoothService, exchangeMethod, reqData);
|
||||||
|
jenv->DeleteLocalRef(reqData);
|
||||||
|
if (dataRecv == nullptr) {
|
||||||
|
detachJVM(jenv, envStat);
|
||||||
|
LOGD("BtExchange: error reading");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
jsize respLen = jenv->GetArrayLength(dataRecv);
|
||||||
|
LOGD("BtExchange response is %u bytes", respLen);
|
||||||
|
if (respLen <= max_resp_len) {
|
||||||
|
jenv->GetByteArrayRegion(dataRecv, 0, respLen, (jbyte *) response);
|
||||||
|
jenv->DeleteLocalRef(dataRecv);
|
||||||
|
detachJVM(jenv, envStat);
|
||||||
|
return static_cast<int>(respLen);;
|
||||||
|
} else {
|
||||||
|
jenv->DeleteLocalRef(dataRecv);
|
||||||
|
detachJVM(jenv, envStat);
|
||||||
|
LOGE("BtExchange response buffer too small: %u < %u", respLen, max_resp_len);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief ConfirmTransfers
|
||||||
|
* @param transfers - string of "fee (':' address ':' amount)+"
|
||||||
|
*
|
||||||
|
* @return true on accept, false on reject
|
||||||
|
*/
|
||||||
|
bool ConfirmTransfers(const char *transfers) {
|
||||||
|
JNIEnv *jenv;
|
||||||
|
int envStat = attachJVM(&jenv);
|
||||||
|
if (envStat == JNI_ERR) return -16;
|
||||||
|
|
||||||
|
jmethodID confirmMethod = jenv->GetStaticMethodID(class_SidekickService, "ConfirmTransfers",
|
||||||
|
"(Ljava/lang/String;)Z");
|
||||||
|
|
||||||
|
jstring _transfers = jenv->NewStringUTF(transfers);
|
||||||
|
auto confirmed =
|
||||||
|
jenv->CallStaticBooleanMethod(class_SidekickService, confirmMethod, _transfers);
|
||||||
|
jenv->DeleteLocalRef(_transfers);
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) 2017 m2049r
|
* Copyright (c) 2017-2024 m2049r
|
||||||
* <p>
|
* <p>
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -19,6 +19,8 @@
|
||||||
|
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
|
@ -28,6 +30,10 @@
|
||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
void ThrowException(JNIEnv *jenv, const char* type, const char* msg) {
|
||||||
|
jenv->ThrowNew(jenv->FindClass(type), msg);
|
||||||
|
}
|
||||||
|
|
||||||
jfieldID getHandleField(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
|
jfieldID getHandleField(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
|
||||||
jclass c = env->GetObjectClass(obj);
|
jclass c = env->GetObjectClass(obj);
|
||||||
return env->GetFieldID(c, fieldName, "J"); // of type long
|
return env->GetFieldID(c, fieldName, "J"); // of type long
|
||||||
|
@ -35,8 +41,16 @@ jfieldID getHandleField(JNIEnv *env, jobject obj, const char *fieldName = "handl
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
T *getHandle(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
|
T *getHandle(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
|
||||||
|
return reinterpret_cast<T *>(env->GetLongField(obj, getHandleField(env, obj, fieldName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void destroyNativeObject(JNIEnv *env, T nativeObjectHandle, jobject obj, const char *fieldName = "handle") {
|
||||||
jlong handle = env->GetLongField(obj, getHandleField(env, obj, fieldName));
|
jlong handle = env->GetLongField(obj, getHandleField(env, obj, fieldName));
|
||||||
return reinterpret_cast<T *>(handle);
|
if (handle != 0) {
|
||||||
|
ThrowException(env, "java/lang/IllegalStateException", "invalid handle (destroy)");
|
||||||
|
}
|
||||||
|
delete reinterpret_cast<T *>(nativeObjectHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setHandleFromLong(JNIEnv *env, jobject obj, jlong handle) {
|
void setHandleFromLong(JNIEnv *env, jobject obj, jlong handle) {
|
||||||
|
@ -62,18 +76,40 @@ enum {
|
||||||
HASH_DATA_AREA = 136
|
HASH_DATA_AREA = 136
|
||||||
};
|
};
|
||||||
|
|
||||||
void cn_slow_hash(const void *data, size_t length, char *hash, int variant, int prehashed, uint64_t height);
|
void cn_slow_hash(const void *data, size_t length, char *hash, int variant, int prehashed,
|
||||||
|
uint64_t height);
|
||||||
|
|
||||||
inline void slow_hash(const void *data, const size_t length, char *hash) {
|
inline void slow_hash(const void *data, const size_t length, char *hash) {
|
||||||
cn_slow_hash(data, length, hash, 0 /*variant*/, 0 /*prehashed*/, 0 /*height*/);
|
cn_slow_hash(data, length, hash, 0 /*variant*/, 0 /*prehashed*/, 0 /*height*/);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void slow_hash_broken(const void *data, char *hash, int variant) {
|
inline void slow_hash_broken(const void *data, char *hash, int variant) {
|
||||||
cn_slow_hash(data, 200 /*sizeof(union hash_state)*/, hash, variant, 1 /*prehashed*/, 0 /*height*/);
|
cn_slow_hash(data, 200 /*sizeof(union hash_state)*/, hash, variant, 1 /*prehashed*/,
|
||||||
|
0 /*height*/);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace Monerujo {
|
||||||
|
class SidekickWallet {
|
||||||
|
public:
|
||||||
|
enum Status {
|
||||||
|
Status_Ok,
|
||||||
|
Status_Error,
|
||||||
|
Status_Critical
|
||||||
|
};
|
||||||
|
|
||||||
|
SidekickWallet(uint8_t networkType, std::string a, std::string b);
|
||||||
|
|
||||||
|
~SidekickWallet();
|
||||||
|
|
||||||
|
std::string call(int commandId, const std::string &request);
|
||||||
|
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
Status status() const;
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
#endif //XMRWALLET_WALLET_LIB_H
|
#endif //XMRWALLET_WALLET_LIB_H
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -16,7 +16,17 @@
|
||||||
|
|
||||||
package com.m2049r.xmrwallet;
|
package com.m2049r.xmrwallet;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.Network;
|
||||||
|
import android.net.NetworkCapabilities;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
@ -24,7 +34,8 @@ import android.os.PowerManager;
|
||||||
|
|
||||||
import androidx.activity.OnBackPressedCallback;
|
import androidx.activity.OnBackPressedCallback;
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||||
import com.m2049r.xmrwallet.dialog.ProgressDialog;
|
import com.m2049r.xmrwallet.dialog.ProgressDialog;
|
||||||
|
@ -159,4 +170,87 @@ public class BaseActivity extends SecureActivity
|
||||||
barcodeData = null;
|
barcodeData = null;
|
||||||
return popped;
|
return popped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isNetworkAvailable() {
|
||||||
|
ConnectivityManager connectivityManager = (ConnectivityManager) getApplication().getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Network network = connectivityManager.getActiveNetwork();
|
||||||
|
if (network == null) return false;
|
||||||
|
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
|
||||||
|
return networkCapabilities != null && (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH));
|
||||||
|
} else {
|
||||||
|
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
|
||||||
|
return networkInfo != null && networkInfo.isConnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static private final int REQUEST_CODE_BLUETOOTH_PERMISSIONS = 32423;
|
||||||
|
|
||||||
|
void btPermissionGranted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
if (requestCode == REQUEST_CODE_BLUETOOTH_PERMISSIONS) {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
btPermissionGranted();
|
||||||
|
} // else onResume() takes care of trying again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showBtPermissionsDialog() {
|
||||||
|
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
|
||||||
|
alertDialogBuilder.setMessage(R.string.bluetooth_permissions);
|
||||||
|
alertDialogBuilder.setPositiveButton(R.string.bluetooth_permissions_ok,
|
||||||
|
(dialog, which) -> requestBtPermissions());
|
||||||
|
alertDialogBuilder.setCancelable(false);
|
||||||
|
AlertDialog alertDialog = alertDialogBuilder.create();
|
||||||
|
alertDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showAppInfoDialog() {
|
||||||
|
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
|
||||||
|
alertDialogBuilder.setMessage(R.string.bluetooth_permissions);
|
||||||
|
alertDialogBuilder.setPositiveButton(R.string.bluetooth_permissions_settings, (dialog, which) -> {
|
||||||
|
Intent i = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||||
|
i.addCategory(Intent.CATEGORY_DEFAULT);
|
||||||
|
i.setData(Uri.parse("package:" + getPackageName()));
|
||||||
|
startActivity(i);
|
||||||
|
});
|
||||||
|
alertDialogBuilder.setNegativeButton(R.string.bluetooth_permissions_cancel, (dialog, which) -> {
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
alertDialogBuilder.setCancelable(false);
|
||||||
|
AlertDialog alertDialog = alertDialogBuilder.create();
|
||||||
|
alertDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestBtPermissions() {
|
||||||
|
ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_SCAN}, REQUEST_CODE_BLUETOOTH_PERMISSIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean firstCheck = true;
|
||||||
|
|
||||||
|
void checkBtPermissions() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if ((ActivityCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) ||
|
||||||
|
(ActivityCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)) {
|
||||||
|
if (shouldShowRequestPermissionRationale(android.Manifest.permission.BLUETOOTH_SCAN) || shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
|
||||||
|
showBtPermissionsDialog();
|
||||||
|
} else {
|
||||||
|
if (firstCheck) {
|
||||||
|
requestBtPermissions();
|
||||||
|
} else {
|
||||||
|
showAppInfoDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btPermissionGranted();
|
||||||
|
}
|
||||||
|
firstCheck = false;
|
||||||
|
} else {
|
||||||
|
btPermissionGranted();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,326 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// mostly from BluetoothChatFragment https://github.com/android/connectivity-samples
|
||||||
|
|
||||||
|
package com.m2049r.xmrwallet;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.BluetoothService;
|
||||||
|
import com.m2049r.xmrwallet.util.Flasher;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
public class BluetoothFragment extends Fragment {
|
||||||
|
|
||||||
|
public BluetoothFragment() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
void onDeviceConnected(String connectedDeviceName);
|
||||||
|
|
||||||
|
void abort(String message);
|
||||||
|
|
||||||
|
void onReceive(int commandId);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO enable discover only after wallet is loaded
|
||||||
|
Listener activityCallback;
|
||||||
|
|
||||||
|
// Intent request codes
|
||||||
|
private static final int REQUEST_ENABLE_BT = 3;
|
||||||
|
|
||||||
|
private ImageView btIcon;
|
||||||
|
private ProgressBar pbConnecting;
|
||||||
|
private TextView btCode;
|
||||||
|
private TextView btName;
|
||||||
|
|
||||||
|
private String connectedDeviceName = null;
|
||||||
|
|
||||||
|
private BluetoothAdapter bluetoothAdapter = null;
|
||||||
|
|
||||||
|
private BluetoothService bluetoothService = null;
|
||||||
|
|
||||||
|
public enum Mode {
|
||||||
|
CLIENT, SERVER
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mode mode = Mode.CLIENT;
|
||||||
|
|
||||||
|
public BluetoothFragment(Mode mode) {
|
||||||
|
super();
|
||||||
|
this.mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
// Get local Bluetooth adapter
|
||||||
|
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
// If the adapter is null, then Bluetooth is not supported
|
||||||
|
if (bluetoothAdapter == null) {
|
||||||
|
if (activityCallback != null)
|
||||||
|
activityCallback.abort("Bluetooth is not available"); //TODO strings.xml
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (bluetoothAdapter == null) return;
|
||||||
|
|
||||||
|
// If BT is not on, request that it be enabled.
|
||||||
|
// setupComm() will then be called during onActivityResult
|
||||||
|
if (!bluetoothAdapter.isEnabled()) {
|
||||||
|
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
||||||
|
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
|
||||||
|
} else if (bluetoothService == null) {
|
||||||
|
setupCommunication();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (bluetoothService != null) {
|
||||||
|
bluetoothService.setUiHandler(null);
|
||||||
|
bluetoothService = null;
|
||||||
|
}
|
||||||
|
// The BluetoothService is stopped in LoginActivity::onDestroy
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
Timber.d("onPause %s", mode);
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
Timber.d("onResume %s", mode);
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
// Performing this check in onResume() covers the case in which BT was
|
||||||
|
// not enabled during onStart(), so we were paused to enable it...
|
||||||
|
// onResume() will be called when ACTION_REQUEST_ENABLE activity returns.
|
||||||
|
if (bluetoothService != null) {
|
||||||
|
// Only if the state is STATE_NONE, do we know that we haven't started already
|
||||||
|
if (!bluetoothService.isStarted()) {
|
||||||
|
// Start the Bluetooth services
|
||||||
|
bluetoothService.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(@NonNull Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
if (context instanceof BluetoothFragment.Listener) {
|
||||||
|
activityCallback = (BluetoothFragment.Listener) context;
|
||||||
|
} else {
|
||||||
|
throw new ClassCastException(context.toString()
|
||||||
|
+ " must implement Listener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
Timber.d("onCreateView");
|
||||||
|
return inflater.inflate(R.layout.fragment_bluetooth, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
Timber.d("onViewCreated");
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (savedInstanceState != null) return;
|
||||||
|
btName = view.findViewById(R.id.btName);
|
||||||
|
btCode = view.findViewById(R.id.btCode);
|
||||||
|
btIcon = view.findViewById(R.id.btIcon);
|
||||||
|
pbConnecting = view.findViewById(R.id.pbConnecting);
|
||||||
|
setConnecting(false);
|
||||||
|
setInfo(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up the UI and background operations for comms.
|
||||||
|
*/
|
||||||
|
private void setupCommunication() {
|
||||||
|
Timber.d("startCommunication()");
|
||||||
|
if (!isAdded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bluetoothService != null) throw new IllegalStateException("bluetoothService != null");
|
||||||
|
|
||||||
|
// Initialize the BluetoothService to perform bluetooth connections
|
||||||
|
bluetoothService = BluetoothService.GetInstance();
|
||||||
|
bluetoothService.setUiHandler(handler);
|
||||||
|
if (mode == Mode.SERVER)
|
||||||
|
bluetoothService.start();
|
||||||
|
setInfo(bluetoothService.getConnectedName(), bluetoothService.getConnectedCode());
|
||||||
|
showState(bluetoothService.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showState(int state) {
|
||||||
|
if (!isAdded()) return;
|
||||||
|
Light light;
|
||||||
|
switch (state) {
|
||||||
|
case BluetoothService.State.LISTEN:
|
||||||
|
light = Light.LISTEN;
|
||||||
|
break;
|
||||||
|
case BluetoothService.State.CONNECTING:
|
||||||
|
light = Light.CONNECTING;
|
||||||
|
break;
|
||||||
|
case BluetoothService.State.CONNECTED:
|
||||||
|
light = Light.CONNECTED;
|
||||||
|
break;
|
||||||
|
case BluetoothService.State.NONE:
|
||||||
|
default:
|
||||||
|
light = Light.NONE;
|
||||||
|
}
|
||||||
|
final Flasher flash = new Flasher(requireContext(), light);
|
||||||
|
btIcon.setImageDrawable(flash.getDrawable());
|
||||||
|
btFlash = flash;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public enum Light implements Flasher.Light {
|
||||||
|
LISTEN(R.drawable.ic_bluetooth_24),
|
||||||
|
|
||||||
|
CONNECTING(R.drawable.ic_bluetooth_searching_24),
|
||||||
|
|
||||||
|
CONNECTED(R.drawable.ic_bluetooth_connected_24),
|
||||||
|
|
||||||
|
NONE(R.drawable.ic_bluetooth_disabled_24);
|
||||||
|
|
||||||
|
final private int drawableId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Flasher btFlash;
|
||||||
|
|
||||||
|
private void flashState() {
|
||||||
|
if (btFlash != null)
|
||||||
|
btFlash.flash(getView());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Handler that gets information back from the BluetoothService
|
||||||
|
*/
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper()) {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case BluetoothService.MessageType.STATE_CHANGE:
|
||||||
|
showState(msg.arg1);
|
||||||
|
if (msg.arg1 <= BluetoothService.State.LISTEN) {
|
||||||
|
setInfo(null, null);
|
||||||
|
connectedDeviceName = null;
|
||||||
|
activityCallback.onDeviceConnected(null); // i.e. disconnected - ugly :(
|
||||||
|
setConnecting(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BluetoothService.MessageType.WRITE:
|
||||||
|
Timber.d("WRITE_MESSAGE: %d bytes", msg.arg1);
|
||||||
|
break;
|
||||||
|
case BluetoothService.MessageType.READ_CMD:
|
||||||
|
Timber.d("READ_COMMAND 0x%x (%d bytes)", msg.arg2, msg.arg1);
|
||||||
|
if (activityCallback != null) {
|
||||||
|
activityCallback.onReceive(msg.arg2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BluetoothService.MessageType.CODE:
|
||||||
|
Timber.d("CODE: %s", msg.obj);
|
||||||
|
btCode.setText((String) msg.obj);
|
||||||
|
break;
|
||||||
|
case BluetoothService.MessageType.DEVICE_NAME:
|
||||||
|
connectedDeviceName = (String) msg.obj;
|
||||||
|
setInfo(connectedDeviceName, null);
|
||||||
|
activityCallback.onDeviceConnected(connectedDeviceName);
|
||||||
|
setConnecting(false);
|
||||||
|
if (mode == Mode.CLIENT) {
|
||||||
|
final int code = new SecureRandom().nextInt(10000);
|
||||||
|
bluetoothService.write(code);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BluetoothService.MessageType.TOAST:
|
||||||
|
if (isAdded())
|
||||||
|
Toast.makeText(getActivity(), (String) msg.obj, Toast.LENGTH_SHORT).show();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
flashState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
if (requestCode == REQUEST_ENABLE_BT) {// When the request to enable Bluetooth returns
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
// Bluetooth is now enabled, so set up a chat session
|
||||||
|
setupCommunication();
|
||||||
|
} else {
|
||||||
|
// User did not enable Bluetooth or an error occurred
|
||||||
|
Timber.d("BT not enabled");
|
||||||
|
if (activityCallback != null)
|
||||||
|
activityCallback.abort("Bluetooth is not enabled"); //TODO strings.xml
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.w("Unhandled request code %d", requestCode);
|
||||||
|
}
|
||||||
|
flashState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setConnecting(boolean enable) {
|
||||||
|
pbConnecting.setVisibility(enable ? View.VISIBLE : View.INVISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connectDevice(String address) {
|
||||||
|
setConnecting(true);
|
||||||
|
bluetoothService.connect(bluetoothAdapter.getRemoteDevice(address));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInfo(String name, String code) {
|
||||||
|
try {
|
||||||
|
btName.setText(name == null ? getResources().getString(R.string.sidekick_not_connected) : name);
|
||||||
|
btCode.setText(getResources().getString(R.string.sidekick_pin, code != null ? code : "----"));
|
||||||
|
} catch (IllegalStateException ex) { // no context, so no strings
|
||||||
|
// never mind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ package com.m2049r.xmrwallet;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
|
@ -67,8 +66,20 @@ public class GenerateFragment extends Fragment {
|
||||||
static final String TYPE_KEY = "key";
|
static final String TYPE_KEY = "key";
|
||||||
static final String TYPE_SEED = "seed";
|
static final String TYPE_SEED = "seed";
|
||||||
static final String TYPE_LEDGER = "ledger";
|
static final String TYPE_LEDGER = "ledger";
|
||||||
|
static final String TYPE_SIDEKICK = "sidekick";
|
||||||
static final String TYPE_VIEWONLY = "view";
|
static final String TYPE_VIEWONLY = "view";
|
||||||
|
|
||||||
|
static Wallet.Device getDeviceType(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case TYPE_SIDEKICK:
|
||||||
|
return Wallet.Device.Sidekick;
|
||||||
|
case TYPE_LEDGER:
|
||||||
|
return Wallet.Device.Ledger;
|
||||||
|
default:
|
||||||
|
return Wallet.Device.Software;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private TextInputLayout etWalletName;
|
private TextInputLayout etWalletName;
|
||||||
private PasswordEntryView etWalletPassword;
|
private PasswordEntryView etWalletPassword;
|
||||||
private LinearLayout llFingerprintAuth;
|
private LinearLayout llFingerprintAuth;
|
||||||
|
@ -195,6 +206,7 @@ public class GenerateFragment extends Fragment {
|
||||||
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED);
|
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED);
|
||||||
break;
|
break;
|
||||||
case TYPE_LEDGER:
|
case TYPE_LEDGER:
|
||||||
|
case TYPE_SIDEKICK:
|
||||||
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE);
|
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||||
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||||
|
@ -308,7 +320,7 @@ public class GenerateFragment extends Fragment {
|
||||||
private boolean checkName() {
|
private boolean checkName() {
|
||||||
String name = etWalletName.getEditText().getText().toString();
|
String name = etWalletName.getEditText().getText().toString();
|
||||||
boolean ok = true;
|
boolean ok = true;
|
||||||
if (name.length() == 0) {
|
if (name.isEmpty()) {
|
||||||
etWalletName.setError(getString(R.string.generate_wallet_name));
|
etWalletName.setError(getString(R.string.generate_wallet_name));
|
||||||
ok = false;
|
ok = false;
|
||||||
} else if (name.charAt(0) == '.') {
|
} else if (name.charAt(0) == '.') {
|
||||||
|
@ -450,11 +462,12 @@ public class GenerateFragment extends Fragment {
|
||||||
activityCallback.onGenerate(name, crazyPass, seed, offset, height);
|
activityCallback.onGenerate(name, crazyPass, seed, offset, height);
|
||||||
break;
|
break;
|
||||||
case TYPE_LEDGER:
|
case TYPE_LEDGER:
|
||||||
|
case TYPE_SIDEKICK:
|
||||||
bGenerate.setEnabled(false);
|
bGenerate.setEnabled(false);
|
||||||
if (fingerprintAuthAllowed) {
|
if (fingerprintAuthAllowed) {
|
||||||
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
|
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
|
||||||
}
|
}
|
||||||
activityCallback.onGenerateLedger(name, crazyPass, height);
|
activityCallback.onGenerateDevice(getDeviceType(type), name, crazyPass, height);
|
||||||
break;
|
break;
|
||||||
case TYPE_KEY:
|
case TYPE_KEY:
|
||||||
case TYPE_VIEWONLY:
|
case TYPE_VIEWONLY:
|
||||||
|
@ -498,6 +511,8 @@ public class GenerateFragment extends Fragment {
|
||||||
return getString(R.string.generate_wallet_type_seed);
|
return getString(R.string.generate_wallet_type_seed);
|
||||||
case TYPE_LEDGER:
|
case TYPE_LEDGER:
|
||||||
return getString(R.string.generate_wallet_type_ledger);
|
return getString(R.string.generate_wallet_type_ledger);
|
||||||
|
case TYPE_SIDEKICK:
|
||||||
|
return getString(R.string.generate_wallet_type_sidekick);
|
||||||
case TYPE_VIEWONLY:
|
case TYPE_VIEWONLY:
|
||||||
return getString(R.string.generate_wallet_type_view);
|
return getString(R.string.generate_wallet_type_view);
|
||||||
default:
|
default:
|
||||||
|
@ -515,7 +530,7 @@ public class GenerateFragment extends Fragment {
|
||||||
|
|
||||||
void onGenerate(String name, String password, String address, String viewKey, String spendKey, long height);
|
void onGenerate(String name, String password, String address, String viewKey, String spendKey, long height);
|
||||||
|
|
||||||
void onGenerateLedger(String name, String password, long height);
|
void onGenerateDevice(Wallet.Device device, String name, String password, long height);
|
||||||
|
|
||||||
void setTitle(String title);
|
void setTitle(String title);
|
||||||
|
|
||||||
|
@ -555,6 +570,9 @@ public class GenerateFragment extends Fragment {
|
||||||
case TYPE_LEDGER:
|
case TYPE_LEDGER:
|
||||||
inflater.inflate(R.menu.create_wallet_ledger, menu);
|
inflater.inflate(R.menu.create_wallet_ledger, menu);
|
||||||
break;
|
break;
|
||||||
|
case TYPE_SIDEKICK:
|
||||||
|
inflater.inflate(R.menu.create_wallet_sidekick, menu);
|
||||||
|
break;
|
||||||
case TYPE_VIEWONLY:
|
case TYPE_VIEWONLY:
|
||||||
inflater.inflate(R.menu.create_wallet_view, menu);
|
inflater.inflate(R.menu.create_wallet_view, menu);
|
||||||
break;
|
break;
|
||||||
|
@ -581,13 +599,11 @@ public class GenerateFragment extends Fragment {
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton(getString(R.string.label_ok), null)
|
.setPositiveButton(getString(R.string.label_ok), null)
|
||||||
.setNegativeButton(getString(R.string.label_cancel),
|
.setNegativeButton(getString(R.string.label_cancel),
|
||||||
new DialogInterface.OnClickListener() {
|
(dialog, id) -> {
|
||||||
public void onClick(DialogInterface dialog, int id) {
|
|
||||||
Helper.hideKeyboardAlways(activity);
|
Helper.hideKeyboardAlways(activity);
|
||||||
etWalletMnemonic.getEditText().getText().clear();
|
etWalletMnemonic.getEditText().getText().clear();
|
||||||
dialog.cancel();
|
dialog.cancel();
|
||||||
ledgerDialog = null;
|
ledgerDialog = null;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ledgerDialog = alertDialogBuilder.create();
|
ledgerDialog = alertDialogBuilder.create();
|
||||||
|
|
|
@ -99,7 +99,7 @@ public class GenerateReviewFragment extends Fragment {
|
||||||
private String walletPath;
|
private String walletPath;
|
||||||
private String walletName;
|
private String walletName;
|
||||||
|
|
||||||
private OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) {
|
private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) {
|
||||||
@Override
|
@Override
|
||||||
public void handleOnBackPressed() {
|
public void handleOnBackPressed() {
|
||||||
// nothing
|
// nothing
|
||||||
|
@ -164,6 +164,7 @@ public class GenerateReviewFragment extends Fragment {
|
||||||
});
|
});
|
||||||
|
|
||||||
Bundle args = getArguments();
|
Bundle args = getArguments();
|
||||||
|
assert args != null;
|
||||||
type = args.getString(REQUEST_TYPE);
|
type = args.getString(REQUEST_TYPE);
|
||||||
walletPath = args.getString(REQUEST_PATH);
|
walletPath = args.getString(REQUEST_PATH);
|
||||||
localPassword = args.getString(REQUEST_PASSWORD);
|
localPassword = args.getString(REQUEST_PASSWORD);
|
||||||
|
@ -254,7 +255,7 @@ public class GenerateReviewFragment extends Fragment {
|
||||||
showProgress();
|
showProgress();
|
||||||
if ((walletPath != null)
|
if ((walletPath != null)
|
||||||
&& (WalletManager.getInstance().queryWalletDevice(walletPath + ".keys", getPassword())
|
&& (WalletManager.getInstance().queryWalletDevice(walletPath + ".keys", getPassword())
|
||||||
== Wallet.Device.Device_Ledger)
|
== Wallet.Device.Ledger)
|
||||||
&& (progressCallback != null)) {
|
&& (progressCallback != null)) {
|
||||||
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
|
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
|
||||||
dialogOpened = true;
|
dialogOpened = true;
|
||||||
|
@ -286,10 +287,11 @@ public class GenerateReviewFragment extends Fragment {
|
||||||
height = wallet.getRestoreHeight();
|
height = wallet.getRestoreHeight();
|
||||||
seed = wallet.getSeed(getSeedOffset());
|
seed = wallet.getSeed(getSeedOffset());
|
||||||
switch (wallet.getDeviceType()) {
|
switch (wallet.getDeviceType()) {
|
||||||
case Device_Ledger:
|
case Ledger:
|
||||||
viewKey = Ledger.Key();
|
viewKey = Ledger.Key();
|
||||||
break;
|
break;
|
||||||
case Device_Software:
|
case Software:
|
||||||
|
case Sidekick:
|
||||||
viewKey = wallet.getSecretViewKey();
|
viewKey = wallet.getSecretViewKey();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -22,13 +22,14 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class LockFragment extends Fragment {
|
public class LockFragment extends Fragment {
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
Timber.d("onCreateView");
|
Timber.d("onCreateView");
|
||||||
final FrameLayout frame = new FrameLayout(requireContext());
|
final FrameLayout frame = new FrameLayout(requireContext());
|
||||||
frame.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
frame.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
|
@ -40,6 +40,7 @@ import android.widget.Toast;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
@ -55,6 +56,7 @@ import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
|
||||||
import com.m2049r.xmrwallet.model.NetworkType;
|
import com.m2049r.xmrwallet.model.NetworkType;
|
||||||
import com.m2049r.xmrwallet.model.Wallet;
|
import com.m2049r.xmrwallet.model.Wallet;
|
||||||
import com.m2049r.xmrwallet.model.WalletManager;
|
import com.m2049r.xmrwallet.model.WalletManager;
|
||||||
|
import com.m2049r.xmrwallet.service.BluetoothService;
|
||||||
import com.m2049r.xmrwallet.service.WalletService;
|
import com.m2049r.xmrwallet.service.WalletService;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||||
|
@ -76,13 +78,12 @@ import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class LoginActivity extends BaseActivity
|
public class LoginActivity extends BaseActivity
|
||||||
implements LoginFragment.Listener, GenerateFragment.Listener,
|
implements LoginFragment.Listener, GenerateFragment.Listener,
|
||||||
GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener,
|
GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener,
|
||||||
NodeFragment.Listener, SettingsFragment.Listener {
|
NodeFragment.Listener, SettingsFragment.Listener, SidekickConnectFragment.Listener, BluetoothFragment.Listener {
|
||||||
private static final String GENERATE_STACK = "gen";
|
private static final String GENERATE_STACK = "gen";
|
||||||
|
|
||||||
private static final String NODES_PREFS_NAME = "nodes";
|
private static final String NODES_PREFS_NAME = "nodes";
|
||||||
|
@ -278,8 +279,15 @@ public class LoginActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasLedger() {
|
public boolean hasDevice(Wallet.Device type) {
|
||||||
|
switch (type) {
|
||||||
|
case Ledger:
|
||||||
return Ledger.isConnected();
|
return Ledger.isConnected();
|
||||||
|
case Sidekick:
|
||||||
|
return BluetoothService.IsConnected();
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -311,8 +319,8 @@ public class LoginActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isNetworkAvailable())
|
||||||
loadFavouritesWithNetwork();
|
loadFavouritesWithNetwork();
|
||||||
|
|
||||||
LegacyStorageHelper.migrateWallets(this);
|
LegacyStorageHelper.migrateWallets(this);
|
||||||
|
|
||||||
if (savedInstanceState == null) startLoginFragment();
|
if (savedInstanceState == null) startLoginFragment();
|
||||||
|
@ -334,7 +342,7 @@ public class LoginActivity extends BaseActivity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onWalletSelected(String walletName, boolean streetmode) {
|
public boolean onWalletSelected(String walletName, boolean streetmode) {
|
||||||
if (node == null) {
|
if (isNetworkAvailable() && (node == null)) {
|
||||||
Toast.makeText(this, getString(R.string.prompt_daemon_missing), Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, getString(R.string.prompt_daemon_missing), Toast.LENGTH_SHORT).show();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -611,6 +619,7 @@ public class LoginActivity extends BaseActivity
|
||||||
try {
|
try {
|
||||||
GenerateReviewFragment detailsFragment = (GenerateReviewFragment)
|
GenerateReviewFragment detailsFragment = (GenerateReviewFragment)
|
||||||
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||||
|
assert detailsFragment != null;
|
||||||
AlertDialog dialog = detailsFragment.createChangePasswordDialog();
|
AlertDialog dialog = detailsFragment.createChangePasswordDialog();
|
||||||
if (dialog != null) {
|
if (dialog != null) {
|
||||||
Helper.showKeyboard(dialog);
|
Helper.showKeyboard(dialog);
|
||||||
|
@ -682,6 +691,7 @@ public class LoginActivity extends BaseActivity
|
||||||
dismissProgressDialog();
|
dismissProgressDialog();
|
||||||
unregisterDetachReceiver();
|
unregisterDetachReceiver();
|
||||||
Ledger.disconnect();
|
Ledger.disconnect();
|
||||||
|
BluetoothService.Stop();
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -695,6 +705,7 @@ public class LoginActivity extends BaseActivity
|
||||||
new AsyncWaitForService().execute();
|
new AsyncWaitForService().execute();
|
||||||
}
|
}
|
||||||
if (!Ledger.isConnected()) attachLedger();
|
if (!Ledger.isConnected()) attachLedger();
|
||||||
|
if (BluetoothService.IsConnected()) onLedgerAction(); //TODO sidekick & show sidekick fab
|
||||||
registerTor();
|
registerTor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -727,14 +738,14 @@ public class LoginActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void startWallet(String walletName, String walletPassword,
|
void startWallet(String walletName, String walletPassword, boolean fingerprintUsed, StartMode mode) {
|
||||||
boolean fingerprintUsed, boolean streetmode) {
|
|
||||||
Timber.d("startWallet()");
|
Timber.d("startWallet()");
|
||||||
|
|
||||||
Intent intent = new Intent(getApplicationContext(), WalletActivity.class);
|
Intent intent = new Intent(getApplicationContext(), WalletActivity.class);
|
||||||
intent.putExtra(WalletActivity.REQUEST_ID, walletName);
|
intent.putExtra(WalletActivity.REQUEST_ID, walletName);
|
||||||
intent.putExtra(WalletActivity.REQUEST_PW, walletPassword);
|
intent.putExtra(WalletActivity.REQUEST_PW, walletPassword);
|
||||||
intent.putExtra(WalletActivity.REQUEST_FINGERPRINT_USED, fingerprintUsed);
|
intent.putExtra(WalletActivity.REQUEST_FINGERPRINT_USED, fingerprintUsed);
|
||||||
intent.putExtra(WalletActivity.REQUEST_STREETMODE, streetmode);
|
intent.putExtra(WalletActivity.REQUEST_STREETMODE, mode == StartMode.Street);
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
intent.putExtra(WalletActivity.REQUEST_URI, uri);
|
intent.putExtra(WalletActivity.REQUEST_URI, uri);
|
||||||
uri = null; // use only once
|
uri = null; // use only once
|
||||||
|
@ -783,6 +794,11 @@ public class LoginActivity extends BaseActivity
|
||||||
Timber.d("SettingsFragment placed");
|
Timber.d("SettingsFragment placed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void startSidekickConnectFragment() {
|
||||||
|
replaceFragment(new SidekickConnectFragment(), null, null);
|
||||||
|
Timber.d("SidekickConnectFragment placed");
|
||||||
|
}
|
||||||
|
|
||||||
void replaceFragment(Fragment newFragment, String stackName, Bundle extras) {
|
void replaceFragment(Fragment newFragment, String stackName, Bundle extras) {
|
||||||
if (extras != null) {
|
if (extras != null) {
|
||||||
newFragment.setArguments(extras);
|
newFragment.setArguments(extras);
|
||||||
|
@ -821,7 +837,7 @@ public class LoginActivity extends BaseActivity
|
||||||
protected void onPreExecute() {
|
protected void onPreExecute() {
|
||||||
super.onPreExecute();
|
super.onPreExecute();
|
||||||
acquireWakeLock();
|
acquireWakeLock();
|
||||||
if (walletCreator.isLedger()) {
|
if (walletCreator.device() == Wallet.Device.Ledger) {
|
||||||
showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
|
showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
|
||||||
} else {
|
} else {
|
||||||
showProgressDialog(R.string.generate_wallet_creating);
|
showProgressDialog(R.string.generate_wallet_creating);
|
||||||
|
@ -881,6 +897,7 @@ public class LoginActivity extends BaseActivity
|
||||||
try {
|
try {
|
||||||
GenerateFragment genFragment = (GenerateFragment)
|
GenerateFragment genFragment = (GenerateFragment)
|
||||||
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||||
|
assert genFragment != null;
|
||||||
genFragment.walletGenerateError();
|
genFragment.walletGenerateError();
|
||||||
} catch (ClassCastException ex) {
|
} catch (ClassCastException ex) {
|
||||||
Timber.e("walletGenerateError() but not in GenerateFragment");
|
Timber.e("walletGenerateError() but not in GenerateFragment");
|
||||||
|
@ -890,8 +907,7 @@ public class LoginActivity extends BaseActivity
|
||||||
interface WalletCreator {
|
interface WalletCreator {
|
||||||
boolean createWallet(File aFile, String password);
|
boolean createWallet(File aFile, String password);
|
||||||
|
|
||||||
boolean isLedger();
|
Wallet.Device device();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean checkAndCloseWallet(Wallet aWallet) {
|
boolean checkAndCloseWallet(Wallet aWallet) {
|
||||||
|
@ -909,8 +925,8 @@ public class LoginActivity extends BaseActivity
|
||||||
createWallet(name, password,
|
createWallet(name, password,
|
||||||
new WalletCreator() {
|
new WalletCreator() {
|
||||||
@Override
|
@Override
|
||||||
public boolean isLedger() {
|
public Wallet.Device device() {
|
||||||
return false;
|
return Wallet.Device.Software;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -933,8 +949,8 @@ public class LoginActivity extends BaseActivity
|
||||||
createWallet(name, password,
|
createWallet(name, password,
|
||||||
new WalletCreator() {
|
new WalletCreator() {
|
||||||
@Override
|
@Override
|
||||||
public boolean isLedger() {
|
public Wallet.Device device() {
|
||||||
return false;
|
return Wallet.Device.Software;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -947,20 +963,19 @@ public class LoginActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGenerateLedger(final String name, final String password,
|
public void onGenerateDevice(final Wallet.Device device, final String name, final String password, long restoreHeight) {
|
||||||
final long restoreHeight) {
|
|
||||||
createWallet(name, password,
|
createWallet(name, password,
|
||||||
new WalletCreator() {
|
new WalletCreator() {
|
||||||
@Override
|
@Override
|
||||||
public boolean isLedger() {
|
public Wallet.Device device() {
|
||||||
return true;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean createWallet(File aFile, String password) {
|
public boolean createWallet(File aFile, String password) {
|
||||||
Wallet newWallet = WalletManager.getInstance()
|
Wallet newWallet = WalletManager.getInstance()
|
||||||
.createWalletFromDevice(aFile, password,
|
.createWalletFromDevice(aFile, password,
|
||||||
restoreHeight, "Ledger");
|
restoreHeight, device);
|
||||||
return checkAndCloseWallet(newWallet);
|
return checkAndCloseWallet(newWallet);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -973,8 +988,8 @@ public class LoginActivity extends BaseActivity
|
||||||
createWallet(name, password,
|
createWallet(name, password,
|
||||||
new WalletCreator() {
|
new WalletCreator() {
|
||||||
@Override
|
@Override
|
||||||
public boolean isLedger() {
|
public Wallet.Device device() {
|
||||||
return false;
|
return Wallet.Device.Software;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1105,6 +1120,9 @@ public class LoginActivity extends BaseActivity
|
||||||
} else if (id == R.id.action_create_help_ledger) {
|
} else if (id == R.id.action_create_help_ledger) {
|
||||||
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_ledger);
|
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_ledger);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (id == R.id.action_create_help_sidekick) {
|
||||||
|
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_sidekick);
|
||||||
|
return true;
|
||||||
} else if (id == R.id.action_details_help) {
|
} else if (id == R.id.action_details_help) {
|
||||||
HelpFragment.display(getSupportFragmentManager(), R.string.help_details);
|
HelpFragment.display(getSupportFragmentManager(), R.string.help_details);
|
||||||
return true;
|
return true;
|
||||||
|
@ -1117,6 +1135,9 @@ public class LoginActivity extends BaseActivity
|
||||||
} else if (id == R.id.action_help_node) {
|
} else if (id == R.id.action_help_node) {
|
||||||
HelpFragment.display(getSupportFragmentManager(), R.string.help_node);
|
HelpFragment.display(getSupportFragmentManager(), R.string.help_node);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (id == R.id.action_help_sidekick) {
|
||||||
|
HelpFragment.display(getSupportFragmentManager(), R.string.help_sidekick);
|
||||||
|
return true;
|
||||||
} else if (id == R.id.action_default_nodes) {
|
} else if (id == R.id.action_default_nodes) {
|
||||||
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||||
if ((WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) &&
|
if ((WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) &&
|
||||||
|
@ -1124,6 +1145,9 @@ public class LoginActivity extends BaseActivity
|
||||||
((NodeFragment) f).restoreDefaultNodes();
|
((NodeFragment) f).restoreDefaultNodes();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
} else if (id == R.id.action_sidekick) {
|
||||||
|
checkBtPermissions();
|
||||||
|
return true;
|
||||||
} else if (id == R.id.action_ledger_seed) {
|
} else if (id == R.id.action_ledger_seed) {
|
||||||
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||||
if (f instanceof GenerateFragment) {
|
if (f instanceof GenerateFragment) {
|
||||||
|
@ -1135,6 +1159,11 @@ public class LoginActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void btPermissionGranted() {
|
||||||
|
startSidekickConnectFragment();
|
||||||
|
}
|
||||||
|
|
||||||
// an AsyncTask which tests the node before trying to open the wallet
|
// an AsyncTask which tests the node before trying to open the wallet
|
||||||
private class AsyncOpenWallet extends AsyncTask<Void, Void, Boolean> {
|
private class AsyncOpenWallet extends AsyncTask<Void, Void, Boolean> {
|
||||||
final static int OK = 0;
|
final static int OK = 0;
|
||||||
|
@ -1172,7 +1201,7 @@ public class LoginActivity extends BaseActivity
|
||||||
if (result) {
|
if (result) {
|
||||||
Timber.d("selected wallet is .%s.", node.getName());
|
Timber.d("selected wallet is .%s.", node.getName());
|
||||||
// now it's getting real, onValidateFields if wallet exists
|
// now it's getting real, onValidateFields if wallet exists
|
||||||
promptAndStart(walletName, streetmode);
|
promptAndStart(walletName, streetmode ? StartMode.Street : StartMode.Normal);
|
||||||
} else {
|
} else {
|
||||||
if (node.getResponseCode() == 0) { // IOException
|
if (node.getResponseCode() == 0) { // IOException
|
||||||
Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_node_invalid), Toast.LENGTH_LONG).show();
|
Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_node_invalid), Toast.LENGTH_LONG).show();
|
||||||
|
@ -1184,25 +1213,25 @@ public class LoginActivity extends BaseActivity
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean checkDevice(String walletName, String password) {
|
private boolean checkDevice(String walletName, String password) {
|
||||||
String keyPath = new File(Helper.getWalletRoot(LoginActivity.this),
|
String keyPath = new File(Helper.getWalletRoot(this), walletName + ".keys").getAbsolutePath();
|
||||||
walletName + ".keys").getAbsolutePath();
|
|
||||||
// check if we need connected hardware
|
// check if we need connected hardware
|
||||||
Wallet.Device device = WalletManager.getInstance().queryWalletDevice(keyPath, password);
|
final Wallet.Device device = WalletManager.getInstance().queryWalletDevice(keyPath, password);
|
||||||
if (device == Wallet.Device.Device_Ledger) {
|
if (!hasDevice(device)) {
|
||||||
if (!hasLedger()) {
|
if (device == Wallet.Device.Ledger) {
|
||||||
toast(R.string.open_wallet_ledger_missing);
|
toast(R.string.open_wallet_ledger_missing);
|
||||||
} else {
|
} else if (device == Wallet.Device.Sidekick) {
|
||||||
return true;
|
toast(R.string.open_wallet_sidekick_missing);
|
||||||
}
|
|
||||||
} else {// device could be undefined meaning the password is wrong
|
|
||||||
// this gets dealt with later
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// else // device could be undefined meaning the password is wrong
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void promptAndStart(String walletName, final boolean streetmode) {
|
enum StartMode {Normal, Street}
|
||||||
|
|
||||||
|
void promptAndStart(String walletName, final StartMode mode) {
|
||||||
File walletFile = Helper.getWalletFile(this, walletName);
|
File walletFile = Helper.getWalletFile(this, walletName);
|
||||||
if (WalletManager.getInstance().walletExists(walletFile)) {
|
if (WalletManager.getInstance().walletExists(walletFile)) {
|
||||||
Helper.promptPassword(LoginActivity.this, walletName, false,
|
Helper.promptPassword(LoginActivity.this, walletName, false,
|
||||||
|
@ -1210,7 +1239,7 @@ public class LoginActivity extends BaseActivity
|
||||||
@Override
|
@Override
|
||||||
public void act(String walletName, String password, boolean fingerprintUsed) {
|
public void act(String walletName, String password, boolean fingerprintUsed) {
|
||||||
if (checkDevice(walletName, password))
|
if (checkDevice(walletName, password))
|
||||||
startWallet(walletName, password, fingerprintUsed, streetmode);
|
startWallet(walletName, password, fingerprintUsed, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1234,7 +1263,7 @@ public class LoginActivity extends BaseActivity
|
||||||
if (usbManager.hasPermission(device)) {
|
if (usbManager.hasPermission(device)) {
|
||||||
connectLedger(usbManager, device);
|
connectLedger(usbManager, device);
|
||||||
} else {
|
} else {
|
||||||
registerReceiver(usbPermissionReceiver, new IntentFilter(ACTION_USB_PERMISSION));
|
ContextCompat.registerReceiver(this, usbPermissionReceiver, new IntentFilter(ACTION_USB_PERMISSION), ContextCompat.RECEIVER_EXPORTED);
|
||||||
usbManager.requestPermission(device,
|
usbManager.requestPermission(device,
|
||||||
PendingIntent.getBroadcast(this, 0,
|
PendingIntent.getBroadcast(this, 0,
|
||||||
new Intent(ACTION_USB_PERMISSION),
|
new Intent(ACTION_USB_PERMISSION),
|
||||||
|
@ -1339,7 +1368,7 @@ public class LoginActivity extends BaseActivity
|
||||||
private void registerDetachReceiver() {
|
private void registerDetachReceiver() {
|
||||||
detachReceiver = new BroadcastReceiver() {
|
detachReceiver = new BroadcastReceiver() {
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
|
if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(intent.getAction())) {
|
||||||
unregisterDetachReceiver();
|
unregisterDetachReceiver();
|
||||||
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||||
Timber.i("Ledger detached!");
|
Timber.i("Ledger detached!");
|
||||||
|
@ -1353,8 +1382,7 @@ public class LoginActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
ContextCompat.registerReceiver(this, detachReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED), ContextCompat.RECEIVER_EXPORTED);
|
||||||
registerReceiver(detachReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onLedgerAction() {
|
public void onLedgerAction() {
|
||||||
|
@ -1378,6 +1406,34 @@ public class LoginActivity extends BaseActivity
|
||||||
return usbManager;
|
return usbManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeviceConnected(String connectedDeviceName) {
|
||||||
|
Timber.d("onDeviceConnected: %s", connectedDeviceName);
|
||||||
|
try {
|
||||||
|
SidekickConnectFragment f = (SidekickConnectFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||||
|
if (f == null) return;
|
||||||
|
f.allowClick();
|
||||||
|
} catch (ClassCastException ex) {
|
||||||
|
// ignore it
|
||||||
|
}
|
||||||
|
if (connectedDeviceName != null) {
|
||||||
|
setSubtitle(getString(R.string.sidekick_connected));
|
||||||
|
} else {
|
||||||
|
setSubtitle(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void abort(String message) {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||||
|
onBackPressed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(int command) {
|
||||||
|
Timber.e("this should not be");
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Tor (Orbot) stuff
|
// Tor (Orbot) stuff
|
||||||
//
|
//
|
||||||
|
|
|
@ -46,6 +46,7 @@ import com.google.android.material.progressindicator.CircularProgressIndicator;
|
||||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||||
import com.m2049r.xmrwallet.dialog.HelpFragment;
|
import com.m2049r.xmrwallet.dialog.HelpFragment;
|
||||||
import com.m2049r.xmrwallet.layout.WalletInfoAdapter;
|
import com.m2049r.xmrwallet.layout.WalletInfoAdapter;
|
||||||
|
import com.m2049r.xmrwallet.model.Wallet;
|
||||||
import com.m2049r.xmrwallet.model.WalletManager;
|
import com.m2049r.xmrwallet.model.WalletManager;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||||
|
@ -61,6 +62,7 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInteractionListener,
|
public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInteractionListener,
|
||||||
|
@ -115,7 +117,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
|
|
||||||
Set<NodeInfo> getOrPopulateFavourites();
|
Set<NodeInfo> getOrPopulateFavourites();
|
||||||
|
|
||||||
boolean hasLedger();
|
boolean hasDevice(Wallet.Device type);
|
||||||
|
|
||||||
|
boolean isNetworkAvailable();
|
||||||
|
|
||||||
void runOnNetCipher(Runnable runnable);
|
void runOnNetCipher(Runnable runnable);
|
||||||
}
|
}
|
||||||
|
@ -146,10 +150,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
activityCallback.setToolbarButton(Toolbar.BUTTON_SETTINGS);
|
activityCallback.setToolbarButton(Toolbar.BUTTON_SETTINGS);
|
||||||
activityCallback.showNet();
|
activityCallback.showNet();
|
||||||
showNetwork();
|
showNetwork();
|
||||||
//activityCallback.runOnNetCipher(this::pingSelectedNode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
|
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
|
||||||
@Override
|
@Override
|
||||||
public void handleOnBackPressed() {
|
public void handleOnBackPressed() {
|
||||||
animateFAB();
|
animateFAB();
|
||||||
|
@ -172,6 +175,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
fabSeed = view.findViewById(R.id.fabSeed);
|
fabSeed = view.findViewById(R.id.fabSeed);
|
||||||
fabImport = view.findViewById(R.id.fabImport);
|
fabImport = view.findViewById(R.id.fabImport);
|
||||||
fabLedger = view.findViewById(R.id.fabLedger);
|
fabLedger = view.findViewById(R.id.fabLedger);
|
||||||
|
fabSidekick = view.findViewById(R.id.fabSidekick);
|
||||||
|
|
||||||
fabNewL = view.findViewById(R.id.fabNewL);
|
fabNewL = view.findViewById(R.id.fabNewL);
|
||||||
fabViewL = view.findViewById(R.id.fabViewL);
|
fabViewL = view.findViewById(R.id.fabViewL);
|
||||||
|
@ -179,6 +183,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
fabSeedL = view.findViewById(R.id.fabSeedL);
|
fabSeedL = view.findViewById(R.id.fabSeedL);
|
||||||
fabImportL = view.findViewById(R.id.fabImportL);
|
fabImportL = view.findViewById(R.id.fabImportL);
|
||||||
fabLedgerL = view.findViewById(R.id.fabLedgerL);
|
fabLedgerL = view.findViewById(R.id.fabLedgerL);
|
||||||
|
fabSidekickL = view.findViewById(R.id.fabSidekickL);
|
||||||
|
|
||||||
fab_pulse = AnimationUtils.loadAnimation(getContext(), R.anim.fab_pulse);
|
fab_pulse = AnimationUtils.loadAnimation(getContext(), R.anim.fab_pulse);
|
||||||
fab_open_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open_screen);
|
fab_open_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open_screen);
|
||||||
|
@ -194,6 +199,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
fabSeed.setOnClickListener(this);
|
fabSeed.setOnClickListener(this);
|
||||||
fabImport.setOnClickListener(this);
|
fabImport.setOnClickListener(this);
|
||||||
fabLedger.setOnClickListener(this);
|
fabLedger.setOnClickListener(this);
|
||||||
|
fabSidekick.setOnClickListener(this);
|
||||||
fabScreen.setOnClickListener(this);
|
fabScreen.setOnClickListener(this);
|
||||||
|
|
||||||
RecyclerView recyclerView = view.findViewById(R.id.list);
|
RecyclerView recyclerView = view.findViewById(R.id.list);
|
||||||
|
@ -202,6 +208,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
ViewGroup llNotice = view.findViewById(R.id.llNotice);
|
ViewGroup llNotice = view.findViewById(R.id.llNotice);
|
||||||
|
|
||||||
Notice.showAll(llNotice, ".*_login");
|
Notice.showAll(llNotice, ".*_login");
|
||||||
|
|
||||||
view.findViewById(R.id.llNode).setOnClickListener(v -> startNodePrefs());
|
view.findViewById(R.id.llNode).setOnClickListener(v -> startNodePrefs());
|
||||||
|
@ -276,7 +283,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove information of non-existent wallet
|
// remove information of non-existent wallet
|
||||||
Set<String> removedWallets = getActivity()
|
Set<String> removedWallets = requireActivity()
|
||||||
.getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
|
.getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
.getAll().keySet();
|
.getAll().keySet();
|
||||||
for (WalletManager.WalletInfo s : walletList) {
|
for (WalletManager.WalletInfo s : walletList) {
|
||||||
|
@ -304,17 +311,14 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
private boolean fabOpen = false;
|
private boolean fabOpen = false;
|
||||||
private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabImport, fabLedger;
|
private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabImport, fabLedger, fabSidekick;
|
||||||
private RelativeLayout fabScreen;
|
private RelativeLayout fabScreen;
|
||||||
private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL, fabImportL, fabLedgerL;
|
private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL, fabImportL, fabLedgerL, fabSidekickL;
|
||||||
private Animation fab_open, fab_close, rotate_forward, rotate_backward, fab_open_screen, fab_close_screen;
|
private Animation fab_open, fab_close, rotate_forward, rotate_backward, fab_open_screen, fab_close_screen;
|
||||||
private Animation fab_pulse;
|
private Animation fab_pulse;
|
||||||
|
|
||||||
public boolean isFabOpen() {
|
|
||||||
return fabOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setFabOpen(boolean value) {
|
private void setFabOpen(boolean value) {
|
||||||
fabOpen = value;
|
fabOpen = value;
|
||||||
onBackPressedCallback.setEnabled(value);
|
onBackPressedCallback.setEnabled(value);
|
||||||
|
@ -328,6 +332,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
if (fabLedgerL.getVisibility() == View.VISIBLE) {
|
if (fabLedgerL.getVisibility() == View.VISIBLE) {
|
||||||
fabLedgerL.startAnimation(fab_close);
|
fabLedgerL.startAnimation(fab_close);
|
||||||
fabLedger.setClickable(false);
|
fabLedger.setClickable(false);
|
||||||
|
} else if (fabSidekickL.getVisibility() == View.VISIBLE) {
|
||||||
|
fabSidekickL.startAnimation(fab_close);
|
||||||
|
fabSidekick.setClickable(false);
|
||||||
} else {
|
} else {
|
||||||
fabNewL.startAnimation(fab_close);
|
fabNewL.startAnimation(fab_close);
|
||||||
fabNew.setClickable(false);
|
fabNew.setClickable(false);
|
||||||
|
@ -345,17 +352,25 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
fabScreen.setClickable(true);
|
fabScreen.setClickable(true);
|
||||||
fabScreen.startAnimation(fab_open_screen);
|
fabScreen.startAnimation(fab_open_screen);
|
||||||
fab.startAnimation(rotate_forward);
|
fab.startAnimation(rotate_forward);
|
||||||
if (activityCallback.hasLedger()) {
|
if ((activityCallback.hasDevice(Wallet.Device.Ledger)
|
||||||
fabLedgerL.setVisibility(View.VISIBLE);
|
|| activityCallback.hasDevice(Wallet.Device.Sidekick))) {
|
||||||
fabNewL.setVisibility(View.GONE);
|
fabNewL.setVisibility(View.GONE);
|
||||||
fabViewL.setVisibility(View.GONE);
|
fabViewL.setVisibility(View.GONE);
|
||||||
fabKeyL.setVisibility(View.GONE);
|
fabKeyL.setVisibility(View.GONE);
|
||||||
fabSeedL.setVisibility(View.GONE);
|
fabSeedL.setVisibility(View.GONE);
|
||||||
fabImportL.setVisibility(View.GONE);
|
fabImportL.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
if (activityCallback.hasDevice(Wallet.Device.Ledger)) {
|
||||||
|
fabLedgerL.setVisibility(View.VISIBLE);
|
||||||
fabLedgerL.startAnimation(fab_open);
|
fabLedgerL.startAnimation(fab_open);
|
||||||
fabLedger.setClickable(true);
|
fabLedger.setClickable(true);
|
||||||
|
} else { // Sidekick
|
||||||
|
fabSidekickL.setVisibility(View.VISIBLE);
|
||||||
|
fabSidekickL.startAnimation(fab_open);
|
||||||
|
fabSidekick.setClickable(true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
fabSidekickL.setVisibility(View.GONE);
|
||||||
fabLedgerL.setVisibility(View.GONE);
|
fabLedgerL.setVisibility(View.GONE);
|
||||||
fabNewL.setVisibility(View.VISIBLE);
|
fabNewL.setVisibility(View.VISIBLE);
|
||||||
fabViewL.setVisibility(View.VISIBLE);
|
fabViewL.setVisibility(View.VISIBLE);
|
||||||
|
@ -404,6 +419,10 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
Timber.d("FAB_LEDGER");
|
Timber.d("FAB_LEDGER");
|
||||||
animateFAB();
|
animateFAB();
|
||||||
activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER);
|
activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER);
|
||||||
|
} else if (id == R.id.fabSidekick) {
|
||||||
|
Timber.d("FAB_SIDEKICK");
|
||||||
|
animateFAB();
|
||||||
|
activityCallback.onAddWallet(GenerateFragment.TYPE_SIDEKICK);
|
||||||
} else if (id == R.id.fabScreen) {
|
} else if (id == R.id.fabScreen) {
|
||||||
animateFAB();
|
animateFAB();
|
||||||
}
|
}
|
||||||
|
@ -426,7 +445,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSubtext(String status) {
|
private void setSubtext(String status) {
|
||||||
final Context ctx = getContext();
|
final Context ctx = requireContext();
|
||||||
final Spanned text = Html.fromHtml(ctx.getString(R.string.status,
|
final Spanned text = Html.fromHtml(ctx.getString(R.string.status,
|
||||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
|
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
|
||||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),
|
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),
|
||||||
|
|
|
@ -32,6 +32,7 @@ import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.OnBackPressedCallback;
|
import androidx.activity.OnBackPressedCallback;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
@ -68,7 +69,7 @@ public class NodeFragment extends Fragment
|
||||||
|
|
||||||
static private int NODES_TO_FIND = 10;
|
static private int NODES_TO_FIND = 10;
|
||||||
|
|
||||||
static private NumberFormat FORMATTER = NumberFormat.getInstance();
|
static private final NumberFormat FORMATTER = NumberFormat.getInstance();
|
||||||
|
|
||||||
private SwipeRefreshLayout pullToRefresh;
|
private SwipeRefreshLayout pullToRefresh;
|
||||||
private TextView tvPull;
|
private TextView tvPull;
|
||||||
|
@ -104,7 +105,7 @@ public class NodeFragment extends Fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
if (context instanceof Listener) {
|
if (context instanceof Listener) {
|
||||||
this.activityCallback = (Listener) context;
|
this.activityCallback = (Listener) context;
|
||||||
|
@ -146,7 +147,7 @@ public class NodeFragment extends Fragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
|
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
|
||||||
@Override
|
@Override
|
||||||
public void handleOnBackPressed() {
|
public void handleOnBackPressed() {
|
||||||
Toast.makeText(requireActivity(), getString(R.string.node_refresh_wait), Toast.LENGTH_LONG).show();
|
Toast.makeText(requireActivity(), getString(R.string.node_refresh_wait), Toast.LENGTH_LONG).show();
|
||||||
|
@ -210,7 +211,7 @@ public class NodeFragment extends Fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.node_menu, menu);
|
inflater.inflate(R.menu.node_menu, menu);
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
}
|
}
|
||||||
|
@ -289,8 +290,7 @@ public class NodeFragment extends Fragment
|
||||||
} else if (params[0] == SCAN) {
|
} else if (params[0] == SCAN) {
|
||||||
// otherwise scan the network
|
// otherwise scan the network
|
||||||
Timber.d("scanning");
|
Timber.d("scanning");
|
||||||
Set<NodeInfo> seedList = new HashSet<>();
|
Set<NodeInfo> seedList = new HashSet<>(nodeList);
|
||||||
seedList.addAll(nodeList);
|
|
||||||
nodeList.clear();
|
nodeList.clear();
|
||||||
Timber.d("seed %d", seedList.size());
|
Timber.d("seed %d", seedList.size());
|
||||||
Dispatcher d = new Dispatcher(info -> publishProgress(info));
|
Dispatcher d = new Dispatcher(info -> publishProgress(info));
|
||||||
|
@ -455,7 +455,7 @@ public class NodeFragment extends Fragment
|
||||||
|
|
||||||
private void closeDialog() {
|
private void closeDialog() {
|
||||||
if (editDialog == null) throw new IllegalStateException();
|
if (editDialog == null) throw new IllegalStateException();
|
||||||
Helper.hideKeyboardAlways(getActivity());
|
Helper.hideKeyboardAlways(requireActivity());
|
||||||
editDialog.dismiss();
|
editDialog.dismiss();
|
||||||
editDialog = null;
|
editDialog = null;
|
||||||
NodeFragment.this.editDialog = null;
|
NodeFragment.this.editDialog = null;
|
||||||
|
|
|
@ -32,12 +32,10 @@ import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageButton;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
@ -84,8 +82,6 @@ public class ReceiveFragment extends Fragment {
|
||||||
private ImageView ivQrCode;
|
private ImageView ivQrCode;
|
||||||
private ImageView ivQrCodeFull;
|
private ImageView ivQrCodeFull;
|
||||||
private EditText etDummy;
|
private EditText etDummy;
|
||||||
private ImageButton bCopyAddress;
|
|
||||||
private MenuItem shareItem;
|
|
||||||
|
|
||||||
private Wallet wallet = null;
|
private Wallet wallet = null;
|
||||||
private boolean isMyWallet = false;
|
private boolean isMyWallet = false;
|
||||||
|
@ -116,11 +112,10 @@ public class ReceiveFragment extends Fragment {
|
||||||
tvQrCode = view.findViewById(R.id.tvQrCode);
|
tvQrCode = view.findViewById(R.id.tvQrCode);
|
||||||
ivQrCodeFull = view.findViewById(R.id.qrCodeFull);
|
ivQrCodeFull = view.findViewById(R.id.qrCodeFull);
|
||||||
etDummy = view.findViewById(R.id.etDummy);
|
etDummy = view.findViewById(R.id.etDummy);
|
||||||
bCopyAddress = view.findViewById(R.id.bCopyAddress);
|
|
||||||
|
|
||||||
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
|
|
||||||
bCopyAddress.setOnClickListener(v -> copyAddress());
|
view.findViewById(R.id.bCopyAddress).setOnClickListener(v -> copyAddress());
|
||||||
|
|
||||||
evAmount.setOnNewAmountListener(xmr -> {
|
evAmount.setOnNewAmountListener(xmr -> {
|
||||||
Timber.d("new amount = %s", xmr);
|
Timber.d("new amount = %s", xmr);
|
||||||
|
@ -211,8 +206,7 @@ public class ReceiveFragment extends Fragment {
|
||||||
inflater.inflate(R.menu.receive_menu, menu);
|
inflater.inflate(R.menu.receive_menu, menu);
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
|
||||||
shareItem = menu.findItem(R.id.menu_item_share);
|
menu.findItem(R.id.menu_item_share).setOnMenuItemClickListener(item -> {
|
||||||
shareItem.setOnMenuItemClickListener(item -> {
|
|
||||||
if (shareRequested) return true;
|
if (shareRequested) return true;
|
||||||
shareRequested = true;
|
shareRequested = true;
|
||||||
if (!qrValid) {
|
if (!qrValid) {
|
||||||
|
@ -238,7 +232,7 @@ public class ReceiveFragment extends Fragment {
|
||||||
private boolean saveQrCode() {
|
private boolean saveQrCode() {
|
||||||
if (!qrValid) throw new IllegalStateException("trying to save null qr code!");
|
if (!qrValid) throw new IllegalStateException("trying to save null qr code!");
|
||||||
|
|
||||||
File cachePath = new File(getActivity().getCacheDir(), "images");
|
File cachePath = new File(requireActivity().getCacheDir(), "images");
|
||||||
if (!cachePath.exists())
|
if (!cachePath.exists())
|
||||||
if (!cachePath.mkdirs()) throw new IllegalStateException("cannot create images folder");
|
if (!cachePath.mkdirs()) throw new IllegalStateException("cannot create images folder");
|
||||||
File png = new File(cachePath, "QR.png");
|
File png = new File(cachePath, "QR.png");
|
||||||
|
@ -452,7 +446,7 @@ public class ReceiveFragment extends Fragment {
|
||||||
.withEndAction(resetSize).start();
|
.withEndAction(resetSize).start();
|
||||||
}
|
}
|
||||||
subaddress = newSubaddress;
|
subaddress = newSubaddress;
|
||||||
final Context context = getContext();
|
final Context context = requireContext();
|
||||||
Spanned label = Html.fromHtml(context.getString(R.string.receive_subaddress,
|
Spanned label = Html.fromHtml(context.getString(R.string.receive_subaddress,
|
||||||
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF),
|
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF),
|
||||||
Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF),
|
Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF),
|
||||||
|
|
|
@ -24,6 +24,7 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import com.google.zxing.BarcodeFormat;
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
@ -43,7 +44,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
|
||||||
private ZXingScannerView mScannerView;
|
private ZXingScannerView mScannerView;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
Timber.d("onCreateView");
|
Timber.d("onCreateView");
|
||||||
mScannerView = new ZXingScannerView(getActivity());
|
mScannerView = new ZXingScannerView(getActivity());
|
||||||
return mScannerView;
|
return mScannerView;
|
||||||
|
@ -85,7 +86,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
if (context instanceof OnScannedListener) {
|
if (context instanceof OnScannedListener) {
|
||||||
this.onScannedListener = (OnScannedListener) context;
|
this.onScannedListener = (OnScannedListener) context;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.StyleRes;
|
import androidx.annotation.StyleRes;
|
||||||
import androidx.preference.ListPreference;
|
import androidx.preference.ListPreference;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
@ -60,7 +61,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||||
private SettingsFragment.Listener activity;
|
private SettingsFragment.Listener activity;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
if (context instanceof SettingsFragment.Listener) {
|
if (context instanceof SettingsFragment.Listener) {
|
||||||
activity = (SettingsFragment.Listener) context;
|
activity = (SettingsFragment.Listener) context;
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018 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 com.m2049r.xmrwallet;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
|
import android.bluetooth.BluetoothClass;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.data.BluetoothInfo;
|
||||||
|
import com.m2049r.xmrwallet.layout.BluetoothInfoAdapter;
|
||||||
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
|
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
public class SidekickConnectFragment extends Fragment
|
||||||
|
implements BluetoothInfoAdapter.OnInteractionListener {
|
||||||
|
|
||||||
|
private BluetoothAdapter bluetoothAdapter;
|
||||||
|
|
||||||
|
private SwipeRefreshLayout pullToRefresh;
|
||||||
|
|
||||||
|
private BluetoothInfoAdapter infoAdapter;
|
||||||
|
|
||||||
|
private Listener activityCallback;
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void setToolbarButton(int type);
|
||||||
|
|
||||||
|
void setSubtitle(String title);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(@NonNull Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
if (context instanceof Listener) {
|
||||||
|
this.activityCallback = (Listener) context;
|
||||||
|
} else {
|
||||||
|
throw new ClassCastException(context + " must implement Listener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
Timber.d("onPause()");
|
||||||
|
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Toast.makeText(requireContext(), "Bluetooth permission not granted", Toast.LENGTH_LONG).show();
|
||||||
|
} else {
|
||||||
|
if (bluetoothAdapter.isDiscovering()) {
|
||||||
|
bluetoothAdapter.cancelDiscovery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
Timber.d("onResume()");
|
||||||
|
activityCallback.setSubtitle(getString(R.string.label_bluetooth));
|
||||||
|
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
|
||||||
|
final BluetoothFragment btFragment = (BluetoothFragment) getChildFragmentManager().findFragmentById(R.id.bt_fragment);
|
||||||
|
assert btFragment != null;
|
||||||
|
btFragment.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
Timber.d("onCreateView");
|
||||||
|
View view = inflater.inflate(R.layout.fragment_sidekick_connect, container, false);
|
||||||
|
|
||||||
|
RecyclerView recyclerView = view.findViewById(R.id.list);
|
||||||
|
infoAdapter = new BluetoothInfoAdapter(this);
|
||||||
|
recyclerView.setAdapter(infoAdapter);
|
||||||
|
|
||||||
|
pullToRefresh = view.findViewById(R.id.pullToRefresh);
|
||||||
|
pullToRefresh.setOnRefreshListener(() -> {
|
||||||
|
populateList();
|
||||||
|
pullToRefresh.setRefreshing(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateList() {
|
||||||
|
List<BluetoothInfo> items = new ArrayList<>();
|
||||||
|
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Toast.makeText(requireContext(), "Bluetooth permission not granted", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
|
||||||
|
final int deviceCLass = device.getBluetoothClass().getDeviceClass();
|
||||||
|
switch (deviceCLass) {
|
||||||
|
case BluetoothClass.Device.PHONE_SMART:
|
||||||
|
//TODO verify these are correct
|
||||||
|
case BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA:
|
||||||
|
case BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA:
|
||||||
|
items.add(new BluetoothInfo(device));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
infoAdapter.setItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
Helper.hideKeyboard(getActivity());
|
||||||
|
|
||||||
|
// Get the local Bluetooth adapter
|
||||||
|
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
populateList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
|
||||||
|
inflater.inflate(R.menu.sidekick_connect_menu, menu);
|
||||||
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
// Make sure we're not doing discovery anymore
|
||||||
|
if (bluetoothAdapter != null) {
|
||||||
|
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Toast.makeText(requireContext(), "Bluetooth permission not granted", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bluetoothAdapter.cancelDiscovery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInteraction(final View view, final BluetoothInfo item) {
|
||||||
|
Timber.d("onInteraction %s", item);
|
||||||
|
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
|
||||||
|
throw new IllegalStateException("Bluetooth permission not granted");
|
||||||
|
bluetoothAdapter.cancelDiscovery();
|
||||||
|
|
||||||
|
final BluetoothFragment btFragment = (BluetoothFragment) getChildFragmentManager().findFragmentById(R.id.bt_fragment);
|
||||||
|
assert btFragment != null;
|
||||||
|
btFragment.connectDevice(item.getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void allowClick() {
|
||||||
|
infoAdapter.allowClick(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -195,7 +195,7 @@ public class SubaddressFragment extends Fragment implements SubaddressInfoAdapte
|
||||||
@Override
|
@Override
|
||||||
protected void onPreExecute() {
|
protected void onPreExecute() {
|
||||||
super.onPreExecute();
|
super.onPreExecute();
|
||||||
if ((wallet.getDeviceType() == Wallet.Device.Device_Ledger) && (progressCallback != null)) {
|
if ((wallet.getDeviceType() == Wallet.Device.Ledger) && (progressCallback != null)) {
|
||||||
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_SUBADDRESS);
|
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_SUBADDRESS);
|
||||||
dialogOpened = true;
|
dialogOpened = true;
|
||||||
}
|
}
|
||||||
|
@ -226,7 +226,7 @@ public class SubaddressFragment extends Fragment implements SubaddressInfoAdapte
|
||||||
|
|
||||||
// Callbacks from SubaddressInfoAdapter
|
// Callbacks from SubaddressInfoAdapter
|
||||||
@Override
|
@Override
|
||||||
public void onInteraction(final View view, final Subaddress subaddress) {
|
public void onInteraction(final View view, @NonNull final Subaddress subaddress) {
|
||||||
if (managerMode)
|
if (managerMode)
|
||||||
activityCallback.showSubaddress(view, subaddress.getAddressIndex());
|
activityCallback.showSubaddress(view, subaddress.getAddressIndex());
|
||||||
else
|
else
|
||||||
|
|
|
@ -52,7 +52,6 @@ public class SubaddressInfoFragment extends Fragment
|
||||||
private Subaddress subaddress;
|
private Subaddress subaddress;
|
||||||
|
|
||||||
private TextInputLayout etName;
|
private TextInputLayout etName;
|
||||||
private TextView tvAddress;
|
|
||||||
private TextView tvTxLabel;
|
private TextView tvTxLabel;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -61,7 +60,6 @@ public class SubaddressInfoFragment extends Fragment
|
||||||
View view = inflater.inflate(R.layout.fragment_subaddressinfo, container, false);
|
View view = inflater.inflate(R.layout.fragment_subaddressinfo, container, false);
|
||||||
|
|
||||||
etName = view.findViewById(R.id.etName);
|
etName = view.findViewById(R.id.etName);
|
||||||
tvAddress = view.findViewById(R.id.tvAddress);
|
|
||||||
tvTxLabel = view.findViewById(R.id.tvTxLabel);
|
tvTxLabel = view.findViewById(R.id.tvTxLabel);
|
||||||
|
|
||||||
final RecyclerView list = view.findViewById(R.id.list);
|
final RecyclerView list = view.findViewById(R.id.list);
|
||||||
|
@ -71,11 +69,13 @@ public class SubaddressInfoFragment extends Fragment
|
||||||
final Wallet wallet = activityCallback.getWallet();
|
final Wallet wallet = activityCallback.getWallet();
|
||||||
|
|
||||||
Bundle b = getArguments();
|
Bundle b = getArguments();
|
||||||
|
assert b != null;
|
||||||
final int subaddressIndex = b.getInt("subaddressIndex");
|
final int subaddressIndex = b.getInt("subaddressIndex");
|
||||||
subaddress = wallet.getSubaddressObject(subaddressIndex);
|
subaddress = wallet.getSubaddressObject(subaddressIndex);
|
||||||
|
|
||||||
etName.getEditText().setText(subaddress.getDisplayLabel());
|
etName.getEditText().setText(subaddress.getDisplayLabel());
|
||||||
tvAddress.setText(getContext().getString(R.string.subbaddress_info_subtitle,
|
final TextView tvAddress = view.findViewById(R.id.tvAddress);
|
||||||
|
tvAddress.setText(requireContext().getString(R.string.subbaddress_info_subtitle,
|
||||||
subaddress.getAddressIndex(), subaddress.getAddress()));
|
subaddress.getAddressIndex(), subaddress.getAddress()));
|
||||||
|
|
||||||
etName.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
etName.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
|
|
@ -20,7 +20,6 @@ import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
|
@ -34,6 +33,7 @@ import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.transition.Transition;
|
import androidx.transition.Transition;
|
||||||
|
@ -44,6 +44,8 @@ import com.m2049r.xmrwallet.data.UserNotes;
|
||||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||||
import com.m2049r.xmrwallet.model.Transfer;
|
import com.m2049r.xmrwallet.model.Transfer;
|
||||||
import com.m2049r.xmrwallet.model.Wallet;
|
import com.m2049r.xmrwallet.model.Wallet;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||||
|
@ -61,6 +63,7 @@ public class TxFragment extends Fragment {
|
||||||
|
|
||||||
static public final String ARG_INFO = "info";
|
static public final String ARG_INFO = "info";
|
||||||
|
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
|
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
|
||||||
|
|
||||||
public TxFragment() {
|
public TxFragment() {
|
||||||
|
@ -106,6 +109,7 @@ public class TxFragment extends Fragment {
|
||||||
tvTxAmountBtc = view.findViewById(R.id.tvTxAmountBtc);
|
tvTxAmountBtc = view.findViewById(R.id.tvTxAmountBtc);
|
||||||
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
|
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
|
||||||
tvXmrToKeyLabel = view.findViewById(R.id.tvXmrToKeyLabel);
|
tvXmrToKeyLabel = view.findViewById(R.id.tvXmrToKeyLabel);
|
||||||
|
|
||||||
tvXmrToLogo = view.findViewById(R.id.tvXmrToLogo);
|
tvXmrToLogo = view.findViewById(R.id.tvXmrToLogo);
|
||||||
|
|
||||||
tvAccount = view.findViewById(R.id.tvAccount);
|
tvAccount = view.findViewById(R.id.tvAccount);
|
||||||
|
@ -127,18 +131,20 @@ public class TxFragment extends Fragment {
|
||||||
tvWarning = view.findViewById(R.id.tvWarning);
|
tvWarning = view.findViewById(R.id.tvWarning);
|
||||||
|
|
||||||
tvTxXmrToKey.setOnClickListener(v -> {
|
tvTxXmrToKey.setOnClickListener(v -> {
|
||||||
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
|
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
|
||||||
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
info = getArguments().getParcelable(ARG_INFO);
|
final Bundle args = getArguments();
|
||||||
|
assert args != null;
|
||||||
|
info = args.getParcelable(ARG_INFO);
|
||||||
show();
|
show();
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
void shareTxInfo() {
|
void shareTxInfo() {
|
||||||
if (this.info == null) return;
|
if (this.info == null) return;
|
||||||
StringBuffer sb = new StringBuffer();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
sb.append(getString(R.string.tx_timestamp)).append(":\n");
|
sb.append(getString(R.string.tx_timestamp)).append(":\n");
|
||||||
sb.append(TS_FORMATTER.format(new Date(info.timestamp * 1000))).append("\n\n");
|
sb.append(TS_FORMATTER.format(new Date(info.timestamp * 1000))).append("\n\n");
|
||||||
|
@ -216,7 +222,7 @@ public class TxFragment extends Fragment {
|
||||||
|
|
||||||
private void showSubaddressLabel() {
|
private void showSubaddressLabel() {
|
||||||
final Subaddress subaddress = activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex);
|
final Subaddress subaddress = activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex);
|
||||||
final Context ctx = getContext();
|
final Context ctx = requireContext();
|
||||||
Spanned label = Html.fromHtml(ctx.getString(R.string.tx_account_formatted,
|
Spanned label = Html.fromHtml(ctx.getString(R.string.tx_account_formatted,
|
||||||
info.accountIndex, info.addressIndex,
|
info.accountIndex, info.addressIndex,
|
||||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
|
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
|
||||||
|
@ -264,16 +270,17 @@ public class TxFragment extends Fragment {
|
||||||
tvTxFee.setVisibility(View.GONE);
|
tvTxFee.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Context ctx = requireContext();
|
||||||
if (info.isFailed) {
|
if (info.isFailed) {
|
||||||
tvTxAmount.setText(getString(R.string.tx_list_amount_failed, Wallet.getDisplayAmount(info.amount)));
|
tvTxAmount.setText(getString(R.string.tx_list_amount_failed, Wallet.getDisplayAmount(info.amount)));
|
||||||
tvTxFee.setText(getString(R.string.tx_list_failed_text));
|
tvTxFee.setText(getString(R.string.tx_list_failed_text));
|
||||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
|
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.neutralColor));
|
||||||
} else if (info.isPending) {
|
} else if (info.isPending) {
|
||||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
|
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.neutralColor));
|
||||||
} else if (info.direction == TransactionInfo.Direction.Direction_In) {
|
} else if (info.direction == TransactionInfo.Direction.Direction_In) {
|
||||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor));
|
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor));
|
||||||
} else {
|
} else {
|
||||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.negativeColor));
|
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.negativeColor));
|
||||||
}
|
}
|
||||||
Set<String> destinations = new HashSet<>();
|
Set<String> destinations = new HashSet<>();
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
@ -328,31 +335,29 @@ public class TxFragment extends Fragment {
|
||||||
void showBtcInfo() {
|
void showBtcInfo() {
|
||||||
if (userNotes.xmrtoKey != null) {
|
if (userNotes.xmrtoKey != null) {
|
||||||
cvXmrTo.setVisibility(View.VISIBLE);
|
cvXmrTo.setVisibility(View.VISIBLE);
|
||||||
String key = userNotes.xmrtoKey;
|
|
||||||
if ("xmrto".equals(userNotes.xmrtoTag)) { // legacy xmr.to service :(
|
ShiftService service = ShiftService.findWithTag(userNotes.xmrtoTag);
|
||||||
key = "xmrto-" + key;
|
tvXmrToKeyLabel.setText(getString(R.string.label_send_btc_xmrto_key_lb, service.getLabel()));
|
||||||
}
|
if (service.getIconId() == 0)
|
||||||
tvTxXmrToKey.setText(key);
|
tvXmrToLogo.setVisibility(View.GONE);
|
||||||
|
else
|
||||||
|
tvXmrToLogo.setImageResource(service.getLogoId());
|
||||||
|
|
||||||
|
tvTxXmrToKey.setText(userNotes.xmrtoKey);
|
||||||
|
|
||||||
tvDestinationBtc.setText(userNotes.xmrtoDestination);
|
tvDestinationBtc.setText(userNotes.xmrtoDestination);
|
||||||
tvTxAmountBtc.setText(userNotes.xmrtoAmount + " " + userNotes.xmrtoCurrency);
|
tvTxAmountBtc.setText(userNotes.xmrtoAmount + " " + userNotes.xmrtoCurrency);
|
||||||
switch (userNotes.xmrtoTag) {
|
|
||||||
case "xmrto":
|
ShiftApi shiftApi = service.getShiftApi();
|
||||||
tvXmrToSupport.setVisibility(View.GONE);
|
if (shiftApi != null) {
|
||||||
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
|
tvXmrToSupport.setText(getString(R.string.label_send_btc_xmrto_info, service.getLabel()));
|
||||||
tvXmrToLogo.setImageResource(R.drawable.ic_xmrto_logo);
|
|
||||||
break;
|
|
||||||
case "side": // defaults in layout - just add underline
|
|
||||||
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
||||||
tvXmrToSupport.setOnClickListener(v -> {
|
tvXmrToSupport.setOnClickListener(v -> {
|
||||||
Uri uri = Uri.parse("https://sideshift.ai/orders/" + userNotes.xmrtoKey);
|
startActivity(new Intent(Intent.ACTION_VIEW, shiftApi.getQueryOrderUri(userNotes.xmrtoKey)));
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
|
||||||
startActivity(intent);
|
|
||||||
});
|
});
|
||||||
break;
|
} else {
|
||||||
default:
|
|
||||||
tvXmrToSupport.setVisibility(View.GONE);
|
tvXmrToSupport.setVisibility(View.GONE);
|
||||||
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
|
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
|
||||||
tvXmrToLogo.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cvXmrTo.setVisibility(View.GONE);
|
cvXmrTo.setVisibility(View.GONE);
|
||||||
|
@ -369,7 +374,7 @@ public class TxFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.tx_info_menu, menu);
|
inflater.inflate(R.menu.tx_info_menu, menu);
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
}
|
}
|
||||||
|
@ -397,7 +402,7 @@ public class TxFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
if (context instanceof TxFragment.Listener) {
|
if (context instanceof TxFragment.Listener) {
|
||||||
this.activityCallback = (TxFragment.Listener) context;
|
this.activityCallback = (TxFragment.Listener) context;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2017 m2049r
|
* Copyright (c) 2017-2024 m2049r
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -38,13 +38,13 @@ import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.core.view.GravityCompat;
|
import androidx.core.view.GravityCompat;
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
@ -92,6 +92,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
public static final String REQUEST_FINGERPRINT_USED = "fingerprint";
|
public static final String REQUEST_FINGERPRINT_USED = "fingerprint";
|
||||||
public static final String REQUEST_STREETMODE = "streetmode";
|
public static final String REQUEST_STREETMODE = "streetmode";
|
||||||
public static final String REQUEST_URI = "uri";
|
public static final String REQUEST_URI = "uri";
|
||||||
|
public static final String REQUEST_SIDEKICK = "sidekick";
|
||||||
|
|
||||||
private NavigationView accountsView;
|
private NavigationView accountsView;
|
||||||
private DrawerLayout drawer;
|
private DrawerLayout drawer;
|
||||||
|
@ -202,19 +203,9 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
return getWallet().getSubaddress(major, minor);
|
return getWallet().getSubaddress(major, minor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startWalletService() {
|
private void startWalletService(String walletId) {
|
||||||
Bundle extras = getIntent().getExtras();
|
|
||||||
if (extras != null) {
|
|
||||||
acquireWakeLock();
|
acquireWakeLock();
|
||||||
String walletId = extras.getString(REQUEST_ID);
|
|
||||||
// we can set the streetmode height AFTER opening the wallet
|
|
||||||
requestStreetMode = extras.getBoolean(REQUEST_STREETMODE);
|
|
||||||
password = extras.getString(REQUEST_PW);
|
|
||||||
uri = extras.getString(REQUEST_URI);
|
|
||||||
connectWalletService(walletId, password);
|
connectWalletService(walletId, password);
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopWalletService() {
|
private void stopWalletService() {
|
||||||
|
@ -330,7 +321,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void onWalletChangePassword() {
|
public void onWalletChangePassword() {
|
||||||
try {
|
try {
|
||||||
GenerateReviewFragment detailsFragment = (GenerateReviewFragment) getCurrentFragment();
|
GenerateReviewFragment detailsFragment = (GenerateReviewFragment) getCurrentFragment();
|
||||||
|
@ -356,14 +346,24 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bundle extras = getIntent().getExtras();
|
||||||
|
if (extras == null) {
|
||||||
|
finish(); // we need extras!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String walletId = extras.getString(REQUEST_ID);
|
||||||
|
requestStreetMode = extras.getBoolean(REQUEST_STREETMODE);
|
||||||
|
password = extras.getString(REQUEST_PW);
|
||||||
|
uri = extras.getString(REQUEST_URI);
|
||||||
|
boolean sidekick = extras.getBoolean(REQUEST_SIDEKICK);
|
||||||
|
|
||||||
setContentView(R.layout.activity_wallet);
|
setContentView(R.layout.activity_wallet);
|
||||||
toolbar = findViewById(R.id.toolbar);
|
toolbar = findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||||
|
|
||||||
toolbar.setOnButtonListener(new Toolbar.OnButtonListener() {
|
toolbar.setOnButtonListener(type -> {
|
||||||
@Override
|
|
||||||
public void onButton(int type) {
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Toolbar.BUTTON_BACK:
|
case Toolbar.BUTTON_BACK:
|
||||||
onDisposeRequest();
|
onDisposeRequest();
|
||||||
|
@ -383,7 +383,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
default:
|
default:
|
||||||
Timber.e("Button " + type + "pressed - how can this be?");
|
Timber.e("Button " + type + "pressed - how can this be?");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
drawer = findViewById(R.id.drawer_layout);
|
drawer = findViewById(R.id.drawer_layout);
|
||||||
|
@ -398,11 +397,16 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
showNet();
|
showNet();
|
||||||
|
|
||||||
Fragment walletFragment = new WalletFragment();
|
Fragment walletFragment = new WalletFragment();
|
||||||
|
final FragmentTransaction tx =
|
||||||
getSupportFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.add(R.id.fragment_container, walletFragment, WalletFragment.class.getName()).commit();
|
.add(R.id.fragment_container, walletFragment, WalletFragment.class.getName());
|
||||||
Timber.d("fragment added");
|
if (sidekick) {
|
||||||
|
tx.add(R.id.fragment_bluetooth, new BluetoothFragment(BluetoothFragment.Mode.CLIENT), BluetoothFragment.class.getName());
|
||||||
|
}
|
||||||
|
tx.commit();
|
||||||
|
Timber.d("fragments added");
|
||||||
|
|
||||||
startWalletService();
|
if (!sidekick) startWalletService(walletId);
|
||||||
Timber.d("onCreate() done.");
|
Timber.d("onCreate() done.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -616,7 +620,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onWalletOpen(final Wallet.Device device) {
|
public void onWalletOpen(final Wallet.Device device) {
|
||||||
if (device == Wallet.Device.Device_Ledger) {
|
if (device == Wallet.Device.Ledger) {
|
||||||
runOnUiThread(() -> showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE));
|
runOnUiThread(() -> showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -766,7 +770,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
intent.putExtra(WalletService.REQUEST_CMD_TX_TAG, tag);
|
intent.putExtra(WalletService.REQUEST_CMD_TX_TAG, tag);
|
||||||
startService(intent);
|
startService(intent);
|
||||||
Timber.d("CREATE TX request sent");
|
Timber.d("CREATE TX request sent");
|
||||||
if (getWallet().getDeviceType() == Wallet.Device.Device_Ledger)
|
if (getWallet().getDeviceType() == Wallet.Device.Ledger)
|
||||||
showLedgerProgressDialog(LedgerProgressDialog.TYPE_SEND);
|
showLedgerProgressDialog(LedgerProgressDialog.TYPE_SEND);
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Service not bound");
|
Timber.e("Service not bound");
|
||||||
|
@ -1132,11 +1136,12 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
protected void onPreExecute() {
|
protected void onPreExecute() {
|
||||||
super.onPreExecute();
|
super.onPreExecute();
|
||||||
switch (getWallet().getDeviceType()) {
|
switch (getWallet().getDeviceType()) {
|
||||||
case Device_Ledger:
|
case Ledger:
|
||||||
showLedgerProgressDialog(LedgerProgressDialog.TYPE_ACCOUNT);
|
showLedgerProgressDialog(LedgerProgressDialog.TYPE_ACCOUNT);
|
||||||
dialogOpened = true;
|
dialogOpened = true;
|
||||||
break;
|
break;
|
||||||
case Device_Software:
|
case Software:
|
||||||
|
case Sidekick:
|
||||||
showProgressDialog(R.string.accounts_progress_new);
|
showProgressDialog(R.string.accounts_progress_new);
|
||||||
dialogOpened = true;
|
dialogOpened = true;
|
||||||
break;
|
break;
|
||||||
|
@ -1176,7 +1181,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSubaddressSelected(@Nullable final Subaddress subaddress) {
|
public void onSubaddressSelected(@NonNull final Subaddress subaddress) {
|
||||||
selectedSubaddressIndex = subaddress.getAddressIndex();
|
selectedSubaddressIndex = subaddress.getAddressIndex();
|
||||||
getOnBackPressedDispatcher().onBackPressed();
|
getOnBackPressedDispatcher().onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ public class WalletFragment extends Fragment
|
||||||
flExchange = view.findViewById(R.id.flExchange);
|
flExchange = view.findViewById(R.id.flExchange);
|
||||||
((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable().
|
((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable().
|
||||||
setColorFilter(
|
setColorFilter(
|
||||||
ThemeHelper.getThemedColor(getContext(), com.google.android.material.R.attr.colorPrimaryVariant),
|
ThemeHelper.getThemedColor(requireContext(), com.google.android.material.R.attr.colorPrimaryVariant),
|
||||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||||
|
|
||||||
tvProgress = view.findViewById(R.id.tvProgress);
|
tvProgress = view.findViewById(R.id.tvProgress);
|
||||||
|
|
|
@ -22,27 +22,19 @@ import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.OptIn;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentStateManagerControl;
|
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.model.NetworkType;
|
import com.m2049r.xmrwallet.model.NetworkType;
|
||||||
import com.m2049r.xmrwallet.util.LocaleHelper;
|
import com.m2049r.xmrwallet.util.LocaleHelper;
|
||||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||||
import com.m2049r.xmrwallet.util.NightmodeHelper;
|
import com.m2049r.xmrwallet.util.NightmodeHelper;
|
||||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class XmrWalletApplication extends Application {
|
public class XmrWalletApplication extends Application {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@OptIn(markerClass = FragmentStateManagerControl.class)
|
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
FragmentManager.enableNewStateManager(false);
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Timber.plant(new Timber.DebugTree());
|
Timber.plant(new Timber.DebugTree());
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,43 +24,38 @@ import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
@Getter
|
||||||
@ToString
|
@ToString
|
||||||
public class BarcodeData {
|
public class BarcodeData {
|
||||||
|
|
||||||
public enum Security {
|
public enum Security {
|
||||||
NORMAL,
|
NORMAL,
|
||||||
OA_NO_DNSSEC,
|
OA_NO_DNSSEC,
|
||||||
OA_DNSSEC
|
OA_DNSSEC
|
||||||
}
|
}
|
||||||
|
|
||||||
final public Crypto asset;
|
final private Set<Crypto> possibleAssets = new HashSet<>();
|
||||||
final public List<Crypto> ambiguousAssets;
|
private String address = null;
|
||||||
final public String address;
|
private String addressName = null;
|
||||||
final public String addressName;
|
private String amount = null;
|
||||||
final public String amount;
|
private String description = null;
|
||||||
final public String description;
|
private Security security = null;
|
||||||
final public Security security;
|
|
||||||
|
|
||||||
public BarcodeData(List<Crypto> assets, String address) {
|
public BarcodeData(List<Crypto> assets, String address) {
|
||||||
if (assets.isEmpty())
|
if (assets.isEmpty())
|
||||||
throw new IllegalArgumentException("no assets specified");
|
throw new IllegalArgumentException("no assets specified");
|
||||||
this.addressName = null;
|
security = Security.NORMAL;
|
||||||
this.description = null;
|
|
||||||
this.amount = null;
|
|
||||||
this.security = Security.NORMAL;
|
|
||||||
this.address = address;
|
this.address = address;
|
||||||
if (assets.size() == 1) {
|
possibleAssets.addAll(assets);
|
||||||
this.asset = assets.get(0);
|
|
||||||
this.ambiguousAssets = null;
|
|
||||||
} else {
|
|
||||||
this.asset = null;
|
|
||||||
this.ambiguousAssets = assets;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public BarcodeData(Crypto asset, String address, String description, String amount) {
|
public BarcodeData(Crypto asset, String address, String description, String amount) {
|
||||||
|
@ -68,8 +63,7 @@ public class BarcodeData {
|
||||||
}
|
}
|
||||||
|
|
||||||
public BarcodeData(Crypto asset, String address, String addressName, String description, String amount, Security security) {
|
public BarcodeData(Crypto asset, String address, String addressName, String description, String amount, Security security) {
|
||||||
this.ambiguousAssets = null;
|
possibleAssets.add(asset);
|
||||||
this.asset = asset;
|
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.addressName = addressName;
|
this.addressName = addressName;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
|
@ -82,7 +76,7 @@ public class BarcodeData {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUriString() {
|
public String getUriString() {
|
||||||
if (asset != Crypto.XMR) throw new IllegalStateException("We can only do XMR stuff!");
|
if (getAsset() != Crypto.XMR) throw new IllegalStateException("We can only do XMR stuff!");
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append(Crypto.XMR.getUriScheme())
|
sb.append(Crypto.XMR.getUriScheme())
|
||||||
.append(':')
|
.append(':')
|
||||||
|
@ -227,6 +221,20 @@ public class BarcodeData {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAmbiguous() {
|
public boolean isAmbiguous() {
|
||||||
return ambiguousAssets != null;
|
return possibleAssets.size() > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Crypto getAsset() {
|
||||||
|
if (possibleAssets.size() == 1) {
|
||||||
|
return possibleAssets.iterator().next();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return true if we still have possible assets
|
||||||
|
public boolean filter(Set<Crypto> assets) {
|
||||||
|
possibleAssets.retainAll(assets);
|
||||||
|
return !possibleAssets.isEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018 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 com.m2049r.xmrwallet.data;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class BluetoothInfo {
|
||||||
|
@Getter
|
||||||
|
final private String name;
|
||||||
|
@Getter
|
||||||
|
final private String address;
|
||||||
|
@Getter
|
||||||
|
private boolean bonded;
|
||||||
|
|
||||||
|
public BluetoothInfo(BluetoothDevice device) {
|
||||||
|
name = device.getName().trim();
|
||||||
|
address = device.getAddress();
|
||||||
|
bonded = device.getBondState() == BluetoothDevice.BOND_BONDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public Comparator<BluetoothInfo> NameComparator = (o1, o2) -> o1.name.compareTo(o2.name);
|
||||||
|
}
|
|
@ -1,3 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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 com.m2049r.xmrwallet.data;
|
package com.m2049r.xmrwallet.data;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -5,35 +21,30 @@ import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.R;
|
import com.m2049r.xmrwallet.R;
|
||||||
import com.m2049r.xmrwallet.model.Wallet;
|
import com.m2049r.xmrwallet.model.Wallet;
|
||||||
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
|
|
||||||
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
|
import java.util.regex.Pattern;
|
||||||
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public enum Crypto {
|
public enum Crypto {
|
||||||
XMR("XMR", true, "monero:tx_amount:recipient_name:tx_description", R.id.ibXMR, R.drawable.ic_monero, R.drawable.ic_monero_bw, Wallet::isAddressValid),
|
XMR("XMR", "XMR", "XMR", "monero:tx_amount:recipient_name:tx_description", R.id.ibXMR, R.drawable.ic_monero, R.drawable.ic_monero_bw, Pattern.compile("^[48][a-zA-Z|\\d]{94}([a-zA-Z|\\d]{11})?$")),
|
||||||
BTC("BTC", true, "bitcoin:amount:label:message", R.id.ibBTC, R.drawable.ic_xmrto_btc, R.drawable.ic_xmrto_btc_off, address -> {
|
BTC("BTC", "BTC", "BTC", "bitcoin:amount:label:message", R.id.ibBTC, R.drawable.ic_xmrto_btc, R.drawable.ic_xmrto_btc_off, Pattern.compile("^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$|^(bc1q)|(bc1p)[0-9A-Za-z]{37,62}$")),
|
||||||
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC);
|
LTC("LTC", "LTC", "LTC", "litecoin:amount:label:message", R.id.ibLTC, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, Pattern.compile("^([LM3])[A-Za-z0-9]{33}$|^(ltc1)[0-9A-Za-z]{39}$")),
|
||||||
}),
|
ETH("ETH", "ETH", "ETH", "ethereum:amount:label:message", R.id.ibETH, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, Pattern.compile("^(0x)[0-9A-Fa-f]{40}$")),
|
||||||
DASH("DASH", true, "dash:amount:label:message", R.id.ibDASH, R.drawable.ic_xmrto_dash, R.drawable.ic_xmrto_dash_off, address -> {
|
USDT("USDT", "TRX", "USDT(TRC20)", "usdt:amount:label:message", R.id.ibUSDT, R.drawable.ic_xmrto_usdt_trc20, R.drawable.ic_xmrto_usdt_trc20_off, Pattern.compile("^T[1-9A-HJ-NP-Za-km-z]{33}$")),
|
||||||
return BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
|
SOLANA("SOL", "SOL", "SOL", "solana:amount:label:message", R.id.ibSOL, R.drawable.ic_xmrto_sol, R.drawable.ic_xmrto_sol_off, Pattern.compile("^[1-9A-HJ-NP-Za-km-z]{32,44}$"));
|
||||||
}),
|
|
||||||
DOGE("DOGE", true, "dogecoin:amount:label:message", R.id.ibDOGE, R.drawable.ic_xmrto_doge, R.drawable.ic_xmrto_doge_off, address -> {
|
|
||||||
return BitcoinAddressValidator.validate(address, BitcoinAddressType.DOGE);
|
|
||||||
}),
|
|
||||||
ETH("ETH", false, "ethereum:amount:label:message", R.id.ibETH, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, EthAddressValidator::validate),
|
|
||||||
LTC("LTC", true, "litecoin:amount:label:message", R.id.ibLTC, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, address -> {
|
|
||||||
return BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC);
|
|
||||||
});
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String symbol;
|
private final String symbol;
|
||||||
@Getter
|
@Getter
|
||||||
private final boolean casefull;
|
@NonNull
|
||||||
|
private final String network;
|
||||||
|
@Getter
|
||||||
|
@NonNull
|
||||||
|
private final String label;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String uriSpec;
|
private final String uriSpec;
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -43,7 +54,7 @@ public enum Crypto {
|
||||||
@Getter
|
@Getter
|
||||||
private final int iconDisabledId;
|
private final int iconDisabledId;
|
||||||
@NonNull
|
@NonNull
|
||||||
private final Validator validator;
|
private final Pattern regex;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static Crypto withScheme(@NonNull String scheme) {
|
public static Crypto withScheme(@NonNull String scheme) {
|
||||||
|
@ -62,10 +73,6 @@ public enum Crypto {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Validator {
|
|
||||||
boolean validate(String address);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO maybe cache these segments
|
// TODO maybe cache these segments
|
||||||
String getUriScheme() {
|
String getUriScheme() {
|
||||||
return uriSpec.split(":")[0];
|
return uriSpec.split(":")[0];
|
||||||
|
@ -83,7 +90,8 @@ public enum Crypto {
|
||||||
return uriSpec.split(":")[3];
|
return uriSpec.split(":")[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean validate(String address) {
|
public boolean validate(String address) {
|
||||||
return validator.validate(address);
|
if (this == XMR) return Wallet.isAddressValid(address);
|
||||||
|
return regex.matcher(address).find();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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 com.m2049r.xmrwallet.data;
|
||||||
|
|
||||||
|
import lombok.Value;
|
||||||
|
|
||||||
|
@Value
|
||||||
|
public class CryptoAmount {
|
||||||
|
Crypto crypto;
|
||||||
|
double amount;
|
||||||
|
|
||||||
|
public CryptoAmount newWithAmount(double amount) {
|
||||||
|
return new CryptoAmount(this.crypto, amount);
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,23 +19,26 @@ package com.m2049r.xmrwallet.data;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
// Nodes stolen from https://moneroworld.com/#nodes
|
@Getter
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public enum DefaultNodes {
|
public enum DefaultNodes {
|
||||||
MONERUJO("nodex.monerujo.io:18081"),
|
AGORIST("xmr.agor.ist:18089/mainnet/agor.ist"),
|
||||||
XMRTO("node.xmr.to:18081"),
|
BOLDSUCK("xmr-de.boldsuck.org:18080/mainnet/boldsuck.org"),
|
||||||
SUPPORTXMR("node.supportxmr.com:18081"),
|
|
||||||
HASHVAULT("nodes.hashvault.pro:18081"),
|
|
||||||
MONEROWORLD("node.moneroworld.com:18089"),
|
|
||||||
XMRTW("opennode.xmr-tw.org:18089"),
|
|
||||||
ds_jetzt("monero.ds-jetzt.de: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"),
|
boldsuck("6dsdenp6vjkvqzy4wzsnzn6wixkdzihx3khiumyzieauxuxslmcaeiad.onion:18081/mainnet/boldsuck.onion"),
|
||||||
ds_jetzt_onion("qvlr4w7yhnjrdg3txa72jwtpnjn4ezsrivzvocbnvpfbdo342fahhoad.onion:18089/mainnet/ds-jetzt.onion");
|
CAKE("xmr-node.cakewallet.com:18081/mainnet/cakewallet.com"),
|
||||||
|
DS_JETZT("monero.ds-jetzt.de:18089/mainnet/ds-jetzt.de"),
|
||||||
|
ds_jetzt("qvlr4w7yhnjrdg3txa72jwtpnjn4ezsrivzvocbnvpfbdo342fahhoad.onion:18089/mainnet/ds-jetzt.onion"),
|
||||||
|
MONERODEVS("node.monerodevs.org:18089/mainnet/monerodevs.org"),
|
||||||
|
MONERUJO("nodex.monerujo.io:18081/mainnet/monerujo.io"),
|
||||||
|
monerujo("monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion:18081/mainnet/monerujo.onion"),
|
||||||
|
SETH("node.sethforprivacy.com:18089/mainnet/sethforprivacy.com"),
|
||||||
|
seth("sfpp2p7wnfjv3lrvfan4jmmkvhnbsbimpa3cqyuf7nt6zd24xhcqcsyd.onion/mainnet/sethforprivacy.onion"),
|
||||||
|
STACK("monero.stackwallet.com:18081/mainnet/stackwallet.com"),
|
||||||
|
STORMYCLOUD("xmr.stormycloud.org:18089/mainnet/stormycloud.org"),
|
||||||
|
TENZ("monero.10z.com.ar:18089/mainnet/10z.com.ar"),
|
||||||
|
XMRROCKS("node.xmr.rocks:18089/mainnet/xmr.rocks"),
|
||||||
|
xmrrocks("xqnnz2xmlmtpy2p4cm4cphg2elkwu5oob7b7so5v4wwgt44p6vbx5ryd.onion/mainnet/xmr.rocks.onion"),
|
||||||
|
XMRTW("opennode.xmr-tw.org:18089/mainnet/xmr-tw.org");
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final String uri;
|
private final String uri;
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,7 +144,7 @@ public class Node {
|
||||||
if ((nodeString == null) || nodeString.isEmpty())
|
if ((nodeString == null) || nodeString.isEmpty())
|
||||||
throw new IllegalArgumentException("daemon is empty");
|
throw new IllegalArgumentException("daemon is empty");
|
||||||
String daemonAddress;
|
String daemonAddress;
|
||||||
String a[] = nodeString.split("@");
|
String[] a = nodeString.split("@");
|
||||||
if (a.length == 1) { // no credentials
|
if (a.length == 1) { // no credentials
|
||||||
daemonAddress = a[0];
|
daemonAddress = a[0];
|
||||||
username = "";
|
username = "";
|
||||||
|
@ -169,7 +169,7 @@ public class Node {
|
||||||
throw new IllegalArgumentException("Too many '/' or too few");
|
throw new IllegalArgumentException("Too many '/' or too few");
|
||||||
|
|
||||||
daemonAddress = daParts[0];
|
daemonAddress = daParts[0];
|
||||||
String da[] = daemonAddress.split(":");
|
String[] da = daemonAddress.split(":");
|
||||||
if ((da.length > 2) || (da.length < 1))
|
if ((da.length > 2) || (da.length < 1))
|
||||||
throw new IllegalArgumentException("Too many ':' or too few");
|
throw new IllegalArgumentException("Too many ':' or too few");
|
||||||
String host = da[0];
|
String host = da[0];
|
||||||
|
|
|
@ -201,8 +201,13 @@ public class NodeInfo extends Node {
|
||||||
.port(port)
|
.port(port)
|
||||||
.addPathSegment("json_rpc")
|
.addPathSegment("json_rpc")
|
||||||
.build();
|
.build();
|
||||||
final String json = "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}";
|
|
||||||
|
try {
|
||||||
|
final JSONObject json = new JSONObject("{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}");
|
||||||
return new Request(url, json, getUsername(), getPassword());
|
return new Request(url, json, getUsername(), getPassword());
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean testRpcService(int port) {
|
private boolean testRpcService(int port) {
|
||||||
|
|
|
@ -20,22 +20,21 @@ import android.os.Parcel;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
public class TxDataBtc extends TxData {
|
public class TxDataBtc extends TxData {
|
||||||
@Getter
|
private ShiftService shiftService;
|
||||||
@Setter
|
|
||||||
private String btcSymbol; // the actual non-XMR thing we're sending
|
private String btcSymbol; // the actual non-XMR thing we're sending
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private String xmrtoOrderId; // shown in success screen
|
private String xmrtoOrderId; // shown in success screen
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private String btcAddress;
|
private String btcAddress;
|
||||||
@Getter
|
private CryptoAmount shiftAmount; // what we want to send
|
||||||
@Setter
|
private String xmrtoQueryOrderToken; // used for queryOrder API
|
||||||
private double btcAmount;
|
|
||||||
|
|
||||||
public TxDataBtc() {
|
public TxDataBtc() {
|
||||||
super();
|
super();
|
||||||
|
@ -47,7 +46,9 @@ public class TxDataBtc extends TxData {
|
||||||
out.writeString(btcSymbol);
|
out.writeString(btcSymbol);
|
||||||
out.writeString(xmrtoOrderId);
|
out.writeString(xmrtoOrderId);
|
||||||
out.writeString(btcAddress);
|
out.writeString(btcAddress);
|
||||||
out.writeDouble(btcAmount);
|
out.writeString(shiftAmount.getCrypto().name());
|
||||||
|
out.writeDouble(shiftAmount.getAmount());
|
||||||
|
out.writeString(xmrtoQueryOrderToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
|
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
|
||||||
|
@ -66,7 +67,8 @@ public class TxDataBtc extends TxData {
|
||||||
btcSymbol = in.readString();
|
btcSymbol = in.readString();
|
||||||
xmrtoOrderId = in.readString();
|
xmrtoOrderId = in.readString();
|
||||||
btcAddress = in.readString();
|
btcAddress = in.readString();
|
||||||
btcAmount = in.readDouble();
|
shiftAmount = new CryptoAmount(Crypto.valueOf(in.readString()), in.readDouble());
|
||||||
|
xmrtoQueryOrderToken = in.readString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -79,19 +81,33 @@ public class TxDataBtc extends TxData {
|
||||||
sb.append(btcSymbol);
|
sb.append(btcSymbol);
|
||||||
sb.append(",btcAddress:");
|
sb.append(",btcAddress:");
|
||||||
sb.append(btcAddress);
|
sb.append(btcAddress);
|
||||||
sb.append(",btcAmount:");
|
sb.append(",amount:");
|
||||||
sb.append(btcAmount);
|
sb.append(shiftAmount);
|
||||||
|
sb.append(",xmrtoQueryOrderToken:");
|
||||||
|
sb.append(xmrtoQueryOrderToken);
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateAddress(@NonNull String address) {
|
public boolean validateAddress(@NonNull String address) {
|
||||||
if ((btcSymbol == null) || (btcAddress == null)) return false;
|
|
||||||
final Crypto crypto = Crypto.withSymbol(btcSymbol);
|
final Crypto crypto = Crypto.withSymbol(btcSymbol);
|
||||||
if (crypto == null) return false;
|
if (crypto == null) return false;
|
||||||
if (crypto.isCasefull()) { // compare as-is
|
return address.equalsIgnoreCase(btcAddress);
|
||||||
return address.equals(btcAddress);
|
}
|
||||||
} else { // normalize & compare (e.g. ETH with and without checksum capitals
|
|
||||||
return address.toLowerCase().equals(btcAddress.toLowerCase());
|
public double getBtcAmount() {
|
||||||
|
return (shiftAmount.getCrypto() == Crypto.XMR) ? 0 : shiftAmount.getAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getXmrAmount() {
|
||||||
|
return (shiftAmount.getCrypto() == Crypto.XMR) ? shiftAmount.getAmount() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validate(RequestQuote quote) {
|
||||||
|
if (shiftAmount.getCrypto() == Crypto.XMR) {
|
||||||
|
return (quote.getXmrAmount() == getXmrAmount());
|
||||||
|
} else {
|
||||||
|
return (quote.getBtcAmount() == getBtcAmount());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.data;
|
package com.m2049r.xmrwallet.data;
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
|
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
@ -61,7 +61,7 @@ public class UserNotes {
|
||||||
|
|
||||||
public void setXmrtoOrder(CreateOrder order) {
|
public void setXmrtoOrder(CreateOrder order) {
|
||||||
if (order != null) {
|
if (order != null) {
|
||||||
xmrtoTag = order.TAG;
|
xmrtoTag = order.getTag();
|
||||||
xmrtoKey = order.getOrderId();
|
xmrtoKey = order.getOrderId();
|
||||||
xmrtoAmount = Helper.getDisplayAmount(order.getBtcAmount());
|
xmrtoAmount = Helper.getDisplayAmount(order.getBtcAmount());
|
||||||
xmrtoCurrency = order.getBtcCurrency();
|
xmrtoCurrency = order.getBtcCurrency();
|
||||||
|
|
|
@ -20,10 +20,10 @@ import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
@ -57,13 +57,14 @@ public class AboutFragment extends DialogFragment {
|
||||||
AboutFragment.newInstance().show(ft, TAG);
|
AboutFragment.newInstance().show(ft, TAG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_about, null);
|
final View view = getLayoutInflater().inflate(R.layout.fragment_about, null);
|
||||||
((TextView) view.findViewById(R.id.tvHelp)).setText(Html.fromHtml(getLicencesHtml()));
|
((TextView) view.findViewById(R.id.tvHelp)).setText(Html.fromHtml(getLicencesHtml()));
|
||||||
((TextView) view.findViewById(R.id.tvVersion)).setText(getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
((TextView) view.findViewById(R.id.tvVersion)).setText(getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||||
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setNegativeButton(R.string.about_close,
|
.setNegativeButton(R.string.about_close,
|
||||||
new DialogInterface.OnClickListener() {
|
new DialogInterface.OnClickListener() {
|
||||||
|
@ -77,7 +78,7 @@ public class AboutFragment extends DialogFragment {
|
||||||
|
|
||||||
private String getLicencesHtml() {
|
private String getLicencesHtml() {
|
||||||
try (BufferedReader reader = new BufferedReader(
|
try (BufferedReader reader = new BufferedReader(
|
||||||
new InputStreamReader(getContext().getAssets().open("licenses.html"), StandardCharsets.UTF_8))) {
|
new InputStreamReader(requireContext().getAssets().open("licenses.html"), StandardCharsets.UTF_8))) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null)
|
while ((line = reader.readLine()) != null)
|
||||||
|
|
|
@ -20,10 +20,10 @@ import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
@ -49,13 +49,14 @@ public class CreditsFragment extends DialogFragment {
|
||||||
CreditsFragment.newInstance().show(ft, TAG);
|
CreditsFragment.newInstance().show(ft, TAG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_credits, null);
|
final View view = getLayoutInflater().inflate(R.layout.fragment_credits, null);
|
||||||
|
|
||||||
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.credits_text)));
|
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.credits_text)));
|
||||||
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setNegativeButton(R.string.about_close,
|
.setNegativeButton(R.string.about_close,
|
||||||
new DialogInterface.OnClickListener() {
|
new DialogInterface.OnClickListener() {
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.dialog;
|
package com.m2049r.xmrwallet.dialog;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@ -65,9 +65,10 @@ public class HelpFragment extends DialogFragment {
|
||||||
|
|
||||||
private Spanned getHtml(String html, double textSize) {
|
private Spanned getHtml(String html, double textSize) {
|
||||||
final Html.ImageGetter imageGetter = source -> {
|
final Html.ImageGetter imageGetter = source -> {
|
||||||
final int imageId = getResources().getIdentifier(source.replace("/", ""), "drawable", requireActivity().getPackageName());
|
@SuppressLint("DiscouragedApi") final int imageId = getResources().getIdentifier(source.replace("/", ""), "drawable", requireActivity().getPackageName());
|
||||||
// Don't die if we don't find the image - use a heart instead
|
// 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 Drawable drawable = ContextCompat.getDrawable(requireActivity(), imageId > 0 ? imageId : R.drawable.ic_favorite_24dp);
|
||||||
|
assert drawable != null;
|
||||||
final double f = textSize / drawable.getIntrinsicHeight();
|
final double f = textSize / drawable.getIntrinsicHeight();
|
||||||
drawable.setBounds(0, 0, (int) (f * drawable.getIntrinsicWidth()), (int) textSize);
|
drawable.setBounds(0, 0, (int) (f * drawable.getIntrinsicWidth()), (int) textSize);
|
||||||
return drawable;
|
return drawable;
|
||||||
|
@ -82,7 +83,7 @@ public class HelpFragment extends DialogFragment {
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_help, null);
|
final View view = getLayoutInflater().inflate(R.layout.fragment_help, null);
|
||||||
|
|
||||||
int helpId = 0;
|
int helpId = 0;
|
||||||
boolean torButton = false;
|
boolean torButton = false;
|
||||||
|
|
|
@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.dialog;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@ -66,7 +65,7 @@ public class PocketChangeFragment extends DialogFragment implements Slider.OnCha
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pocketchange_setting, null);
|
final View view = getLayoutInflater().inflate(R.layout.fragment_pocketchange_setting, null);
|
||||||
boolean enabled = false;
|
boolean enabled = false;
|
||||||
int progress = 0;
|
int progress = 0;
|
||||||
Bundle arguments = getArguments();
|
Bundle arguments = getArguments();
|
||||||
|
|
|
@ -20,10 +20,10 @@ import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
@ -49,13 +49,14 @@ public class PrivacyFragment extends DialogFragment {
|
||||||
PrivacyFragment.newInstance().show(ft, TAG);
|
PrivacyFragment.newInstance().show(ft, TAG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_privacy_policy, null);
|
final View view = getLayoutInflater().inflate(R.layout.fragment_privacy_policy, null);
|
||||||
|
|
||||||
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.privacy_policy)));
|
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.privacy_policy)));
|
||||||
|
|
||||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setNegativeButton(R.string.about_close,
|
.setNegativeButton(R.string.about_close,
|
||||||
new DialogInterface.OnClickListener() {
|
new DialogInterface.OnClickListener() {
|
||||||
|
|
|
@ -17,6 +17,7 @@ package com.m2049r.xmrwallet.dialog;
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
@ -31,6 +32,7 @@ import com.m2049r.xmrwallet.R;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
@ -56,7 +58,7 @@ public class ProgressDialog extends AlertDialog {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ledger_progress, null);
|
@SuppressLint("InflateParams") final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ledger_progress, null);
|
||||||
pbCircle = view.findViewById(R.id.pbCircle);
|
pbCircle = view.findViewById(R.id.pbCircle);
|
||||||
tvMessage = view.findViewById(R.id.tvMessage);
|
tvMessage = view.findViewById(R.id.tvMessage);
|
||||||
rlProgressBar = view.findViewById(R.id.rlProgressBar);
|
rlProgressBar = view.findViewById(R.id.rlProgressBar);
|
||||||
|
@ -78,7 +80,7 @@ public class ProgressDialog extends AlertDialog {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if (Helper.preventScreenshot()) {
|
if (Helper.preventScreenshot()) {
|
||||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
Objects.requireNonNull(getWindow()).setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.m2049r.xmrwallet.fragment.send;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.data.CryptoAmount;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
|
||||||
|
|
||||||
|
public interface PreShifter {
|
||||||
|
CryptoAmount getAmount();
|
||||||
|
|
||||||
|
void onOrderParametersError(final Exception ex);
|
||||||
|
|
||||||
|
void onOrderParametersReceived(final QueryOrderParameters orderParameters);
|
||||||
|
|
||||||
|
boolean isActive();
|
||||||
|
|
||||||
|
void showProgress();
|
||||||
|
}
|
|
@ -44,12 +44,9 @@ import com.m2049r.xmrwallet.data.TxDataBtc;
|
||||||
import com.m2049r.xmrwallet.data.UserNotes;
|
import com.m2049r.xmrwallet.data.UserNotes;
|
||||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||||
import com.m2049r.xmrwallet.model.Wallet;
|
import com.m2049r.xmrwallet.model.Wallet;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
import com.m2049r.xmrwallet.util.OpenAliasHelper;
|
import com.m2049r.xmrwallet.util.OpenAliasHelper;
|
||||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
|
||||||
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
|
|
||||||
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
|
|
||||||
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -64,15 +61,11 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
public static SendAddressWizardFragment newInstance(Listener listener) {
|
public static SendAddressWizardFragment newInstance(Listener listener) {
|
||||||
SendAddressWizardFragment instance = new SendAddressWizardFragment();
|
SendAddressWizardFragment instance = new SendAddressWizardFragment();
|
||||||
instance.setSendListener(listener);
|
instance.sendListener = listener;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Listener sendListener;
|
private Listener sendListener;
|
||||||
|
|
||||||
public void setSendListener(Listener listener) {
|
|
||||||
this.sendListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
void setBarcodeData(BarcodeData data);
|
void setBarcodeData(BarcodeData data);
|
||||||
|
@ -103,13 +96,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
void onScan();
|
void onScan();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Crypto getCryptoForButton(ImageButton button) {
|
|
||||||
for (Map.Entry<Crypto, ImageButton> entry : ibCrypto.entrySet()) {
|
|
||||||
if (entry.getValue() == button) return entry.getKey();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
|
@ -122,7 +108,9 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
ibCrypto = new HashMap<>();
|
ibCrypto = new HashMap<>();
|
||||||
for (Crypto crypto : Crypto.values()) {
|
for (Crypto crypto : Crypto.values()) {
|
||||||
final ImageButton button = view.findViewById(crypto.getButtonId());
|
final ImageButton button = view.findViewById(crypto.getButtonId());
|
||||||
if (Helper.ALLOW_SHIFT || (crypto == Crypto.XMR)) {
|
if ((crypto == Crypto.XMR)
|
||||||
|
|| (Helper.ALLOW_SHIFT && ShiftService.isAssetSupported(crypto))) {
|
||||||
|
button.setVisibility(View.VISIBLE);
|
||||||
ibCrypto.put(crypto, button);
|
ibCrypto.put(crypto, button);
|
||||||
button.setOnClickListener(v -> {
|
button.setOnClickListener(v -> {
|
||||||
if (possibleCryptos.contains(crypto)) {
|
if (possibleCryptos.contains(crypto)) {
|
||||||
|
@ -131,9 +119,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
} else {
|
} else {
|
||||||
// show help what to do:
|
// show help what to do:
|
||||||
if (button.getId() != R.id.ibXMR) {
|
if (button.getId() != R.id.ibXMR) {
|
||||||
final String name = getResources().getStringArray(R.array.cryptos)[crypto.ordinal()];
|
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, crypto.getNetwork(), crypto.getLabel(), ShiftService.DEFAULT.getLabel())));
|
||||||
final String symbol = getCryptoForButton(button).getSymbol();
|
|
||||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, name, symbol)));
|
|
||||||
tvXmrTo.setVisibility(View.VISIBLE);
|
tvXmrTo.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
|
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
|
||||||
|
@ -143,9 +129,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
button.setImageResource(crypto.getIconDisabledId());
|
button.setVisibility(View.INVISIBLE);
|
||||||
button.setImageAlpha(128);
|
|
||||||
button.setEnabled(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!Helper.ALLOW_SHIFT) {
|
if (!Helper.ALLOW_SHIFT) {
|
||||||
|
@ -175,49 +159,26 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable editable) {
|
public void afterTextChanged(Editable editable) {
|
||||||
Timber.d("AFTER: %s", editable.toString());
|
Timber.d("AFTER: %s", editable.toString());
|
||||||
etAddress.setError(null);
|
BarcodeData bc = sendListener.getBarcodeData();
|
||||||
|
if (bc == null) {
|
||||||
|
final String address = etAddress.getEditText().getText().toString();
|
||||||
|
bc = BarcodeData.fromString(address);
|
||||||
|
}
|
||||||
|
sendListener.setBarcodeData(null); // it's used up now
|
||||||
possibleCryptos.clear();
|
possibleCryptos.clear();
|
||||||
selectedCrypto = null;
|
selectedCrypto = null;
|
||||||
final String address = etAddress.getEditText().getText().toString();
|
if ((bc != null) && (bc.filter(ShiftService.getPossibleAssets()))) {
|
||||||
if (isIntegratedAddress(address)) {
|
possibleCryptos.clear();
|
||||||
Timber.d("isIntegratedAddress");
|
possibleCryptos.addAll(bc.getPossibleAssets());
|
||||||
possibleCryptos.add(Crypto.XMR);
|
selectedCrypto = bc.getAsset();
|
||||||
selectedCrypto = Crypto.XMR;
|
if (checkAddress()) {
|
||||||
etAddress.setError(getString(R.string.info_paymentid_integrated));
|
if (bc.getSecurity() == BarcodeData.Security.OA_NO_DNSSEC)
|
||||||
sendListener.setMode(SendFragment.Mode.XMR);
|
etAddress.setError(getString(R.string.send_address_no_dnssec));
|
||||||
} else if (isStandardAddress(address)) {
|
else if (bc.getSecurity() == BarcodeData.Security.OA_DNSSEC)
|
||||||
Timber.d("isStandardAddress");
|
etAddress.setError(getString(R.string.send_address_openalias));
|
||||||
possibleCryptos.add(Crypto.XMR);
|
|
||||||
selectedCrypto = Crypto.XMR;
|
|
||||||
sendListener.setMode(SendFragment.Mode.XMR);
|
|
||||||
}
|
|
||||||
if (!Helper.ALLOW_SHIFT) return;
|
|
||||||
if ((selectedCrypto == null) && isEthAddress(address)) {
|
|
||||||
Timber.d("isEthAddress");
|
|
||||||
possibleCryptos.add(Crypto.ETH);
|
|
||||||
selectedCrypto = Crypto.ETH;
|
|
||||||
tvXmrTo.setVisibility(View.VISIBLE);
|
|
||||||
sendListener.setMode(SendFragment.Mode.BTC);
|
|
||||||
}
|
|
||||||
if (possibleCryptos.isEmpty()) {
|
|
||||||
Timber.d("isBitcoinAddress");
|
|
||||||
for (BitcoinAddressType type : BitcoinAddressType.values()) {
|
|
||||||
if (BitcoinAddressValidator.validate(address, type)) {
|
|
||||||
possibleCryptos.add(Crypto.valueOf(type.name()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!possibleCryptos.isEmpty()) // found something in need of shifting!
|
updateCryptoButtons(false);
|
||||||
sendListener.setMode(SendFragment.Mode.BTC);
|
|
||||||
if (possibleCryptos.size() == 1) {
|
|
||||||
selectedCrypto = (Crypto) possibleCryptos.toArray()[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (possibleCryptos.isEmpty()) {
|
|
||||||
Timber.d("other");
|
|
||||||
tvXmrTo.setVisibility(View.INVISIBLE);
|
|
||||||
sendListener.setMode(SendFragment.Mode.XMR);
|
|
||||||
}
|
|
||||||
updateCryptoButtons(address.isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -231,7 +192,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
final ImageButton bPasteAddress = view.findViewById(R.id.bPasteAddress);
|
final ImageButton bPasteAddress = view.findViewById(R.id.bPasteAddress);
|
||||||
bPasteAddress.setOnClickListener(v -> {
|
bPasteAddress.setOnClickListener(v -> {
|
||||||
final String clip = Helper.getClipBoardText(getActivity());
|
final String clip = Helper.getClipBoardText(requireActivity());
|
||||||
if (clip == null) return;
|
if (clip == null) return;
|
||||||
// clean it up
|
// clean it up
|
||||||
final String address = clip.replaceAll("( +)|(\\r?\\n?)", "");
|
final String address = clip.replaceAll("( +)|(\\r?\\n?)", "");
|
||||||
|
@ -248,9 +209,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
etNotes = view.findViewById(R.id.etNotes);
|
etNotes = view.findViewById(R.id.etNotes);
|
||||||
etNotes.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT);
|
etNotes.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT);
|
||||||
etNotes.getEditText().
|
etNotes.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
|
||||||
setOnEditorActionListener((v, actionId, event) -> {
|
|
||||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||||
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||||
etDummy.requestFocus();
|
etDummy.requestFocus();
|
||||||
|
@ -271,13 +230,19 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
private void selectedCrypto(Crypto crypto) {
|
private void selectedCrypto(Crypto crypto) {
|
||||||
final ImageButton button = ibCrypto.get(crypto);
|
final ImageButton button = ibCrypto.get(crypto);
|
||||||
|
assert button != null;
|
||||||
button.setImageResource(crypto.getIconEnabledId());
|
button.setImageResource(crypto.getIconEnabledId());
|
||||||
button.setImageAlpha(255);
|
button.setImageAlpha(255);
|
||||||
button.setEnabled(true);
|
button.setEnabled(true);
|
||||||
|
if (selectedCrypto == Crypto.XMR)
|
||||||
|
sendListener.setMode(SendFragment.Mode.XMR);
|
||||||
|
else
|
||||||
|
sendListener.setMode(SendFragment.Mode.BTC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void possibleCrypto(Crypto crypto) {
|
private void possibleCrypto(Crypto crypto) {
|
||||||
final ImageButton button = ibCrypto.get(crypto);
|
final ImageButton button = ibCrypto.get(crypto);
|
||||||
|
assert button != null;
|
||||||
button.setImageResource(crypto.getIconDisabledId());
|
button.setImageResource(crypto.getIconDisabledId());
|
||||||
button.setImageAlpha(255);
|
button.setImageAlpha(255);
|
||||||
button.setEnabled(true);
|
button.setEnabled(true);
|
||||||
|
@ -285,6 +250,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
private void impossibleCrypto(Crypto crypto) {
|
private void impossibleCrypto(Crypto crypto) {
|
||||||
final ImageButton button = ibCrypto.get(crypto);
|
final ImageButton button = ibCrypto.get(crypto);
|
||||||
|
if (button == null) return; // not all buttons exist for all providers
|
||||||
button.setImageResource(crypto.getIconDisabledId());
|
button.setImageResource(crypto.getIconDisabledId());
|
||||||
button.setImageAlpha(128);
|
button.setImageAlpha(128);
|
||||||
button.setEnabled(true);
|
button.setEnabled(true);
|
||||||
|
@ -302,7 +268,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((selectedCrypto != null) && (selectedCrypto != Crypto.XMR)) {
|
if ((selectedCrypto != null) && (selectedCrypto != Crypto.XMR)) {
|
||||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getSymbol())));
|
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getNetwork(), selectedCrypto.getLabel(), ShiftService.DEFAULT.getLabel())));
|
||||||
tvXmrTo.setVisibility(View.VISIBLE);
|
tvXmrTo.setVisibility(View.VISIBLE);
|
||||||
} else if ((selectedCrypto == null) && (possibleCryptos.size() > 1)) {
|
} else if ((selectedCrypto == null) && (possibleCryptos.size() > 1)) {
|
||||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_ambiguous)));
|
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_ambiguous)));
|
||||||
|
@ -328,7 +294,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
BarcodeData barcodeData = dataMap.get(Crypto.XMR);
|
BarcodeData barcodeData = dataMap.get(Crypto.XMR);
|
||||||
if (barcodeData == null) barcodeData = dataMap.get(Crypto.BTC);
|
if (barcodeData == null) barcodeData = dataMap.get(Crypto.BTC);
|
||||||
if (barcodeData != null) {
|
if (barcodeData != null) {
|
||||||
Timber.d("Security=%s, %s", barcodeData.security.toString(), barcodeData.address);
|
Timber.d("Security=%s, %s", barcodeData.getSecurity().toString(), barcodeData.getAddress());
|
||||||
processScannedData(barcodeData);
|
processScannedData(barcodeData);
|
||||||
} else {
|
} else {
|
||||||
etAddress.setError(getString(R.string.send_address_not_openalias));
|
etAddress.setError(getString(R.string.send_address_not_openalias));
|
||||||
|
@ -369,18 +335,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
&& Wallet.isAddressValid(address);
|
&& Wallet.isAddressValid(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isBitcoinishAddress(String address) {
|
|
||||||
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC)
|
|
||||||
||
|
|
||||||
BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC)
|
|
||||||
||
|
|
||||||
BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isEthAddress(String address) {
|
|
||||||
return EthAddressValidator.validate(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void shakeAddress() {
|
private void shakeAddress() {
|
||||||
if (possibleCryptos.size() > 1) { // address ambiguous
|
if (possibleCryptos.size() > 1) { // address ambiguous
|
||||||
for (Crypto crypto : Crypto.values()) {
|
for (Crypto crypto : Crypto.values()) {
|
||||||
|
@ -412,10 +366,10 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString());
|
((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString());
|
||||||
((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol());
|
((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol());
|
||||||
txData.setDestination(null);
|
txData.setDestination(null);
|
||||||
ServiceHelper.ASSET = selectedCrypto.getSymbol().toLowerCase();
|
ShiftService.ASSET = selectedCrypto;
|
||||||
} else {
|
} else {
|
||||||
txData.setDestination(etAddress.getEditText().getText().toString());
|
txData.setDestination(etAddress.getEditText().getText().toString());
|
||||||
ServiceHelper.ASSET = null;
|
ShiftService.ASSET = null;
|
||||||
}
|
}
|
||||||
txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString()));
|
txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString()));
|
||||||
txData.setPriority(PendingTransaction.Priority.Priority_Default);
|
txData.setPriority(PendingTransaction.Priority.Priority_Default);
|
||||||
|
@ -445,49 +399,33 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processScannedData(BarcodeData barcodeData) {
|
public void processScannedData(BarcodeData barcodeData) {
|
||||||
|
barcodeData.filter(ShiftService.getPossibleAssets());
|
||||||
sendListener.setBarcodeData(barcodeData);
|
sendListener.setBarcodeData(barcodeData);
|
||||||
if (isResumed())
|
if (isResumed())
|
||||||
processScannedData();
|
processScannedData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void processScannedData() {
|
public void processScannedData() {
|
||||||
BarcodeData barcodeData = sendListener.getBarcodeData();
|
final BarcodeData barcodeData = sendListener.getBarcodeData();
|
||||||
if (barcodeData != null) {
|
if (barcodeData != null) {
|
||||||
Timber.d("GOT DATA");
|
Timber.d("GOT DATA");
|
||||||
if (!Helper.ALLOW_SHIFT && (barcodeData.asset != Crypto.XMR)) {
|
if (!Helper.ALLOW_SHIFT && (barcodeData.getAsset() != Crypto.XMR)) {
|
||||||
Timber.d("BUT ONLY XMR SUPPORTED");
|
Timber.d("BUT ONLY XMR SUPPORTED");
|
||||||
barcodeData = null;
|
sendListener.setBarcodeData(null);
|
||||||
sendListener.setBarcodeData(barcodeData);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (barcodeData.address != null) {
|
if (barcodeData.getAddress() != null) {
|
||||||
etAddress.getEditText().setText(barcodeData.address);
|
etAddress.getEditText().setText(barcodeData.getAddress());
|
||||||
possibleCryptos.clear();
|
|
||||||
selectedCrypto = null;
|
|
||||||
if (barcodeData.isAmbiguous()) {
|
|
||||||
possibleCryptos.addAll(barcodeData.ambiguousAssets);
|
|
||||||
} else {
|
|
||||||
possibleCryptos.add(barcodeData.asset);
|
|
||||||
selectedCrypto = barcodeData.asset;
|
|
||||||
}
|
|
||||||
if (Helper.ALLOW_SHIFT)
|
|
||||||
updateCryptoButtons(false);
|
|
||||||
if (checkAddress()) {
|
|
||||||
if (barcodeData.security == BarcodeData.Security.OA_NO_DNSSEC)
|
|
||||||
etAddress.setError(getString(R.string.send_address_no_dnssec));
|
|
||||||
else if (barcodeData.security == BarcodeData.Security.OA_DNSSEC)
|
|
||||||
etAddress.setError(getString(R.string.send_address_openalias));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
etAddress.getEditText().getText().clear();
|
etAddress.getEditText().getText().clear();
|
||||||
etAddress.setError(null);
|
etAddress.setError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
String scannedNotes = barcodeData.addressName;
|
String scannedNotes = barcodeData.getAddressName();
|
||||||
if (scannedNotes == null) {
|
if (scannedNotes == null) {
|
||||||
scannedNotes = barcodeData.description;
|
scannedNotes = barcodeData.getDescription();
|
||||||
} else if (barcodeData.description != null) {
|
} else if (barcodeData.getDescription() != null) {
|
||||||
scannedNotes = scannedNotes + ": " + barcodeData.description;
|
scannedNotes = scannedNotes + ": " + barcodeData.getDescription();
|
||||||
}
|
}
|
||||||
if (scannedNotes != null) {
|
if (scannedNotes != null) {
|
||||||
etNotes.getEditText().setText(scannedNotes);
|
etNotes.getEditText().setText(scannedNotes);
|
||||||
|
|
|
@ -23,8 +23,6 @@ import android.view.ViewGroup;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
|
||||||
import com.m2049r.xmrwallet.R;
|
import com.m2049r.xmrwallet.R;
|
||||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||||
import com.m2049r.xmrwallet.data.TxData;
|
import com.m2049r.xmrwallet.data.TxData;
|
||||||
|
@ -38,17 +36,13 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
public static SendAmountWizardFragment newInstance(Listener listener) {
|
public static SendAmountWizardFragment newInstance(Listener listener) {
|
||||||
SendAmountWizardFragment instance = new SendAmountWizardFragment();
|
SendAmountWizardFragment instance = new SendAmountWizardFragment();
|
||||||
instance.setSendListener(listener);
|
instance.sendListener = listener;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Listener sendListener;
|
private Listener sendListener;
|
||||||
|
|
||||||
public void setSendListener(Listener listener) {
|
public interface Listener {
|
||||||
this.sendListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Listener {
|
|
||||||
SendFragment.Listener getActivityCallback();
|
SendFragment.Listener getActivityCallback();
|
||||||
|
|
||||||
TxData getTxData();
|
TxData getTxData();
|
||||||
|
@ -139,7 +133,7 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||||
public void onResumeFragment() {
|
public void onResumeFragment() {
|
||||||
super.onResumeFragment();
|
super.onResumeFragment();
|
||||||
Timber.d("onResumeFragment()");
|
Timber.d("onResumeFragment()");
|
||||||
Helper.showKeyboard(getActivity());
|
Helper.showKeyboard(requireActivity());
|
||||||
final long funds = getTotalFunds();
|
final long funds = getTotalFunds();
|
||||||
maxFunds = 1.0 * funds / Helper.ONE_XMR;
|
maxFunds = 1.0 * funds / Helper.ONE_XMR;
|
||||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||||
|
@ -150,8 +144,8 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||||
getString(R.string.unknown_amount)));
|
getString(R.string.unknown_amount)));
|
||||||
}
|
}
|
||||||
final BarcodeData data = sendListener.popBarcodeData();
|
final BarcodeData data = sendListener.popBarcodeData();
|
||||||
if ((data != null) && (data.amount != null)) {
|
if ((data != null) && (data.getAmount() != null)) {
|
||||||
etAmount.setAmount(data.amount);
|
etAmount.setAmount(data.getAmount());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,48 +17,49 @@
|
||||||
package com.m2049r.xmrwallet.fragment.send;
|
package com.m2049r.xmrwallet.fragment.send;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.R;
|
import com.m2049r.xmrwallet.R;
|
||||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||||
|
import com.m2049r.xmrwallet.data.CryptoAmount;
|
||||||
|
import com.m2049r.xmrwallet.data.TxData;
|
||||||
import com.m2049r.xmrwallet.data.TxDataBtc;
|
import com.m2049r.xmrwallet.data.TxDataBtc;
|
||||||
import com.m2049r.xmrwallet.model.Wallet;
|
import com.m2049r.xmrwallet.model.Wallet;
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftError;
|
import com.m2049r.xmrwallet.service.shift.ShiftError;
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
import com.m2049r.xmrwallet.service.shift.process.PreShiftProcess;
|
||||||
|
import com.m2049r.xmrwallet.util.AmountHelper;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
|
||||||
import com.m2049r.xmrwallet.widget.ExchangeOtherEditText;
|
import com.m2049r.xmrwallet.widget.ExchangeOtherEditText;
|
||||||
import com.m2049r.xmrwallet.widget.SendProgressView;
|
import com.m2049r.xmrwallet.widget.SendProgressView;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
public class SendBtcAmountWizardFragment extends SendWizardFragment implements PreShifter, ExchangeOtherEditText.Listener {
|
||||||
|
|
||||||
public static SendBtcAmountWizardFragment newInstance(SendAmountWizardFragment.Listener listener) {
|
public static SendBtcAmountWizardFragment newInstance(SendAmountWizardFragment.Listener listener) {
|
||||||
SendBtcAmountWizardFragment instance = new SendBtcAmountWizardFragment();
|
return new SendBtcAmountWizardFragment(listener);
|
||||||
instance.setSendListener(listener);
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SendAmountWizardFragment.Listener sendListener;
|
private SendBtcAmountWizardFragment(@NonNull SendAmountWizardFragment.Listener listener) {
|
||||||
|
super();
|
||||||
public SendBtcAmountWizardFragment setSendListener(SendAmountWizardFragment.Listener listener) {
|
sendListener = listener;
|
||||||
this.sendListener = listener;
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final SendAmountWizardFragment.Listener sendListener;
|
||||||
|
|
||||||
private TextView tvFunds;
|
private TextView tvFunds;
|
||||||
private ExchangeOtherEditText etAmount;
|
private ExchangeOtherEditText etAmount;
|
||||||
|
|
||||||
|
@ -72,8 +73,6 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
||||||
|
|
||||||
sendListener = (SendAmountWizardFragment.Listener) getParentFragment();
|
|
||||||
|
|
||||||
View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false);
|
View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false);
|
||||||
|
|
||||||
tvFunds = view.findViewById(R.id.tvFunds);
|
tvFunds = view.findViewById(R.id.tvFunds);
|
||||||
|
@ -83,6 +82,8 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
tvXmrToParms = view.findViewById(R.id.tvXmrToParms);
|
tvXmrToParms = view.findViewById(R.id.tvXmrToParms);
|
||||||
|
|
||||||
|
((ImageView) view.findViewById(R.id.shiftIcon)).setImageResource(service.getIconId());
|
||||||
|
|
||||||
etAmount = view.findViewById(R.id.etAmount);
|
etAmount = view.findViewById(R.id.etAmount);
|
||||||
etAmount.requestFocus();
|
etAmount.requestFocus();
|
||||||
|
|
||||||
|
@ -98,32 +99,19 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||||
if (orderParameters == null) {
|
if (orderParameters == null) {
|
||||||
return false; // this should never happen
|
return false; // this should never happen
|
||||||
}
|
}
|
||||||
if (sendListener != null) {
|
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
txDataBtc.setShiftAmount(etAmount.getPrimaryAmount());
|
||||||
String btcString = etAmount.getNativeAmount();
|
|
||||||
if (btcString != null) {
|
|
||||||
try {
|
|
||||||
double btc = Double.parseDouble(btcString);
|
|
||||||
Timber.d("setBtcAmount %f", btc);
|
|
||||||
txDataBtc.setBtcAmount(btc);
|
|
||||||
txDataBtc.setAmount(btc / orderParameters.getPrice());
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Timber.d(ex.getLocalizedMessage());
|
|
||||||
txDataBtc.setBtcAmount(0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
txDataBtc.setBtcAmount(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
double maxBtc = 0;
|
private double maxBtc = 0;
|
||||||
double minBtc = 0;
|
private double minBtc = 0;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPauseFragment() {
|
public void onPauseFragment() {
|
||||||
|
super.onPauseFragment();
|
||||||
llXmrToParms.setVisibility(View.INVISIBLE);
|
llXmrToParms.setVisibility(View.INVISIBLE);
|
||||||
|
etAmount.setListener(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -131,8 +119,8 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||||
super.onResumeFragment();
|
super.onResumeFragment();
|
||||||
Timber.d("onResumeFragment()");
|
Timber.d("onResumeFragment()");
|
||||||
final String btcSymbol = ((TxDataBtc) sendListener.getTxData()).getBtcSymbol();
|
final String btcSymbol = ((TxDataBtc) sendListener.getTxData()).getBtcSymbol();
|
||||||
if (!btcSymbol.toLowerCase().equals(ServiceHelper.ASSET))
|
if (!btcSymbol.equalsIgnoreCase(ShiftService.ASSET.getSymbol()))
|
||||||
throw new IllegalStateException("Asset Symbol is wrong!");
|
throw new IllegalStateException("Asset Symbol is wrong (" + btcSymbol + "!=" + ShiftService.ASSET.getSymbol() + ")");
|
||||||
final long funds = getTotalFunds();
|
final long funds = getTotalFunds();
|
||||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||||
tvFunds.setText(getString(R.string.send_available,
|
tvFunds.setText(getString(R.string.send_available,
|
||||||
|
@ -143,48 +131,131 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||||
getString(R.string.unknown_amount)));
|
getString(R.string.unknown_amount)));
|
||||||
}
|
}
|
||||||
etAmount.setAmount("");
|
etAmount.setAmount("");
|
||||||
|
etAmount.setListener(this);
|
||||||
final BarcodeData data = sendListener.popBarcodeData();
|
final BarcodeData data = sendListener.popBarcodeData();
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
if (data.amount != null) {
|
if (data.getAmount() != null) {
|
||||||
etAmount.setAmount(data.amount);
|
etAmount.setAmount(data.getAmount());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
etAmount.setBaseCurrency(btcSymbol);
|
etAmount.setBaseCurrency(btcSymbol);
|
||||||
callXmrTo();
|
updateShift();
|
||||||
}
|
}
|
||||||
|
|
||||||
long getTotalFunds() {
|
long getTotalFunds() {
|
||||||
return sendListener.getActivityCallback().getTotalFunds();
|
return sendListener.getActivityCallback().getTotalFunds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final ShiftService service = ShiftService.DEFAULT;
|
||||||
|
private final PreShiftProcess preShiftProcess = service.createPreProcess(this);
|
||||||
|
|
||||||
private QueryOrderParameters orderParameters = null;
|
private QueryOrderParameters orderParameters = null;
|
||||||
|
|
||||||
private void processOrderParms(final QueryOrderParameters orderParameters) {
|
private void reset() {
|
||||||
this.orderParameters = orderParameters;
|
orderParameters = null;
|
||||||
getView().post(() -> {
|
maxBtc = 0;
|
||||||
|
minBtc = 0;
|
||||||
|
etAmount.setExchangeRate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateShift() {
|
||||||
|
reset();
|
||||||
|
getTxData().setShiftService(service);
|
||||||
|
llXmrToParms.setVisibility(View.INVISIBLE);
|
||||||
|
preShiftProcess.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TxDataBtc getTxData() {
|
||||||
|
final TxData txData = sendListener.getTxData();
|
||||||
|
if (txData instanceof TxDataBtc) {
|
||||||
|
return (TxDataBtc) txData;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("TxData not BTC");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValid() {
|
||||||
|
return orderParameters != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CryptoAmount getAmount() { // of BTC
|
||||||
|
return etAmount.getPrimaryAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLowerLimit() {
|
||||||
|
if (!isValid()) throw new IllegalStateException();
|
||||||
|
return orderParameters.getLowerLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPrice() {
|
||||||
|
if (!isValid()) throw new IllegalStateException();
|
||||||
|
return orderParameters.getPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getUpperLimit() {
|
||||||
|
if (!isValid()) throw new IllegalStateException();
|
||||||
|
return orderParameters.getUpperLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOrderParametersError(final Exception ex) {
|
||||||
|
reset();
|
||||||
|
Timber.e(ex);
|
||||||
|
requireView().post(() -> {
|
||||||
|
if (ex instanceof ShiftException) {
|
||||||
|
ShiftException xmrEx = (ShiftException) ex;
|
||||||
|
ShiftError xmrErr = xmrEx.getError();
|
||||||
|
if (xmrErr != null) {
|
||||||
|
if (xmrErr.isRetryable()) {
|
||||||
|
evParams.showMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
|
||||||
|
getString(R.string.text_retry));
|
||||||
|
evParams.setOnClickListener(v -> {
|
||||||
|
evParams.setOnClickListener(null);
|
||||||
|
updateShift();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
evParams.showMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
|
||||||
|
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
|
||||||
|
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
|
||||||
|
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
|
||||||
|
ex.getLocalizedMessage(),
|
||||||
|
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOrderParametersReceived(QueryOrderParameters orderParameters) {
|
||||||
final double price = orderParameters.getPrice();
|
final double price = orderParameters.getPrice();
|
||||||
etAmount.setExchangeRate(1 / price);
|
|
||||||
maxBtc = price * orderParameters.getUpperLimit();
|
maxBtc = price * orderParameters.getUpperLimit();
|
||||||
minBtc = price * orderParameters.getLowerLimit();
|
minBtc = price * orderParameters.getLowerLimit();
|
||||||
|
this.orderParameters = orderParameters;
|
||||||
|
requireView().post(() -> {
|
||||||
|
etAmount.setExchangeRate(1 / price);
|
||||||
Timber.d("minBtc=%f / maxBtc=%f", minBtc, maxBtc);
|
Timber.d("minBtc=%f / maxBtc=%f", minBtc, maxBtc);
|
||||||
NumberFormat df = NumberFormat.getInstance(Locale.US);
|
final String min = AmountHelper.format_6(minBtc);
|
||||||
df.setMaximumFractionDigits(6);
|
final String max = AmountHelper.format_6(maxBtc);
|
||||||
String min = df.format(minBtc);
|
final String rate = AmountHelper.format_6(price);
|
||||||
String max = df.format(maxBtc);
|
|
||||||
String rate = df.format(price);
|
|
||||||
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||||
Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
|
final Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
|
||||||
min, max, rate, txDataBtc.getBtcSymbol()));
|
min, max, rate, txDataBtc.getBtcSymbol(), service.getLabel()));
|
||||||
tvXmrToParms.setText(xmrParmText);
|
tvXmrToParms.setText(xmrParmText);
|
||||||
|
|
||||||
final long funds = getTotalFunds();
|
final long funds = getTotalFunds();
|
||||||
double availableXmr = 1.0 * funds / Helper.ONE_XMR;
|
final double availableXmr = 1.0 * funds / Helper.ONE_XMR;
|
||||||
|
|
||||||
String availBtcString;
|
String availBtcString;
|
||||||
String availXmrString;
|
String availXmrString;
|
||||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||||
availBtcString = df.format(availableXmr * price);
|
availBtcString = AmountHelper.format_6(availableXmr * price);
|
||||||
availXmrString = df.format(availableXmr);
|
availXmrString = AmountHelper.format_6(availableXmr);
|
||||||
} else {
|
} else {
|
||||||
availBtcString = getString(R.string.unknown_amount);
|
availBtcString = getString(R.string.unknown_amount);
|
||||||
availXmrString = availBtcString;
|
availXmrString = availBtcString;
|
||||||
|
@ -198,66 +269,28 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processOrderParmsError(final Exception ex) {
|
@Override
|
||||||
etAmount.setExchangeRate(0);
|
public boolean isActive() {
|
||||||
orderParameters = null;
|
return true;
|
||||||
maxBtc = 0;
|
} // TODO Test what happens if we swtich away while querying
|
||||||
minBtc = 0;
|
|
||||||
Timber.e(ex);
|
|
||||||
getView().post(() -> {
|
|
||||||
if (ex instanceof ShiftException) {
|
|
||||||
ShiftException xmrEx = (ShiftException) ex;
|
|
||||||
ShiftError xmrErr = xmrEx.getError();
|
|
||||||
if (xmrErr != null) {
|
|
||||||
if (xmrErr.isRetryable()) {
|
|
||||||
evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
|
||||||
getString(R.string.text_retry));
|
|
||||||
evParams.setOnClickListener(v -> {
|
|
||||||
evParams.setOnClickListener(null);
|
|
||||||
callXmrTo();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
|
||||||
getString(R.string.text_noretry));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
|
|
||||||
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
|
|
||||||
getString(R.string.text_noretry));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
|
|
||||||
ex.getLocalizedMessage(),
|
|
||||||
getString(R.string.text_noretry));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void callXmrTo() {
|
@Override
|
||||||
|
public void showProgress() {
|
||||||
evParams.showProgress(getString(R.string.label_send_progress_queryparms));
|
evParams.showProgress(getString(R.string.label_send_progress_queryparms));
|
||||||
getXmrToApi().queryOrderParameters(new ShiftCallback<QueryOrderParameters>() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(final QueryOrderParameters orderParameters) {
|
|
||||||
processOrderParms(orderParameters);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long lastRequest = 0;
|
||||||
|
final static long EXCHANGE_TIME = 750; //ms
|
||||||
|
final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(final Exception e) {
|
public void onExchangeRequested() {
|
||||||
processOrderParmsError(e);
|
final long now = System.currentTimeMillis();
|
||||||
|
lastRequest = now;
|
||||||
|
handler.postDelayed(() -> {
|
||||||
|
if (now == lastRequest) { // otherwise we are superseded
|
||||||
|
updateShift();
|
||||||
}
|
}
|
||||||
});
|
}, EXCHANGE_TIME);
|
||||||
}
|
|
||||||
|
|
||||||
private SideShiftApi xmrToApi = null;
|
|
||||||
|
|
||||||
private SideShiftApi getXmrToApi() {
|
|
||||||
if (xmrToApi == null) {
|
|
||||||
synchronized (this) {
|
|
||||||
if (xmrToApi == null) {
|
|
||||||
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return xmrToApi;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -24,39 +24,36 @@ import android.widget.Button;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.R;
|
import com.m2049r.xmrwallet.R;
|
||||||
import com.m2049r.xmrwallet.data.TxData;
|
import com.m2049r.xmrwallet.data.TxData;
|
||||||
import com.m2049r.xmrwallet.data.TxDataBtc;
|
import com.m2049r.xmrwallet.data.TxDataBtc;
|
||||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||||
import com.m2049r.xmrwallet.model.Wallet;
|
import com.m2049r.xmrwallet.model.Wallet;
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftError;
|
import com.m2049r.xmrwallet.service.shift.ShiftError;
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
|
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
import com.m2049r.xmrwallet.service.shift.process.ShiftProcess;
|
||||||
|
import com.m2049r.xmrwallet.util.AmountHelper;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
|
||||||
import com.m2049r.xmrwallet.widget.SendProgressView;
|
import com.m2049r.xmrwallet.widget.SendProgressView;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm {
|
public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm, Shifter {
|
||||||
|
|
||||||
public static SendBtcConfirmWizardFragment newInstance(SendConfirmWizardFragment.Listener listener) {
|
public static SendBtcConfirmWizardFragment newInstance(SendConfirmWizardFragment.Listener listener) {
|
||||||
SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment();
|
SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment();
|
||||||
instance.setSendListener(listener);
|
instance.sendListener = listener;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
SendConfirmWizardFragment.Listener sendListener;
|
private SendConfirmWizardFragment.Listener sendListener;
|
||||||
|
|
||||||
public void setSendListener(SendConfirmWizardFragment.Listener listener) {
|
|
||||||
this.sendListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
private View llStageA;
|
private View llStageA;
|
||||||
private SendProgressView evStageA;
|
private SendProgressView evStageA;
|
||||||
|
@ -77,11 +74,12 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
private TextView tvTxChange;
|
private TextView tvTxChange;
|
||||||
private View llPocketChange;
|
private View llPocketChange;
|
||||||
|
|
||||||
|
private TextView tvTxXmrToKeyLabel;
|
||||||
|
private TextView tvTxXmrToInfo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
|
|
||||||
Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState)));
|
Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState)));
|
||||||
|
|
||||||
View view = inflater.inflate(
|
View view = inflater.inflate(
|
||||||
|
@ -93,6 +91,10 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
tvTxBtcRate = view.findViewById(R.id.tvTxBtcRate);
|
tvTxBtcRate = view.findViewById(R.id.tvTxBtcRate);
|
||||||
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
|
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
|
||||||
|
|
||||||
|
tvTxXmrToKeyLabel = view.findViewById(R.id.tvTxXmrToKeyLabel);
|
||||||
|
tvTxXmrToInfo = view.findViewById(R.id.tvTxXmrToInfo);
|
||||||
|
|
||||||
|
|
||||||
tvTxFee = view.findViewById(R.id.tvTxFee);
|
tvTxFee = view.findViewById(R.id.tvTxFee);
|
||||||
tvTxTotal = view.findViewById(R.id.tvTxTotal);
|
tvTxTotal = view.findViewById(R.id.tvTxTotal);
|
||||||
tvTxChange = view.findViewById(R.id.tvTxChange);
|
tvTxChange = view.findViewById(R.id.tvTxChange);
|
||||||
|
@ -106,7 +108,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
evStageC = view.findViewById(R.id.evStageC);
|
evStageC = view.findViewById(R.id.evStageC);
|
||||||
|
|
||||||
tvTxXmrToKey.setOnClickListener(v -> {
|
tvTxXmrToKey.setOnClickListener(v -> {
|
||||||
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
|
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
|
||||||
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -125,68 +127,74 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
int inProgress = 0;
|
@NonNull
|
||||||
final static int STAGE_X = 0;
|
Shifter.Stage inProgress = Stage.X;
|
||||||
final static int STAGE_A = 1;
|
|
||||||
final static int STAGE_B = 2;
|
|
||||||
final static int STAGE_C = 3;
|
|
||||||
|
|
||||||
private void showProgress(int stage, String progressText) {
|
@Override
|
||||||
Timber.d("showProgress(%d)", stage);
|
public void showProgress(@NonNull Shifter.Stage stage) {
|
||||||
inProgress = stage;
|
Timber.d("showProgress(%s)", stage);
|
||||||
|
requireView().post(() -> {
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case STAGE_A:
|
case A:
|
||||||
evStageA.showProgress(progressText);
|
evStageA.showProgress(getString(R.string.label_send_progress_xmrto_create));
|
||||||
break;
|
break;
|
||||||
case STAGE_B:
|
case B:
|
||||||
evStageB.showProgress(progressText);
|
evStageB.showProgress(getString(R.string.label_send_progress_xmrto_query));
|
||||||
break;
|
break;
|
||||||
case STAGE_C:
|
case C:
|
||||||
evStageC.showProgress(progressText);
|
evStageC.showProgress(getString(R.string.label_send_progress_create_tx));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("unknown stage " + stage);
|
throw new IllegalArgumentException("invalid stage " + stage);
|
||||||
}
|
}
|
||||||
|
inProgress = stage;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hideProgress() {
|
public void hideProgress() {
|
||||||
Timber.d("hideProgress(%d)", inProgress);
|
Timber.d("hideProgress(%s)", inProgress);
|
||||||
switch (inProgress) {
|
switch (inProgress) {
|
||||||
case STAGE_A:
|
case A:
|
||||||
evStageA.hideProgress();
|
evStageA.hideProgress();
|
||||||
llStageA.setVisibility(View.VISIBLE);
|
llStageA.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
case STAGE_B:
|
case B:
|
||||||
evStageB.hideProgress();
|
evStageB.hideProgress();
|
||||||
|
llStageA.setVisibility(View.VISIBLE); // show Stage A info when B is ready
|
||||||
llStageB.setVisibility(View.VISIBLE);
|
llStageB.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
case STAGE_C:
|
case C:
|
||||||
evStageC.hideProgress();
|
evStageC.hideProgress();
|
||||||
llStageC.setVisibility(View.VISIBLE);
|
llStageC.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
throw new IllegalStateException("unknown stage " + inProgress);
|
|
||||||
}
|
}
|
||||||
inProgress = STAGE_X;
|
inProgress = Stage.X;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showStageError(String code, String message, String solution) {
|
private void showErrorMessage(String code, String message, String solution) {
|
||||||
switch (inProgress) {
|
switch (inProgress) {
|
||||||
case STAGE_A:
|
case A:
|
||||||
evStageA.showMessage(code, message, solution);
|
evStageA.showMessage(code, message, solution);
|
||||||
break;
|
break;
|
||||||
case STAGE_B:
|
case B:
|
||||||
evStageB.showMessage(code, message, solution);
|
evStageB.showMessage(code, message, solution);
|
||||||
break;
|
break;
|
||||||
case STAGE_C:
|
case C:
|
||||||
evStageC.showMessage(code, message, solution);
|
evStageC.showMessage(code, message, solution);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("unknown stage");
|
throw new IllegalStateException("invalid stage");
|
||||||
}
|
}
|
||||||
inProgress = STAGE_X;
|
inProgress = Stage.X;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void showQuoteError() {
|
||||||
|
showErrorMessage(ShiftError.Type.SERVICE.toString(),
|
||||||
|
getString(R.string.shift_noquote),
|
||||||
|
getString(R.string.shift_checkamount));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
PendingTransaction pendingTransaction = null;
|
PendingTransaction pendingTransaction = null;
|
||||||
|
|
||||||
void send() {
|
void send() {
|
||||||
|
@ -196,11 +204,8 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendListener.getTxData().getUserNotes().setXmrtoOrder(xmrtoOrder); // note the transaction in the TX notes
|
|
||||||
((TxDataBtc) sendListener.getTxData()).setXmrtoOrderId(xmrtoOrder.getOrderId()); // remember the order id for later
|
|
||||||
// TODO make method in TxDataBtc to set both of the above in one go
|
|
||||||
sendListener.commitTransaction();
|
sendListener.commitTransaction();
|
||||||
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
|
requireActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -209,15 +214,18 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
Toast.makeText(getContext(), getString(R.string.status_transaction_failed, error), Toast.LENGTH_LONG).show();
|
Toast.makeText(getContext(), getString(R.string.status_transaction_failed, error), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String orderId = null;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
// callback from wallet when PendingTransaction created (started by prepareSend() here
|
// callback from wallet when PendingTransaction created (started by prepareSend() here
|
||||||
public void transactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
|
public void transactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
|
||||||
if (isResumed
|
if (isResumed
|
||||||
&& (inProgress == STAGE_C)
|
&& (inProgress == Stage.C)
|
||||||
&& (xmrtoOrder != null)
|
&& (orderId != null)
|
||||||
&& (xmrtoOrder.getOrderId().equals(txTag))) {
|
&& (orderId.equals(txTag))) {
|
||||||
this.pendingTransaction = pendingTransaction;
|
this.pendingTransaction = pendingTransaction;
|
||||||
getView().post(() -> {
|
requireView().post(() -> {
|
||||||
|
Timber.d("transactionCreated");
|
||||||
hideProgress();
|
hideProgress();
|
||||||
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
|
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
|
||||||
tvTxTotal.setText(Wallet.getDisplayAmount(
|
tvTxTotal.setText(Wallet.getDisplayAmount(
|
||||||
|
@ -243,29 +251,35 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
if (pendingTransaction != null) {
|
if (pendingTransaction != null) {
|
||||||
throw new IllegalStateException("pendingTransaction is not null");
|
throw new IllegalStateException("pendingTransaction is not null");
|
||||||
}
|
}
|
||||||
showStageError(getString(R.string.send_create_tx_error_title),
|
showErrorMessage(getString(R.string.send_create_tx_error_title),
|
||||||
errorText,
|
errorText,
|
||||||
getString(R.string.text_noretry_monero));
|
getString(R.string.text_noretry_monero));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onValidateFields() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isResumed = false;
|
private boolean isResumed = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPauseFragment() {
|
public void onPauseFragment() {
|
||||||
isResumed = false;
|
isResumed = false;
|
||||||
|
shiftProcess = null;
|
||||||
stopSendTimer();
|
stopSendTimer();
|
||||||
sendListener.disposeTransaction();
|
sendListener.disposeTransaction();
|
||||||
pendingTransaction = null;
|
pendingTransaction = null;
|
||||||
inProgress = STAGE_X;
|
inProgress = Stage.X;
|
||||||
|
//TODO: maybe reset the progress messages
|
||||||
updateSendButton();
|
updateSendButton();
|
||||||
super.onPauseFragment();
|
super.onPauseFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TxDataBtc getTxData() {
|
||||||
|
final TxData txData = sendListener.getTxData();
|
||||||
|
if (txData instanceof TxDataBtc) {
|
||||||
|
return (TxDataBtc) txData;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("TxData not BTC");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResumeFragment() {
|
public void onResumeFragment() {
|
||||||
super.onResumeFragment();
|
super.onResumeFragment();
|
||||||
|
@ -273,8 +287,9 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
if (sendListener.getMode() != SendFragment.Mode.BTC) {
|
if (sendListener.getMode() != SendFragment.Mode.BTC) {
|
||||||
throw new IllegalStateException("Mode is not BTC!");
|
throw new IllegalStateException("Mode is not BTC!");
|
||||||
}
|
}
|
||||||
if (!((TxDataBtc) sendListener.getTxData()).getBtcSymbol().toLowerCase().equals(ServiceHelper.ASSET))
|
final String btcSymbol = getTxData().getBtcSymbol();
|
||||||
throw new IllegalStateException("Asset Symbol is wrong!");
|
if (!btcSymbol.equalsIgnoreCase(ShiftService.ASSET.getSymbol()))
|
||||||
|
throw new IllegalStateException("Asset Symbol is wrong (" + btcSymbol + "!=" + ShiftService.ASSET.getSymbol() + ")");
|
||||||
Helper.hideKeyboard(getActivity());
|
Helper.hideKeyboard(getActivity());
|
||||||
llStageA.setVisibility(View.INVISIBLE);
|
llStageA.setVisibility(View.INVISIBLE);
|
||||||
evStageA.hideProgress();
|
evStageA.hideProgress();
|
||||||
|
@ -282,9 +297,17 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
evStageB.hideProgress();
|
evStageB.hideProgress();
|
||||||
llStageC.setVisibility(View.INVISIBLE);
|
llStageC.setVisibility(View.INVISIBLE);
|
||||||
evStageC.hideProgress();
|
evStageC.hideProgress();
|
||||||
|
|
||||||
|
if (shiftProcess != null) throw new IllegalStateException("shiftProcess not null");
|
||||||
|
shiftProcess = getTxData().getShiftService().createProcess(this);
|
||||||
|
tvTxXmrToKeyLabel.setText(getString(R.string.label_send_btc_xmrto_key, shiftProcess.getService().getLabel()));
|
||||||
|
tvTxXmrToKeyLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(shiftProcess.getService().getIconId(), 0, 0, 0);
|
||||||
|
tvTxXmrToInfo.setText(getString(R.string.label_send_btc_xmrto_info, shiftProcess.getService().getLabel()));
|
||||||
|
|
||||||
isResumed = true;
|
isResumed = true;
|
||||||
if ((pendingTransaction == null) && (inProgress == STAGE_X)) {
|
if ((pendingTransaction == null) && (inProgress == Stage.X)) {
|
||||||
stageA();
|
Timber.d("Starting ShiftProcess");
|
||||||
|
shiftProcess.run(getTxData());
|
||||||
} // otherwise just sit there blank
|
} // otherwise just sit there blank
|
||||||
// TODO: don't sit there blank - can this happen? should we just die?
|
// TODO: don't sit there blank - can this happen? should we just die?
|
||||||
}
|
}
|
||||||
|
@ -310,19 +333,19 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
}
|
}
|
||||||
int minutes = sendCountdown / 60;
|
int minutes = sendCountdown / 60;
|
||||||
int seconds = sendCountdown % 60;
|
int seconds = sendCountdown % 60;
|
||||||
String t = String.format("%d:%02d", minutes, seconds);
|
String t = String.format(Locale.US, "%d:%02d", minutes, seconds);
|
||||||
bSend.setText(getString(R.string.send_send_timed_label, t));
|
bSend.setText(getString(R.string.send_send_timed_label, t));
|
||||||
if (sendCountdown > 0) {
|
if (sendCountdown > 0) {
|
||||||
sendCountdown -= XMRTO_COUNTDOWN_STEP;
|
sendCountdown -= XMRTO_COUNTDOWN_STEP;
|
||||||
getView().postDelayed(this, XMRTO_COUNTDOWN_STEP * 1000);
|
requireView().postDelayed(this, XMRTO_COUNTDOWN_STEP * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
getView().post(updateRunnable);
|
requireView().post(updateRunnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopSendTimer() {
|
void stopSendTimer() {
|
||||||
getView().removeCallbacks(updateRunnable);
|
requireView().removeCallbacks(updateRunnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSendButton() {
|
void updateSendButton() {
|
||||||
|
@ -344,7 +367,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fail(String walletName) {
|
public void fail(String walletName) {
|
||||||
getActivity().runOnUiThread(() -> {
|
requireActivity().runOnUiThread(() -> {
|
||||||
bSend.setEnabled(sendCountdown > 0); // allow to try again
|
bSend.setEnabled(sendCountdown > 0); // allow to try again
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -353,211 +376,127 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||||
|
|
||||||
// creates a pending transaction and calls us back with transactionCreated()
|
// creates a pending transaction and calls us back with transactionCreated()
|
||||||
// or createTransactionFailed()
|
// or createTransactionFailed()
|
||||||
void prepareSend() {
|
public void prepareSend(CreateOrder order) {
|
||||||
if (!isResumed) return;
|
if (!isResumed) return;
|
||||||
if ((xmrtoOrder == null)) {
|
if ((order == null)) {
|
||||||
throw new IllegalStateException("xmrtoOrder is null");
|
throw new IllegalStateException("order is null");
|
||||||
}
|
}
|
||||||
showProgress(3, getString(R.string.label_send_progress_create_tx));
|
showProgress(Stage.C);
|
||||||
final TxData txData = sendListener.getTxData();
|
orderId = order.getOrderId();
|
||||||
txData.setDestination(xmrtoOrder.getXmrAddress());
|
final TxDataBtc txData = getTxData();
|
||||||
txData.setAmount(xmrtoOrder.getXmrAmount());
|
txData.setDestination(order.getXmrAddress());
|
||||||
getActivityCallback().onPrepareSend(xmrtoOrder.getOrderId(), txData);
|
txData.setAmount(order.getXmrAmount());
|
||||||
|
txData.getUserNotes().setXmrtoOrder(order); // note the transaction in the TX notes
|
||||||
|
txData.setXmrtoOrderId(order.getOrderId()); // remember the order id for later
|
||||||
|
txData.setXmrtoQueryOrderToken(order.getQueryOrderId()); // remember the order id for later
|
||||||
|
getActivityCallback().onPrepareSend(order.getOrderId(), txData);
|
||||||
}
|
}
|
||||||
|
|
||||||
SendFragment.Listener getActivityCallback() {
|
SendFragment.Listener getActivityCallback() {
|
||||||
return sendListener.getActivityCallback();
|
return sendListener.getActivityCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequestQuote xmrtoQuote = null;
|
private ShiftProcess shiftProcess;
|
||||||
|
|
||||||
private void processStageA(final RequestQuote requestQuote) {
|
public void showQuote(double btcAmount, double xmrAmount, double price) {
|
||||||
Timber.d("processCreateOrder %s", requestQuote.getId());
|
final String symbol = getTxData().getBtcSymbol();
|
||||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
|
||||||
// verify the BTC amount is correct
|
|
||||||
if (requestQuote.getBtcAmount() != txDataBtc.getBtcAmount()) {
|
|
||||||
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());
|
|
||||||
final String xmrAmountTotal = df.format(xmrtoQuote.getXmrAmount());
|
|
||||||
tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount,
|
tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount,
|
||||||
btcAmount, xmrAmountTotal, txDataBtc.getBtcSymbol()));
|
AmountHelper.format(btcAmount), AmountHelper.format(xmrAmount), symbol));
|
||||||
final String xmrPriceBtc = df.format(xmrtoQuote.getPrice());
|
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, AmountHelper.format(price), symbol));
|
||||||
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, xmrPriceBtc, txDataBtc.getBtcSymbol()));
|
}
|
||||||
|
|
||||||
|
// Shifter
|
||||||
|
public void onQuoteReceived(RequestQuote quote) {
|
||||||
|
requireView().post(() -> {
|
||||||
|
Timber.d("onQuoteReceived");
|
||||||
|
showQuote(quote.getBtcAmount(), quote.getXmrAmount(), quote.getPrice());
|
||||||
hideProgress();
|
hideProgress();
|
||||||
});
|
});
|
||||||
stageB(requestQuote.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processStageAError(final Exception ex) {
|
public void onQuoteError(final Exception ex) {
|
||||||
Timber.e("processStageAError %s", ex.getLocalizedMessage());
|
Timber.e("processStageAError %s", ex.getLocalizedMessage());
|
||||||
getView().post(() -> {
|
requireView().post(() -> {
|
||||||
if (ex instanceof ShiftException) {
|
if (ex instanceof ShiftException) {
|
||||||
ShiftException xmrEx = (ShiftException) ex;
|
ShiftException xmrEx = (ShiftException) ex;
|
||||||
ShiftError xmrErr = xmrEx.getError();
|
ShiftError xmrErr = xmrEx.getError();
|
||||||
if (xmrErr != null) {
|
if (xmrErr != null) {
|
||||||
if (xmrErr.isRetryable()) {
|
if (xmrErr.isRetryable()) {
|
||||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
|
||||||
getString(R.string.text_retry));
|
getString(R.string.text_retry));
|
||||||
evStageA.setOnClickListener(v -> {
|
evStageA.setOnClickListener(v -> {
|
||||||
evStageA.setOnClickListener(null);
|
evStageA.setOnClickListener(null);
|
||||||
stageA();
|
shiftProcess.restart();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
|
||||||
getString(R.string.text_noretry));
|
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showStageError(getString(R.string.label_generic_xmrto_error),
|
showErrorMessage(getString(R.string.label_generic_xmrto_error),
|
||||||
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
|
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
|
||||||
getString(R.string.text_noretry));
|
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
evStageA.showMessage(getString(R.string.label_generic_xmrto_error),
|
evStageA.showMessage(getString(R.string.label_generic_xmrto_error),
|
||||||
ex.getLocalizedMessage(),
|
ex.getLocalizedMessage(),
|
||||||
getString(R.string.text_noretry));
|
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stageA() {
|
|
||||||
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();
|
|
||||||
|
|
||||||
ShiftCallback<RequestQuote> callback = new ShiftCallback<RequestQuote>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(RequestQuote requestQuote) {
|
public boolean isActive() {
|
||||||
if (!isResumed) return;
|
return isResumed;
|
||||||
if (xmrtoQuote != null) {
|
|
||||||
Timber.w("another ongoing request quote request");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
processStageA(requestQuote);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void onOrderCreated(CreateOrder order) {
|
||||||
public void onError(Exception ex) {
|
requireView().post(() -> {
|
||||||
if (!isResumed) return;
|
showQuote(order.getBtcAmount(), order.getXmrAmount(), order.getBtcAmount() / order.getXmrAmount());
|
||||||
if (xmrtoQuote != null) {
|
|
||||||
Timber.w("another ongoing request quote request");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
processStageAError(ex);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getXmrToApi().requestQuote(txDataBtc.getBtcAmount(), callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CreateOrder xmrtoOrder = null;
|
|
||||||
|
|
||||||
private void processStageB(final CreateOrder order) {
|
|
||||||
Timber.d("processCreateOrder %s for %s", order.getOrderId(), order.getQuoteId());
|
|
||||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
|
||||||
// verify amount & destination
|
|
||||||
if ((order.getBtcAmount() != txDataBtc.getBtcAmount())
|
|
||||||
|| (!txDataBtc.validateAddress(order.getBtcAddress()))) {
|
|
||||||
throw new IllegalStateException("Order does not fulfill quote!"); // something is terribly wrong - die
|
|
||||||
}
|
|
||||||
xmrtoOrder = order;
|
|
||||||
getView().post(() -> {
|
|
||||||
tvTxXmrToKey.setText(order.getOrderId());
|
tvTxXmrToKey.setText(order.getOrderId());
|
||||||
tvTxBtcAddress.setText(order.getBtcAddress());
|
tvTxBtcAddress.setText(order.getBtcAddress());
|
||||||
tvTxBtcAddressLabel.setText(getString(R.string.label_send_btc_address, txDataBtc.getBtcSymbol()));
|
tvTxBtcAddressLabel.setText(getString(R.string.label_send_btc_address, order.getBtcCurrency()));
|
||||||
|
Timber.d("onOrderCreated");
|
||||||
hideProgress();
|
hideProgress();
|
||||||
Timber.d("Expires @ %s", order.getExpiresAt().toString());
|
Timber.d("Expires @ %s", order.getExpiresAt().toString());
|
||||||
final int timeout = (int) (order.getExpiresAt().getTime() - order.getCreatedAt().getTime()) / 1000 - 60; // -1 minute buffer
|
final int timeout = (int) (order.getExpiresAt().getTime() - order.getCreatedAt().getTime()) / 1000 - 60; // -1 minute buffer
|
||||||
startSendTimer(timeout);
|
startSendTimer(timeout);
|
||||||
prepareSend();
|
prepareSend(order);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processStageBError(final Exception ex) {
|
public void onOrderError(final Exception ex) {
|
||||||
Timber.e("processCreateOrderError %s", ex.getLocalizedMessage());
|
Timber.e("onOrderError %s", ex.getLocalizedMessage());
|
||||||
getView().post(() -> {
|
requireView().post(() -> {
|
||||||
if (ex instanceof ShiftException) {
|
if (ex instanceof ShiftException) {
|
||||||
ShiftException xmrEx = (ShiftException) ex;
|
ShiftException xmrEx = (ShiftException) ex;
|
||||||
ShiftError xmrErr = xmrEx.getError();
|
ShiftError xmrErr = xmrEx.getError();
|
||||||
if (xmrErr != null) {
|
if (xmrErr != null) {
|
||||||
if (xmrErr.isRetryable()) {
|
if (xmrErr.isRetryable()) {
|
||||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
|
||||||
getString(R.string.text_retry));
|
getString(R.string.text_retry));
|
||||||
evStageB.setOnClickListener(v -> {
|
evStageB.setOnClickListener(v -> {
|
||||||
evStageB.setOnClickListener(null);
|
evStageB.setOnClickListener(null);
|
||||||
stageB(xmrtoOrder.getOrderId());
|
shiftProcess.retryCreateOrder();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(), null);
|
||||||
getString(R.string.text_noretry));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showStageError(getString(R.string.label_generic_xmrto_error),
|
showErrorMessage(getString(R.string.label_generic_xmrto_error),
|
||||||
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
|
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
|
||||||
getString(R.string.text_noretry));
|
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
evStageB.showMessage(getString(R.string.label_generic_xmrto_error),
|
evStageB.showMessage(getString(R.string.label_generic_xmrto_error),
|
||||||
ex.getLocalizedMessage(),
|
ex.getLocalizedMessage(),
|
||||||
getString(R.string.text_noretry));
|
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stageB(final String quoteId) {
|
|
||||||
Timber.d("createOrder(%s)", quoteId);
|
|
||||||
if (!isResumed) return;
|
|
||||||
final String btcAddress = ((TxDataBtc) sendListener.getTxData()).getBtcAddress();
|
|
||||||
getView().post(() -> {
|
|
||||||
xmrtoOrder = null;
|
|
||||||
showProgress(2, getString(R.string.label_send_progress_xmrto_query));
|
|
||||||
getXmrToApi().createOrder(quoteId, btcAddress, new ShiftCallback<CreateOrder>() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(CreateOrder order) {
|
|
||||||
if (!isResumed) return;
|
|
||||||
if (xmrtoQuote == null) return;
|
|
||||||
if (!order.getQuoteId().equals(xmrtoQuote.getId())) {
|
|
||||||
Timber.d("Quote ID does not match");
|
|
||||||
// ignore (we got a response to a stale request)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (xmrtoOrder != null)
|
|
||||||
throw new IllegalStateException("xmrtoOrder must be null here!");
|
|
||||||
processStageB(order);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Exception ex) {
|
public void invalidateShift() {
|
||||||
if (!isResumed) return;
|
orderId = null;
|
||||||
processStageBError(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private SideShiftApi xmrToApi = null;
|
|
||||||
|
|
||||||
private SideShiftApi getXmrToApi() {
|
|
||||||
if (xmrToApi == null) {
|
|
||||||
synchronized (this) {
|
|
||||||
if (xmrToApi == null) {
|
|
||||||
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return xmrToApi;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.fragment.send;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -32,16 +31,17 @@ import android.widget.Toast;
|
||||||
import com.m2049r.xmrwallet.R;
|
import com.m2049r.xmrwallet.R;
|
||||||
import com.m2049r.xmrwallet.data.Crypto;
|
import com.m2049r.xmrwallet.data.Crypto;
|
||||||
import com.m2049r.xmrwallet.data.PendingTx;
|
import com.m2049r.xmrwallet.data.PendingTx;
|
||||||
|
import com.m2049r.xmrwallet.data.TxData;
|
||||||
import com.m2049r.xmrwallet.data.TxDataBtc;
|
import com.m2049r.xmrwallet.data.TxDataBtc;
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
|
||||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@ -51,15 +51,11 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) {
|
public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) {
|
||||||
SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment();
|
SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment();
|
||||||
instance.setSendListener(listener);
|
instance.sendListener = listener;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
SendSuccessWizardFragment.Listener sendListener;
|
private SendSuccessWizardFragment.Listener sendListener;
|
||||||
|
|
||||||
public void setSendListener(SendSuccessWizardFragment.Listener listener) {
|
|
||||||
this.sendListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageButton bCopyTxId;
|
ImageButton bCopyTxId;
|
||||||
private TextView tvTxId;
|
private TextView tvTxId;
|
||||||
|
@ -80,6 +76,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
|
|
||||||
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
||||||
|
final ShiftService shiftService = getTxData().getShiftService();
|
||||||
|
|
||||||
View view = inflater.inflate(
|
View view = inflater.inflate(
|
||||||
R.layout.fragment_send_btc_success, container, false);
|
R.layout.fragment_send_btc_success, container, false);
|
||||||
|
@ -87,7 +84,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||||
bCopyTxId = view.findViewById(R.id.bCopyTxId);
|
bCopyTxId = view.findViewById(R.id.bCopyTxId);
|
||||||
bCopyTxId.setEnabled(false);
|
bCopyTxId.setEnabled(false);
|
||||||
bCopyTxId.setOnClickListener(v -> {
|
bCopyTxId.setOnClickListener(v -> {
|
||||||
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
|
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
|
||||||
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -105,23 +102,22 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||||
pbXmrto = view.findViewById(R.id.pbXmrto);
|
pbXmrto = view.findViewById(R.id.pbXmrto);
|
||||||
pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY);
|
pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||||
|
|
||||||
|
final TextView tvXmrToLabel = view.findViewById(R.id.tvXmrToLabel);
|
||||||
|
tvXmrToLabel.setText(getString(R.string.info_send_xmrto_success_order_label, shiftService.getLabel()));
|
||||||
|
|
||||||
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
|
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
|
||||||
tvTxXmrToKey.setOnClickListener(v -> {
|
tvTxXmrToKey.setOnClickListener(v -> {
|
||||||
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
|
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
|
||||||
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
|
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
|
||||||
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
||||||
|
tvXmrToSupport.setText(getString(R.string.label_send_btc_xmrto_info, shiftService.getLabel()));
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onValidateFields() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isResumed = false;
|
private boolean isResumed = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -130,7 +126,15 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||||
super.onPauseFragment();
|
super.onPauseFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
TxDataBtc btcData = null;
|
private TxDataBtc getTxData() {
|
||||||
|
final TxData txData = sendListener.getTxData();
|
||||||
|
if (txData == null) throw new IllegalStateException("TxDataBtc is null");
|
||||||
|
if (txData instanceof TxDataBtc) {
|
||||||
|
return (TxDataBtc) txData;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("TxData not BTC");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResumeFragment() {
|
public void onResumeFragment() {
|
||||||
|
@ -139,8 +143,8 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||||
Helper.hideKeyboard(getActivity());
|
Helper.hideKeyboard(getActivity());
|
||||||
isResumed = true;
|
isResumed = true;
|
||||||
|
|
||||||
btcData = (TxDataBtc) sendListener.getTxData();
|
final TxDataBtc txData = getTxData();
|
||||||
tvTxAddress.setText(btcData.getDestination());
|
tvTxAddress.setText(txData.getDestination());
|
||||||
|
|
||||||
final PendingTx committedTx = sendListener.getCommittedTx();
|
final PendingTx committedTx = sendListener.getCommittedTx();
|
||||||
if (committedTx != null) {
|
if (committedTx != null) {
|
||||||
|
@ -148,45 +152,41 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||||
bCopyTxId.setEnabled(true);
|
bCopyTxId.setEnabled(true);
|
||||||
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
|
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
|
||||||
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
|
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
|
||||||
if (btcData != null) {
|
|
||||||
NumberFormat df = NumberFormat.getInstance(Locale.US);
|
NumberFormat df = NumberFormat.getInstance(Locale.US);
|
||||||
df.setMaximumFractionDigits(12);
|
df.setMaximumFractionDigits(12);
|
||||||
String btcAmount = df.format(btcData.getBtcAmount());
|
String btcAmount = df.format(txData.getBtcAmount());
|
||||||
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, btcData.getBtcSymbol()));
|
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, txData.getBtcSymbol()));
|
||||||
//TODO btcData.getBtcAddress();
|
//TODO btcData.getBtcAddress();
|
||||||
tvTxXmrToKey.setText(btcData.getXmrtoOrderId());
|
tvTxXmrToKey.setText(txData.getXmrtoOrderId());
|
||||||
final Crypto crypto = Crypto.withSymbol(btcData.getBtcSymbol());
|
final Crypto crypto = Crypto.withSymbol(txData.getBtcSymbol());
|
||||||
|
assert crypto != null;
|
||||||
ivXmrToIcon.setImageResource(crypto.getIconEnabledId());
|
ivXmrToIcon.setImageResource(crypto.getIconEnabledId());
|
||||||
tvXmrToSupport.setOnClickListener(v -> {
|
tvXmrToSupport.setOnClickListener(v -> {
|
||||||
Uri orderUri = getXmrToApi().getQueryOrderUri(btcData.getXmrtoOrderId());
|
startActivity(new Intent(Intent.ACTION_VIEW, txData.getShiftService().getShiftApi().getQueryOrderUri(txData.getXmrtoOrderId())));
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW, orderUri);
|
|
||||||
startActivity(intent);
|
|
||||||
});
|
});
|
||||||
queryOrder();
|
queryOrder();
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("btcData is null");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sendListener.enableDone();
|
sendListener.enableDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processQueryOrder(final QueryOrderStatus status) {
|
private void processQueryOrder(final QueryOrderStatus status) {
|
||||||
Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getOrderId());
|
Timber.d("processQueryOrder %s for %s", status.getStatus().toString(), status.getOrderId());
|
||||||
if (!btcData.getXmrtoOrderId().equals(status.getOrderId()))
|
if (!getTxData().getXmrtoOrderId().equals(status.getOrderId()))
|
||||||
throw new IllegalStateException("UUIDs do not match!");
|
throw new IllegalStateException("UUIDs do not match!");
|
||||||
if (isResumed && (getView() != null))
|
if (isResumed && (getView() != null))
|
||||||
getView().post(() -> {
|
getView().post(() -> {
|
||||||
showXmrToStatus(status);
|
showXmrToStatus(status);
|
||||||
if (!status.isTerminal()) {
|
if (!status.isTerminal()) {
|
||||||
getView().postDelayed(this::queryOrder, SideShiftApi.QUERY_INTERVAL);
|
getView().postDelayed(this::queryOrder, ShiftApi.QUERY_INTERVAL);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void queryOrder() {
|
private void queryOrder() {
|
||||||
|
final TxDataBtc btcData = getTxData();
|
||||||
Timber.d("queryOrder(%s)", btcData.getXmrtoOrderId());
|
Timber.d("queryOrder(%s)", btcData.getXmrtoOrderId());
|
||||||
if (!isResumed) return;
|
if (!isResumed) return;
|
||||||
getXmrToApi().queryOrderStatus(btcData.getXmrtoOrderId(), new ShiftCallback<QueryOrderStatus>() {
|
btcData.getShiftService().getShiftApi().queryOrderStatus(btcData.getXmrtoQueryOrderToken(), new ShiftCallback<QueryOrderStatus>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(QueryOrderStatus status) {
|
public void onSuccess(QueryOrderStatus status) {
|
||||||
if (!isAdded()) return;
|
if (!isAdded()) return;
|
||||||
|
@ -197,45 +197,54 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||||
public void onError(final Exception ex) {
|
public void onError(final Exception ex) {
|
||||||
if (!isResumed) return;
|
if (!isResumed) return;
|
||||||
Timber.w(ex);
|
Timber.w(ex);
|
||||||
getActivity().runOnUiThread(() -> {
|
if (ex instanceof SocketTimeoutException) {
|
||||||
|
// try again
|
||||||
|
if (isResumed && (getView() != null))
|
||||||
|
getView().post(() -> {
|
||||||
|
getView().postDelayed(SendBtcSuccessWizardFragment.this::queryOrder, ShiftApi.QUERY_INTERVAL);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
requireActivity().runOnUiThread(() -> {
|
||||||
if (ex instanceof ShiftException) {
|
if (ex instanceof ShiftException) {
|
||||||
Toast.makeText(getActivity(), ((ShiftException) ex).getError().getErrorMsg(), Toast.LENGTH_LONG).show();
|
Toast.makeText(getActivity(), ((ShiftException) ex).getErrorMessage(), Toast.LENGTH_LONG).show();
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
|
Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void showXmrToStatus(final QueryOrderStatus status) {
|
void showXmrToStatus(final QueryOrderStatus status) {
|
||||||
int statusResource = 0;
|
int statusResource = 0;
|
||||||
|
final TxDataBtc txData = getTxData();
|
||||||
if (status.isError()) {
|
if (status.isError()) {
|
||||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, status.toString()));
|
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, txData.getShiftService().getLabel(), status.toString()));
|
||||||
statusResource = R.drawable.ic_error_red_24dp;
|
statusResource = R.drawable.ic_error_red_24dp;
|
||||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||||
ThemeHelper.getThemedColor(getContext(), android.R.attr.colorError),
|
ThemeHelper.getThemedColor(requireContext(), R.attr.negativeColor),
|
||||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||||
} else if (status.isSent() || status.isPaid()) {
|
} else if (status.isSent() || status.isPaid()) {
|
||||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, btcData.getBtcSymbol()));
|
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, txData.getBtcSymbol()));
|
||||||
statusResource = R.drawable.ic_success;
|
statusResource = R.drawable.ic_success;
|
||||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||||
ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor),
|
ThemeHelper.getThemedColor(requireContext(), R.attr.positiveColor),
|
||||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||||
} else if (status.isWaiting()) {
|
} else if (status.isWaiting()) {
|
||||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid));
|
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid));
|
||||||
statusResource = R.drawable.ic_pending;
|
statusResource = R.drawable.ic_pending;
|
||||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||||
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
|
ThemeHelper.getThemedColor(requireContext(), R.attr.neutralColor),
|
||||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||||
} else if (status.isPending()) {
|
} else if (status.isPending()) {
|
||||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid));
|
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid));
|
||||||
statusResource = R.drawable.ic_pending;
|
statusResource = R.drawable.ic_pending;
|
||||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||||
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
|
ThemeHelper.getThemedColor(requireContext(), R.attr.neutralColor),
|
||||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("status is broken: " + status.toString());
|
throw new IllegalStateException("status is broken: " + status);
|
||||||
}
|
}
|
||||||
ivXmrToStatus.setImageResource(statusResource);
|
ivXmrToStatus.setImageResource(statusResource);
|
||||||
if (status.isTerminal()) {
|
if (status.isTerminal()) {
|
||||||
|
@ -246,17 +255,4 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||||
ivXmrToStatusBig.setVisibility(View.VISIBLE);
|
ivXmrToStatusBig.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SideShiftApi xmrToApi = null;
|
|
||||||
|
|
||||||
private SideShiftApi getXmrToApi() {
|
|
||||||
if (xmrToApi == null) {
|
|
||||||
synchronized (this) {
|
|
||||||
if (xmrToApi == null) {
|
|
||||||
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return xmrToApi;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,18 +38,13 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
|
||||||
|
|
||||||
public static SendConfirmWizardFragment newInstance(Listener listener) {
|
public static SendConfirmWizardFragment newInstance(Listener listener) {
|
||||||
SendConfirmWizardFragment instance = new SendConfirmWizardFragment();
|
SendConfirmWizardFragment instance = new SendConfirmWizardFragment();
|
||||||
instance.setSendListener(listener);
|
instance.sendListener = listener;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Listener sendListener;
|
private Listener sendListener;
|
||||||
|
|
||||||
public SendConfirmWizardFragment setSendListener(Listener listener) {
|
public interface Listener {
|
||||||
this.sendListener = listener;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Listener {
|
|
||||||
SendFragment.Listener getActivityCallback();
|
SendFragment.Listener getActivityCallback();
|
||||||
|
|
||||||
TxData getTxData();
|
TxData getTxData();
|
||||||
|
@ -137,7 +132,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
|
||||||
|
|
||||||
void send() {
|
void send() {
|
||||||
sendListener.commitTransaction();
|
sendListener.commitTransaction();
|
||||||
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
|
requireActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -153,7 +148,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showAlert(String title, String message) {
|
private void showAlert(String title, String message) {
|
||||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(getActivity());
|
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
|
||||||
builder.setCancelable(true).
|
builder.setCancelable(true).
|
||||||
setTitle(title).
|
setTitle(title).
|
||||||
setMessage(message).
|
setMessage(message).
|
||||||
|
@ -235,7 +230,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
|
||||||
}
|
}
|
||||||
|
|
||||||
public void fail(String walletName) {
|
public void fail(String walletName) {
|
||||||
getActivity().runOnUiThread(() -> {
|
requireActivity().runOnUiThread(() -> {
|
||||||
bSend.setEnabled(true); // allow to try again
|
bSend.setEnabled(true); // allow to try again
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ import com.m2049r.xmrwallet.widget.Toolbar;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class SendFragment extends Fragment
|
public class SendFragment extends Fragment
|
||||||
|
@ -67,6 +68,7 @@ public class SendFragment extends Fragment
|
||||||
|
|
||||||
final static public int MIXIN = 0;
|
final static public int MIXIN = 0;
|
||||||
|
|
||||||
|
@Getter
|
||||||
private Listener activityCallback;
|
private Listener activityCallback;
|
||||||
|
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
|
@ -273,7 +275,7 @@ public class SendFragment extends Fragment
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Mode {
|
public enum Mode {
|
||||||
XMR, BTC
|
XMR, BTC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +295,7 @@ public class SendFragment extends Fragment
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!");
|
throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!");
|
||||||
}
|
}
|
||||||
getView().post(() -> pagerAdapter.notifyDataSetChanged());
|
requireView().post(() -> pagerAdapter.notifyDataSetChanged());
|
||||||
Timber.d("New Mode = %s", mode.toString());
|
Timber.d("New Mode = %s", mode.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -478,11 +480,6 @@ public class SendFragment extends Fragment
|
||||||
bDone.setVisibility(View.VISIBLE);
|
bDone.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Listener getActivityCallback() {
|
|
||||||
return activityCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// callbacks from send service
|
// callbacks from send service
|
||||||
|
|
||||||
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
|
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
|
||||||
|
@ -502,12 +499,9 @@ public class SendFragment extends Fragment
|
||||||
activityCallback.onDisposeRequest();
|
activityCallback.onDisposeRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
PendingTx pendingTx;
|
PendingTx pendingTx;
|
||||||
|
|
||||||
public PendingTx getPendingTx() {
|
|
||||||
return pendingTx;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCreateTransactionFailed(String errorText) {
|
public void onCreateTransactionFailed(String errorText) {
|
||||||
final SendConfirm confirm = getSendConfirm();
|
final SendConfirm confirm = getSendConfirm();
|
||||||
if (confirm != null) {
|
if (confirm != null) {
|
||||||
|
|
|
@ -36,18 +36,13 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
|
||||||
|
|
||||||
public static SendSuccessWizardFragment newInstance(Listener listener) {
|
public static SendSuccessWizardFragment newInstance(Listener listener) {
|
||||||
SendSuccessWizardFragment instance = new SendSuccessWizardFragment();
|
SendSuccessWizardFragment instance = new SendSuccessWizardFragment();
|
||||||
instance.setSendListener(listener);
|
instance.sendListener = listener;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Listener sendListener;
|
private Listener sendListener;
|
||||||
|
|
||||||
public SendSuccessWizardFragment setSendListener(Listener listener) {
|
public interface Listener {
|
||||||
this.sendListener = listener;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Listener {
|
|
||||||
TxData getTxData();
|
TxData getTxData();
|
||||||
|
|
||||||
PendingTx getCommittedTx();
|
PendingTx getCommittedTx();
|
||||||
|
@ -62,7 +57,6 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
|
||||||
ImageButton bCopyTxId;
|
ImageButton bCopyTxId;
|
||||||
private TextView tvTxId;
|
private TextView tvTxId;
|
||||||
private TextView tvTxAddress;
|
private TextView tvTxAddress;
|
||||||
private TextView tvTxPaymentId;
|
|
||||||
private TextView tvTxAmount;
|
private TextView tvTxAmount;
|
||||||
private TextView tvTxFee;
|
private TextView tvTxFee;
|
||||||
|
|
||||||
|
@ -78,13 +72,12 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
|
||||||
bCopyTxId = view.findViewById(R.id.bCopyTxId);
|
bCopyTxId = view.findViewById(R.id.bCopyTxId);
|
||||||
bCopyTxId.setEnabled(false);
|
bCopyTxId.setEnabled(false);
|
||||||
bCopyTxId.setOnClickListener(v -> {
|
bCopyTxId.setOnClickListener(v -> {
|
||||||
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
|
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
|
||||||
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
tvTxId = view.findViewById(R.id.tvTxId);
|
tvTxId = view.findViewById(R.id.tvTxId);
|
||||||
tvTxAddress = view.findViewById(R.id.tvTxAddress);
|
tvTxAddress = view.findViewById(R.id.tvTxAddress);
|
||||||
tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId);
|
|
||||||
tvTxAmount = view.findViewById(R.id.tvTxAmount);
|
tvTxAmount = view.findViewById(R.id.tvTxAmount);
|
||||||
tvTxFee = view.findViewById(R.id.tvTxFee);
|
tvTxFee = view.findViewById(R.id.tvTxFee);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.m2049r.xmrwallet.fragment.send;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
|
||||||
|
|
||||||
|
public interface Shifter {
|
||||||
|
void invalidateShift();
|
||||||
|
|
||||||
|
void onQuoteError(final Exception ex);
|
||||||
|
|
||||||
|
void showQuoteError();
|
||||||
|
|
||||||
|
void onQuoteReceived(RequestQuote quote);
|
||||||
|
|
||||||
|
void onOrderCreated(CreateOrder order);
|
||||||
|
|
||||||
|
void onOrderError(final Exception ex);
|
||||||
|
|
||||||
|
boolean isActive();
|
||||||
|
|
||||||
|
void showProgress(Shifter.Stage stage);
|
||||||
|
|
||||||
|
enum Stage {
|
||||||
|
X, A, B, C
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018 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 com.m2049r.xmrwallet.layout;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.R;
|
||||||
|
import com.m2049r.xmrwallet.data.BluetoothInfo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BluetoothInfoAdapter extends RecyclerView.Adapter<BluetoothInfoAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
public interface OnInteractionListener {
|
||||||
|
void onInteraction(View view, BluetoothInfo item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<BluetoothInfo> items = new ArrayList<>();
|
||||||
|
private final OnInteractionListener listener;
|
||||||
|
|
||||||
|
public BluetoothInfoAdapter(OnInteractionListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class BluetoothInfoDiff extends DiffCallback<BluetoothInfo> {
|
||||||
|
|
||||||
|
public BluetoothInfoDiff(List<BluetoothInfo> oldList, List<BluetoothInfo> newList) {
|
||||||
|
super(oldList, newList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||||
|
return mOldList.get(oldItemPosition).getAddress().equals(mNewList.get(newItemPosition).getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||||
|
final BluetoothInfo oldItem = mOldList.get(oldItemPosition);
|
||||||
|
final BluetoothInfo newItem = mNewList.get(newItemPosition);
|
||||||
|
return oldItem.equals(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull
|
||||||
|
ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bluetooth, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(final @NonNull ViewHolder holder, int position) {
|
||||||
|
holder.bind(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return items.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(BluetoothInfo item) {
|
||||||
|
if (item == null) return;
|
||||||
|
List<BluetoothInfo> newItems = new ArrayList<>(items);
|
||||||
|
if (!items.contains(item))
|
||||||
|
newItems.add(item);
|
||||||
|
setItems(newItems); // in case the nodeinfo has changed
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItems(Collection<BluetoothInfo> newItemsCollection) {
|
||||||
|
List<BluetoothInfo> newItems;
|
||||||
|
if (newItemsCollection != null) {
|
||||||
|
newItems = new ArrayList<>(newItemsCollection);
|
||||||
|
Collections.sort(newItems, BluetoothInfo.NameComparator);
|
||||||
|
} else {
|
||||||
|
newItems = new ArrayList<>();
|
||||||
|
}
|
||||||
|
final BluetoothInfoAdapter.BluetoothInfoDiff diffCallback = new BluetoothInfoAdapter.BluetoothInfoDiff(items, newItems);
|
||||||
|
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
|
||||||
|
items.clear();
|
||||||
|
items.addAll(newItems);
|
||||||
|
diffResult.dispatchUpdatesTo(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean itemsClickable = true;
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
public void allowClick(boolean clickable) {
|
||||||
|
itemsClickable = clickable;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||||
|
final TextView tvName;
|
||||||
|
final TextView tvAddress;
|
||||||
|
BluetoothInfo item;
|
||||||
|
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
tvName = itemView.findViewById(R.id.tvName);
|
||||||
|
tvAddress = itemView.findViewById(R.id.tvAddress);
|
||||||
|
itemView.setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind(int position) {
|
||||||
|
item = items.get(position);
|
||||||
|
tvName.setText(item.getName());
|
||||||
|
tvAddress.setText(item.getAddress());
|
||||||
|
itemView.setClickable(itemsClickable);
|
||||||
|
itemView.setEnabled(itemsClickable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
if (listener != null) {
|
||||||
|
int position = getBindingAdapterPosition(); // 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 BluetoothInfo node = items.get(position);
|
||||||
|
allowClick(false);
|
||||||
|
listener.onInteraction(view, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.layout;
|
package com.m2049r.xmrwallet.layout;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -131,12 +132,13 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||||
|
|
||||||
private boolean itemsClickable = true;
|
private boolean itemsClickable = true;
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
public void allowClick(boolean clickable) {
|
public void allowClick(boolean clickable) {
|
||||||
itemsClickable = clickable;
|
itemsClickable = clickable;
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||||
final ImageButton ibBookmark;
|
final ImageButton ibBookmark;
|
||||||
final View pbBookmark;
|
final View pbBookmark;
|
||||||
final TextView tvName;
|
final TextView tvName;
|
||||||
|
@ -195,7 +197,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
int position = getAdapterPosition(); // gets item position
|
int position = getBindingAdapterPosition(); // gets item position
|
||||||
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
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);
|
final NodeInfo node = nodeItems.get(position);
|
||||||
if (node.isOnion()) {
|
if (node.isOnion()) {
|
||||||
|
@ -218,7 +220,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(View view) {
|
public boolean onLongClick(View view) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
int position = getAdapterPosition(); // gets item position
|
int position = getBindingAdapterPosition(); // gets item position
|
||||||
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
||||||
return listener.onLongInteraction(view, nodeItems.get(position));
|
return listener.onLongInteraction(view, nodeItems.get(position));
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
|
||||||
return items.size();
|
return items.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public Subaddress getItem(int position) {
|
public Subaddress getItem(int position) {
|
||||||
return items.get(position);
|
return items.get(position);
|
||||||
}
|
}
|
||||||
|
@ -108,7 +109,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
|
||||||
diffResult.dispatchUpdatesTo(this);
|
diffResult.dispatchUpdatesTo(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||||
final TextView tvName;
|
final TextView tvName;
|
||||||
final TextView tvAddress;
|
final TextView tvAddress;
|
||||||
final TextView tvAmount;
|
final TextView tvAmount;
|
||||||
|
@ -143,7 +144,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
int position = getAdapterPosition(); // gets item position
|
int position = getBindingAdapterPosition(); // gets item position
|
||||||
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
||||||
listener.onInteraction(view, getItem(position));
|
listener.onInteraction(view, getItem(position));
|
||||||
}
|
}
|
||||||
|
@ -153,7 +154,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(View view) {
|
public boolean onLongClick(View view) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
int position = getAdapterPosition(); // gets item position
|
int position = getBindingAdapterPosition(); // gets item position
|
||||||
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
||||||
return listener.onLongInteraction(view, getItem(position));
|
return listener.onLongInteraction(view, getItem(position));
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.layout;
|
package com.m2049r.xmrwallet.layout;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
@ -49,6 +50,7 @@ import java.util.TimeZone;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
|
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
private final static 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 outboundColour;
|
||||||
|
@ -86,7 +88,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean needsTransactionUpdateOnNewBlock() {
|
public boolean needsTransactionUpdateOnNewBlock() {
|
||||||
return (infoItems.size() > 0) && !infoItems.get(0).isConfirmed();
|
return (!infoItems.isEmpty()) && !infoItems.get(0).isConfirmed();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TransactionInfoDiff extends DiffCallback<TransactionInfo> {
|
private static class TransactionInfoDiff extends DiffCallback<TransactionInfo> {
|
||||||
|
@ -156,7 +158,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||||
return infoItems.get(position);
|
return infoItems.get(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||||
final ImageView ivTxType;
|
final ImageView ivTxType;
|
||||||
final TextView tvAmount;
|
final TextView tvAmount;
|
||||||
final TextView tvFailed;
|
final TextView tvFailed;
|
||||||
|
@ -291,7 +293,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
int position = getAdapterPosition(); // gets item position
|
int position = getBindingAdapterPosition(); // gets item position
|
||||||
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
||||||
listener.onInteraction(view, infoItems.get(position));
|
listener.onInteraction(view, infoItems.get(position));
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.layout;
|
package com.m2049r.xmrwallet.layout;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
@ -44,6 +45,7 @@ import timber.log.Timber;
|
||||||
|
|
||||||
public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.ViewHolder> {
|
public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||||
|
|
||||||
public interface OnInteractionListener {
|
public interface OnInteractionListener {
|
||||||
|
@ -120,7 +122,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
|
||||||
diffResult.dispatchUpdatesTo(this);
|
diffResult.dispatchUpdatesTo(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||||
final TextView tvName;
|
final TextView tvName;
|
||||||
final ImageButton ibOptions;
|
final ImageButton ibOptions;
|
||||||
WalletManager.WalletInfo infoItem;
|
WalletManager.WalletInfo infoItem;
|
||||||
|
@ -164,7 +166,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
int position = getAdapterPosition(); // gets item position
|
int position = getBindingAdapterPosition(); // gets item position
|
||||||
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
|
||||||
listener.onInteraction(view, infoItems.get(position));
|
listener.onInteraction(view, infoItems.get(position));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
package com.m2049r.xmrwallet.ledger;
|
||||||
|
|
||||||
|
public interface Hardware {
|
||||||
|
}
|
|
@ -35,12 +35,11 @@ import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class Ledger {
|
public class Ledger implements Hardware {
|
||||||
static final public boolean ENABLED = true;
|
static final public boolean ENABLED = true;
|
||||||
// 5:20 is same as wallet2.cpp::restore()
|
// 5:20 is same as wallet2.cpp::restore()
|
||||||
static public final int LOOKAHEAD_ACCOUNTS = 5;
|
static public final int LOOKAHEAD_ACCOUNTS = 5;
|
||||||
static public final int LOOKAHEAD_SUBADDRESSES = 20;
|
static public final int LOOKAHEAD_SUBADDRESSES = 20;
|
||||||
static public final String SUBADDRESS_LOOKAHEAD = LOOKAHEAD_ACCOUNTS + ":" + LOOKAHEAD_SUBADDRESSES;
|
|
||||||
|
|
||||||
private static final byte PROTOCOL_VERSION = 0x03;
|
private static final byte PROTOCOL_VERSION = 0x03;
|
||||||
public static final int SW_OK = 0x9000;
|
public static final int SW_OK = 0x9000;
|
||||||
|
|
|
@ -111,7 +111,11 @@ public class Wallet {
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
public enum Device {
|
public enum Device {
|
||||||
Device_Undefined(0, 0), Device_Software(50, 200), Device_Ledger(5, 20);
|
Undefined(0, 0),
|
||||||
|
Software(50, 200),
|
||||||
|
Ledger(5, 20),
|
||||||
|
Trezor(5, 20),
|
||||||
|
Sidekick(5, 20);
|
||||||
private final int accountLookahead;
|
private final int accountLookahead;
|
||||||
private final int subaddressLookahead;
|
private final int subaddressLookahead;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.model;
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.XmrWalletApplication;
|
import com.m2049r.xmrwallet.XmrWalletApplication;
|
||||||
import com.m2049r.xmrwallet.data.Node;
|
import com.m2049r.xmrwallet.data.Node;
|
||||||
import com.m2049r.xmrwallet.ledger.Ledger;
|
|
||||||
import com.m2049r.xmrwallet.util.RestoreHeight;
|
import com.m2049r.xmrwallet.util.RestoreHeight;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -161,10 +160,12 @@ public class WalletManager {
|
||||||
String spendKeyString);
|
String spendKeyString);
|
||||||
|
|
||||||
public Wallet createWalletFromDevice(File aFile, String password, long restoreHeight,
|
public Wallet createWalletFromDevice(File aFile, String password, long restoreHeight,
|
||||||
String deviceName) {
|
Wallet.Device device) {
|
||||||
|
final String lookahead = device.getAccountLookahead() + ":" + device.getSubaddressLookahead();
|
||||||
|
Timber.d("Creating from %s with %s lookahead", device, lookahead);
|
||||||
long walletHandle = createWalletFromDeviceJ(aFile.getAbsolutePath(), password,
|
long walletHandle = createWalletFromDeviceJ(aFile.getAbsolutePath(), password,
|
||||||
getNetworkType().getValue(), deviceName, restoreHeight,
|
getNetworkType().getValue(), device.name(), restoreHeight,
|
||||||
Ledger.SUBADDRESS_LOOKAHEAD);
|
lookahead);
|
||||||
Wallet wallet = new Wallet(walletHandle);
|
Wallet wallet = new Wallet(walletHandle);
|
||||||
manageWallet(wallet);
|
manageWallet(wallet);
|
||||||
return wallet;
|
return wallet;
|
||||||
|
|
|
@ -19,6 +19,8 @@ package com.m2049r.xmrwallet.onboarding;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
|
@ -0,0 +1,662 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 m2049r@monerujo.io
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// mostly from BluetoothChatService https://github.com/android/connectivity-samples
|
||||||
|
|
||||||
|
package com.m2049r.xmrwallet.service;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothServerSocket;
|
||||||
|
import android.bluetooth.BluetoothSocket;
|
||||||
|
import android.os.Handler;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class does all the work for setting up and managing Bluetooth
|
||||||
|
* connections with other devices. It has a thread that listens for
|
||||||
|
* incoming connections, a thread for connecting with a device, and a
|
||||||
|
* thread for performing data transmissions when connected.
|
||||||
|
*/
|
||||||
|
public class BluetoothService {
|
||||||
|
final static private byte[] MAGIC = "SIDEKICK".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
|
||||||
|
public interface MessageType {
|
||||||
|
int STATE_CHANGE = 1;
|
||||||
|
int READ = 2;
|
||||||
|
int WRITE = 3;
|
||||||
|
int DEVICE_NAME = 4;
|
||||||
|
int TOAST = 5;
|
||||||
|
int READ_CMD = 6;
|
||||||
|
int CODE = 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO refactor this using an enum with resource ids for messages and stuff
|
||||||
|
public interface Toasts {
|
||||||
|
String CONNECT_FAILED = "Unable to connect device";
|
||||||
|
String CONNECTION_LOST = "Device connection was lost";
|
||||||
|
int READ = 2;
|
||||||
|
int WRITE = 3;
|
||||||
|
int DEVICE_NAME = 4;
|
||||||
|
int TOAST = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants that indicate the current connection state
|
||||||
|
public interface State {
|
||||||
|
int NONE = 0; // we're doing nothing
|
||||||
|
int LISTEN = 1; // now listening for incoming connections
|
||||||
|
int CONNECTING = 2; // now initiating an outgoing connection
|
||||||
|
int CONNECTED = 3; // now connected to a remote device
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name for the SDP record when creating server socket
|
||||||
|
private static final String SDP_NAME = "Monerujo";
|
||||||
|
|
||||||
|
// Unique UUID for this application
|
||||||
|
private static final UUID SDP_UUID = UUID.fromString("2150154b-58ce-4c58-99e3-ccfdd14bed3b");
|
||||||
|
|
||||||
|
static final BluetoothService Instance = new BluetoothService();
|
||||||
|
|
||||||
|
public static BluetoothService GetInstance() {
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean IsConnected() {
|
||||||
|
return Instance.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Stop() {
|
||||||
|
Instance.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member fields
|
||||||
|
private final BluetoothAdapter bluetoothAdapter;
|
||||||
|
@Setter
|
||||||
|
private Handler uiHandler;
|
||||||
|
@Setter
|
||||||
|
private Handler commHandler;
|
||||||
|
private AcceptThread acceptThread;
|
||||||
|
private ConnectThread connectThread;
|
||||||
|
private ConnectedThread connectedThread;
|
||||||
|
@Getter
|
||||||
|
private int state = State.NONE;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private String connectedName;
|
||||||
|
@Getter
|
||||||
|
private String connectedCode;
|
||||||
|
|
||||||
|
public boolean isStarted() {
|
||||||
|
return state != State.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConnected() {
|
||||||
|
return state == State.CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BluetoothService() {
|
||||||
|
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify UI of state changes
|
||||||
|
*/
|
||||||
|
private synchronized void onStateChanged() {
|
||||||
|
Timber.d("onStateChanged() -> %s", state);
|
||||||
|
if (uiHandler != null)
|
||||||
|
uiHandler.obtainMessage(MessageType.STATE_CHANGE, state, -1).sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify UI that we've connected
|
||||||
|
*/
|
||||||
|
private synchronized void onConnected(String remoteName) {
|
||||||
|
Timber.d("onConnected() -> %s", remoteName);
|
||||||
|
connectedName = remoteName;
|
||||||
|
if (uiHandler != null)
|
||||||
|
uiHandler.obtainMessage(MessageType.DEVICE_NAME, remoteName).sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the service: Start AcceptThread to begin a session in listening (server) mode.
|
||||||
|
*/
|
||||||
|
public synchronized void start() {
|
||||||
|
Timber.d("start");
|
||||||
|
halt = false;
|
||||||
|
|
||||||
|
// Cancel any thread attempting to make a connection
|
||||||
|
if (connectThread != null) {
|
||||||
|
connectThread.cancel();
|
||||||
|
connectThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any thread currently running a connection
|
||||||
|
if (connectedThread != null) {
|
||||||
|
connectedThread.cancel();
|
||||||
|
connectedThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the thread to listen on a BluetoothServerSocket (or let it run if already running)
|
||||||
|
if (acceptThread == null) {
|
||||||
|
acceptThread = new AcceptThread();
|
||||||
|
acceptThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
onStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the ConnectThread to initiate a connection to a remote device.
|
||||||
|
*
|
||||||
|
* @param device The BluetoothDevice to connect
|
||||||
|
*/
|
||||||
|
public synchronized void connect(BluetoothDevice device) {
|
||||||
|
Timber.d("connect to: %s", device);
|
||||||
|
|
||||||
|
// Cancel any thread attempting to make a connection
|
||||||
|
if (state == State.CONNECTING) {
|
||||||
|
if (connectThread != null) {
|
||||||
|
connectThread.cancel();
|
||||||
|
connectThread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any thread currently running a connection
|
||||||
|
if (connectedThread != null) {
|
||||||
|
reconnect = true;
|
||||||
|
connectedThread.cancel();
|
||||||
|
connectedThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the thread to connect with the given device
|
||||||
|
connectThread = new ConnectThread(device);
|
||||||
|
connectThread.start();
|
||||||
|
|
||||||
|
onStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean reconnect = false;
|
||||||
|
boolean halt = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the ConnectedThread to begin managing a Bluetooth connection
|
||||||
|
*
|
||||||
|
* @param socket The BluetoothSocket on which the connection was made
|
||||||
|
* @param device The BluetoothDevice that has been connected
|
||||||
|
*/
|
||||||
|
public synchronized void startConnected(BluetoothSocket socket, BluetoothDevice device) {
|
||||||
|
Timber.d("startConnected");
|
||||||
|
|
||||||
|
// Cancel the thread that completed the connection
|
||||||
|
if (connectThread != null) {
|
||||||
|
connectThread.cancel();
|
||||||
|
connectThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any thread currently running a connection
|
||||||
|
if (connectedThread != null) {
|
||||||
|
connectedThread.cancel();
|
||||||
|
connectedThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the accept thread because we only want to connect to one device
|
||||||
|
if (acceptThread != null) {
|
||||||
|
acceptThread.cancel();
|
||||||
|
acceptThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the thread to manage the connection and perform transmissions
|
||||||
|
connectedThread = new ConnectedThread(socket);
|
||||||
|
connectedThread.start();
|
||||||
|
|
||||||
|
onConnected(device.getName());
|
||||||
|
onStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all threads
|
||||||
|
*/
|
||||||
|
public synchronized void stop() {
|
||||||
|
Timber.d("stop");
|
||||||
|
halt = true;
|
||||||
|
|
||||||
|
if (connectThread != null) {
|
||||||
|
connectThread.cancel();
|
||||||
|
connectThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectedThread != null) {
|
||||||
|
connectedThread.cancel();
|
||||||
|
connectedThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptThread != null) {
|
||||||
|
acceptThread.cancel();
|
||||||
|
acceptThread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = State.NONE;
|
||||||
|
|
||||||
|
onStateChanged();
|
||||||
|
|
||||||
|
setUiHandler(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write to the ConnectedThread in an unsynchronized manner
|
||||||
|
*
|
||||||
|
* @param out The bytes to write
|
||||||
|
* @see ConnectedThread#write(byte[])
|
||||||
|
*/
|
||||||
|
public boolean write(byte[] out) {
|
||||||
|
// Synchronize a copy of the ConnectedThread
|
||||||
|
ConnectedThread connectedThread;
|
||||||
|
synchronized (this) {
|
||||||
|
if (state != State.CONNECTED) return false;
|
||||||
|
connectedThread = this.connectedThread;
|
||||||
|
}
|
||||||
|
// Perform the write itself unsynchronized
|
||||||
|
return connectedThread.write(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean write(int code) {
|
||||||
|
// Synchronize a copy of the ConnectedThread
|
||||||
|
ConnectedThread connectedThread;
|
||||||
|
synchronized (this) {
|
||||||
|
if (state != State.CONNECTED) return false;
|
||||||
|
connectedThread = this.connectedThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] buffer = new byte[2];
|
||||||
|
buffer[0] = (byte) (code >> 8);
|
||||||
|
buffer[1] = (byte) (code & 0xff);
|
||||||
|
|
||||||
|
connectedCode = String.format(Locale.US, "%04d", code);
|
||||||
|
if (uiHandler != null)
|
||||||
|
uiHandler.obtainMessage(MessageType.CODE, connectedCode).sendToTarget();
|
||||||
|
|
||||||
|
return connectedThread.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] exchange(byte[] buffer) {
|
||||||
|
// Synchronize a copy of the ConnectedThread
|
||||||
|
ConnectedThread connectedThread;
|
||||||
|
synchronized (this) {
|
||||||
|
if (state != State.CONNECTED) return null; //TODO maybe exception?
|
||||||
|
connectedThread = this.connectedThread;
|
||||||
|
}
|
||||||
|
CountDownLatch signal = new CountDownLatch(1);
|
||||||
|
connectedThread.setReadSignal(signal);
|
||||||
|
connectedThread.write(buffer);
|
||||||
|
try {
|
||||||
|
signal.await(); //TODO what happens when the reader is canceled?
|
||||||
|
return connectedThread.getReadBuffer();
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Timber.d(ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the connection attempt failed and notify
|
||||||
|
*/
|
||||||
|
private void onConnectFailed() {
|
||||||
|
Timber.d("onConnectFailed()");
|
||||||
|
if (uiHandler != null)
|
||||||
|
uiHandler.obtainMessage(MessageType.TOAST, Toasts.CONNECT_FAILED).sendToTarget();
|
||||||
|
|
||||||
|
state = State.NONE;
|
||||||
|
|
||||||
|
// don't notify as start() notifies immediately afterwards
|
||||||
|
// onStateChanged();
|
||||||
|
|
||||||
|
// Start the service over to restart listening mode
|
||||||
|
if (!halt) start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the connection was lost
|
||||||
|
*/
|
||||||
|
private void onConnectionLost() {
|
||||||
|
Timber.d("onConnectionLost()");
|
||||||
|
connectedName = null;
|
||||||
|
connectedCode = null;
|
||||||
|
if (reconnect) return;
|
||||||
|
|
||||||
|
state = State.NONE;
|
||||||
|
|
||||||
|
if (halt) return;
|
||||||
|
|
||||||
|
// don't notify as start() notifies immediately afterwards
|
||||||
|
// onStateChanged();
|
||||||
|
|
||||||
|
// Start the service over to restart listening mode
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This thread runs while listening for incoming connections. It behaves
|
||||||
|
* like a server-side client. It runs until a connection is accepted
|
||||||
|
* (or until cancelled).
|
||||||
|
*/
|
||||||
|
private class AcceptThread extends Thread {
|
||||||
|
private final BluetoothServerSocket serverSocket;
|
||||||
|
|
||||||
|
public AcceptThread() {
|
||||||
|
// Create a new listening server socket
|
||||||
|
try {
|
||||||
|
serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord(SDP_NAME, SDP_UUID);
|
||||||
|
state = BluetoothService.State.LISTEN;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "listen() failed");
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
Timber.d("BEGIN AcceptThread %s", this);
|
||||||
|
|
||||||
|
BluetoothSocket socket;
|
||||||
|
|
||||||
|
// Listen to the server socket if we're not connected
|
||||||
|
while (state != BluetoothService.State.CONNECTED) {
|
||||||
|
try {
|
||||||
|
// This is a blocking call and will only return on a
|
||||||
|
// successful connection or an exception
|
||||||
|
socket = serverSocket.accept();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "accept() failed"); // this also happens on socket.close()
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a connection was accepted
|
||||||
|
if (socket != null) {
|
||||||
|
synchronized (BluetoothService.this) {
|
||||||
|
switch (state) {
|
||||||
|
case BluetoothService.State.LISTEN:
|
||||||
|
case BluetoothService.State.CONNECTING:
|
||||||
|
// Situation normal. Start the ConnectedThread.
|
||||||
|
startConnected(socket, socket.getRemoteDevice());
|
||||||
|
break;
|
||||||
|
case BluetoothService.State.NONE:
|
||||||
|
case BluetoothService.State.CONNECTED:
|
||||||
|
// Either not ready or already connected. Terminate new socket.
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "Could not close unwanted socket");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.d("END AcceptThread %s", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
Timber.d("cancel() %s", this);
|
||||||
|
try {
|
||||||
|
serverSocket.close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "close() of server failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This thread runs while attempting to make an outgoing connection
|
||||||
|
* with a device. It runs straight through; the connection either
|
||||||
|
* succeeds or fails.
|
||||||
|
*/
|
||||||
|
private class ConnectThread extends Thread {
|
||||||
|
private final BluetoothSocket socket;
|
||||||
|
private final BluetoothDevice device;
|
||||||
|
|
||||||
|
public ConnectThread(BluetoothDevice device) {
|
||||||
|
this.device = device;
|
||||||
|
|
||||||
|
// Create a BluetoothSocket
|
||||||
|
try {
|
||||||
|
socket = device.createRfcommSocketToServiceRecord(SDP_UUID);
|
||||||
|
state = BluetoothService.State.CONNECTING;
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "create() failed");
|
||||||
|
throw new IllegalStateException(); //TODO really die here?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
Timber.d("BEGIN ConnectThread");
|
||||||
|
|
||||||
|
// Always cancel discovery because it will slow down a connection
|
||||||
|
bluetoothAdapter.cancelDiscovery(); //TODO show & remember discovery state?
|
||||||
|
|
||||||
|
// Make a connection to the BluetoothSocket
|
||||||
|
try {
|
||||||
|
// This is a blocking call and will only return on a
|
||||||
|
// successful connection or an exception
|
||||||
|
socket.connect(); // sometimes this fails - why?
|
||||||
|
} catch (IOException ex) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException exClose) {
|
||||||
|
Timber.d(exClose, "unable to close() socket during connection failure");
|
||||||
|
}
|
||||||
|
onConnectFailed();
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
// Reset the ConnectThread because we're done
|
||||||
|
synchronized (BluetoothService.this) {
|
||||||
|
connectThread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the ConnectedThread
|
||||||
|
startConnected(socket, device);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "close() of connect socket failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This thread runs during a connection with a remote device.
|
||||||
|
* It handles all incoming and outgoing transmissions.
|
||||||
|
*/
|
||||||
|
private class ConnectedThread extends Thread {
|
||||||
|
private final BluetoothSocket socket;
|
||||||
|
private final InputStream in;
|
||||||
|
private final OutputStream out;
|
||||||
|
|
||||||
|
private final ByteArrayOutputStream bytesIn = new ByteArrayOutputStream();
|
||||||
|
private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
private CountDownLatch readSignal = null; // TODO this needs to be a Map with correlationIds
|
||||||
|
|
||||||
|
public void setReadSignal(CountDownLatch signal) { //TODO see above
|
||||||
|
readSignal = signal;
|
||||||
|
readBuffer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private byte[] readBuffer = null;
|
||||||
|
|
||||||
|
public ConnectedThread(BluetoothSocket bluetoothSocket) {
|
||||||
|
Timber.d("ConnectedThread()");
|
||||||
|
socket = bluetoothSocket;
|
||||||
|
InputStream tmpIn = null;
|
||||||
|
OutputStream tmpOut;
|
||||||
|
|
||||||
|
// Get the BluetoothSocket input and output streams
|
||||||
|
try {
|
||||||
|
tmpIn = socket.getInputStream();
|
||||||
|
tmpOut = socket.getOutputStream();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "temp sockets not created");
|
||||||
|
if (tmpIn != null)
|
||||||
|
try {
|
||||||
|
tmpIn.close();
|
||||||
|
} catch (IOException exIn) {
|
||||||
|
Timber.d(exIn);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
in = tmpIn;
|
||||||
|
out = tmpOut;
|
||||||
|
state = BluetoothService.State.CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
Timber.d("BEGIN ConnectedThread %s", this);
|
||||||
|
final byte[] buffer = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
|
||||||
|
// Keep listening to the InputStream while connected
|
||||||
|
while (state == BluetoothService.State.CONNECTED) {
|
||||||
|
// Protocol: "SIDEKICK"|1-byte:reserved|2-bytes:length|buffer
|
||||||
|
try {
|
||||||
|
// protocol header
|
||||||
|
bytesRead = in.read(buffer, 0, MAGIC.length + 3);
|
||||||
|
int a = in.available() + bytesRead;
|
||||||
|
if (bytesRead != MAGIC.length + 3)
|
||||||
|
throw new IllegalStateException("message too short");
|
||||||
|
for (int i = 0; i < MAGIC.length; i++) {
|
||||||
|
if (buffer[i] != MAGIC[i]) throw new IllegalStateException("no MAGIC");
|
||||||
|
}
|
||||||
|
final int options = buffer[MAGIC.length]; // 0 regular message, else CODE instead of payload length
|
||||||
|
final int payloadLength = ((0xff & buffer[MAGIC.length + 1]) << 8) + (0xff & buffer[MAGIC.length + 2]);
|
||||||
|
Timber.d("READ options %d, payloadLength=%d, available=%d", options, payloadLength, a);
|
||||||
|
if ((options & 0x01) != 0) { // CODE
|
||||||
|
connectedCode = String.format(Locale.US, "%04d", payloadLength);
|
||||||
|
if (uiHandler != null)
|
||||||
|
uiHandler.obtainMessage(MessageType.CODE, connectedCode).sendToTarget();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int remainingBytes = payloadLength;
|
||||||
|
bytesIn.reset();
|
||||||
|
while (remainingBytes > 0) {
|
||||||
|
bytesRead = in.read(buffer, 0, Math.min(remainingBytes, buffer.length));
|
||||||
|
remainingBytes -= bytesRead;
|
||||||
|
bytesIn.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
readBuffer = bytesIn.toByteArray();
|
||||||
|
if (readSignal != null) { // someone is awaiting this
|
||||||
|
readSignal.countDown();
|
||||||
|
} else if (commHandler != null) { // we are the counterparty
|
||||||
|
final int command = readBuffer[0];
|
||||||
|
commHandler.obtainMessage(command, readBuffer).sendToTarget();
|
||||||
|
if (uiHandler != null) {
|
||||||
|
uiHandler.obtainMessage(MessageType.READ_CMD, readBuffer.length, command).sendToTarget();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("would drop a message");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "disconnected");
|
||||||
|
if (readSignal != null) readSignal.countDown(); // readBudder is still null
|
||||||
|
onConnectionLost();
|
||||||
|
reconnect = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.d("END ConnectedThread %s", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write to the connected OutStream.
|
||||||
|
* <p>
|
||||||
|
* Protocol: "SIDEKICK"|1-byte:reserved|2-bytes:length|buffer
|
||||||
|
*
|
||||||
|
* @param buffer The bytes to write
|
||||||
|
*/
|
||||||
|
public boolean write(byte[] buffer) {
|
||||||
|
boolean sendCode = buffer.length == 2; // TODO undo this hack
|
||||||
|
try {
|
||||||
|
final int len = buffer.length;
|
||||||
|
if (len > 65535) {
|
||||||
|
Timber.w("buffer too long %d", len);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bytesOut.reset();
|
||||||
|
bytesOut.write(MAGIC);
|
||||||
|
if (sendCode) {
|
||||||
|
bytesOut.write(0x01); // options bit 0 is CODE
|
||||||
|
} else {
|
||||||
|
bytesOut.write(0);
|
||||||
|
bytesOut.write(len >> 8);
|
||||||
|
bytesOut.write(len & 0xff);
|
||||||
|
}
|
||||||
|
bytesOut.write(buffer);
|
||||||
|
out.write(bytesOut.toByteArray());
|
||||||
|
|
||||||
|
if (uiHandler != null) {
|
||||||
|
uiHandler.obtainMessage(MessageType.WRITE, buffer.length, -1).sendToTarget();
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "Exception during write");
|
||||||
|
return false;
|
||||||
|
//TODO probably kill the connection if this happens?
|
||||||
|
// but the read operation probably throws it as well, and that takes care of that!
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Timber.d(ex, "close() of connect socket failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// for direct communication from JNI
|
||||||
|
// this should block
|
||||||
|
static public byte[] Exchange(byte[] request) {
|
||||||
|
Timber.d("EXCHANGE req = %d bytes", request.length);
|
||||||
|
final byte[] response = Instance.exchange(request);
|
||||||
|
Timber.d("EXCHANGE resp = %d bytes", response.length);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public boolean Write(byte[] request) {
|
||||||
|
return Instance.write(request);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ import android.os.Process;
|
||||||
/**
|
/**
|
||||||
* Handy class for starting a new thread that has a looper. The looper can then be
|
* Handy class for starting a new thread that has a looper. The looper can then be
|
||||||
* used to create handler classes. Note that start() must still be called.
|
* used to create handler classes. Note that start() must still be called.
|
||||||
* The started Thread has a stck size of STACK_SIZE (=5MB)
|
* The started Thread has a stack size of STACK_SIZE (=5MB)
|
||||||
*/
|
*/
|
||||||
public class MoneroHandlerThread extends Thread {
|
public class MoneroHandlerThread extends Thread {
|
||||||
// from src/cryptonote_config.h
|
// from src/cryptonote_config.h
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.service;
|
package com.m2049r.xmrwallet.service;
|
||||||
|
|
||||||
|
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
|
@ -581,8 +583,12 @@ public class WalletService extends Service {
|
||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.build();
|
.build();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||||
|
} else {
|
||||||
startForeground(NOTIFICATION_ID, notification);
|
startForeground(NOTIFICATION_ID, notification);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private String createNotificationChannel() {
|
private String createNotificationChannel() {
|
||||||
|
|
|
@ -142,9 +142,13 @@ public class ExchangeApiImpl implements ExchangeApi {
|
||||||
} catch (ParserConfigurationException | SAXException ex) {
|
} catch (ParserConfigurationException | SAXException ex) {
|
||||||
Timber.w(ex);
|
Timber.w(ex);
|
||||||
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
callback.onError(new ExchangeException(response.code(), response.message()));
|
final ExchangeException ex = new ExchangeException(response.code(), response.message());
|
||||||
|
response.close();
|
||||||
|
callback.onError(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -89,12 +89,12 @@ public class ExchangeApiImpl implements ExchangeApi {
|
||||||
final NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url);
|
final NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url);
|
||||||
httpRequest.enqueue(new okhttp3.Callback() {
|
httpRequest.enqueue(new okhttp3.Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(final Call call, final IOException ex) {
|
public void onFailure(@NonNull final Call call, @NonNull final IOException ex) {
|
||||||
callback.onError(ex);
|
callback.onError(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(final Call call, final Response response) throws IOException {
|
public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
try {
|
try {
|
||||||
final JSONObject json = new JSONObject(response.body().string());
|
final JSONObject json = new JSONObject(response.body().string());
|
||||||
|
@ -108,9 +108,13 @@ public class ExchangeApiImpl implements ExchangeApi {
|
||||||
}
|
}
|
||||||
} catch (JSONException ex) {
|
} catch (JSONException ex) {
|
||||||
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
callback.onError(new ExchangeException(response.code(), response.message()));
|
final ExchangeException ex = new ExchangeException(response.code(), response.message());
|
||||||
|
response.close();
|
||||||
|
callback.onError(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -95,9 +95,13 @@ public class ExchangeApiImpl implements ExchangeApi {
|
||||||
callback.onSuccess(new ExchangeRateImpl(quoteCurrency, rate, timestamp));
|
callback.onSuccess(new ExchangeRateImpl(quoteCurrency, rate, timestamp));
|
||||||
} catch (JSONException ex) {
|
} catch (JSONException ex) {
|
||||||
callback.onError(ex);
|
callback.onError(ex);
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
callback.onError(new ExchangeException(response.code(), response.message()));
|
final ExchangeException ex = new ExchangeException(response.code(), response.message());
|
||||||
|
response.close();
|
||||||
|
callback.onError(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,5 @@ public interface NetworkCallback {
|
||||||
|
|
||||||
void onSuccess(JSONObject jsonObject);
|
void onSuccess(JSONObject jsonObject);
|
||||||
|
|
||||||
void onError(Exception ex);
|
void onError(Exception ex, JSONObject json);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,7 @@ import androidx.annotation.NonNull;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public interface ShiftApiCall {
|
public interface ShiftApiCall {
|
||||||
|
void get(@NonNull final String path, final String parameters, @NonNull final NetworkCallback callback);
|
||||||
|
|
||||||
void call(@NonNull final String path, @NonNull final NetworkCallback callback);
|
void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback);
|
||||||
|
|
||||||
void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,32 +18,23 @@ package com.m2049r.xmrwallet.service.shift;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ShiftError {
|
public class ShiftError {
|
||||||
@Getter
|
@Getter
|
||||||
private final Error errorType;
|
private final Type type;
|
||||||
@Getter
|
@Getter
|
||||||
private final String errorMsg;
|
private final String errorMsg;
|
||||||
|
|
||||||
public enum Error {
|
public enum Type {
|
||||||
SERVICE,
|
SERVICE,
|
||||||
INFRASTRUCTURE
|
INFRASTRUCTURE
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRetryable() {
|
public boolean isRetryable() {
|
||||||
return errorType == Error.INFRASTRUCTURE;
|
return type == Type.INFRASTRUCTURE;
|
||||||
}
|
|
||||||
|
|
||||||
public ShiftError(final JSONObject jsonObject) throws JSONException {
|
|
||||||
final JSONObject errorObject = jsonObject.getJSONObject("error");
|
|
||||||
errorType = Error.SERVICE;
|
|
||||||
errorMsg = errorObject.getString("message");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.service.shift;
|
package com.m2049r.xmrwallet.service.shift;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@ -24,10 +26,14 @@ public class ShiftException extends Exception {
|
||||||
@Getter
|
@Getter
|
||||||
private final int code;
|
private final int code;
|
||||||
@Getter
|
@Getter
|
||||||
|
@Nullable
|
||||||
private final ShiftError error;
|
private final ShiftError error;
|
||||||
|
|
||||||
public ShiftException(int code) {
|
public ShiftException(int code) {
|
||||||
this.code = code;
|
this(code, null);
|
||||||
this.error = null;
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return (error != null) ? error.getErrorMsg() : ("HTTP:" + code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.m2049r.xmrwallet.service.shift;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.R;
|
||||||
|
import com.m2049r.xmrwallet.data.Crypto;
|
||||||
|
import com.m2049r.xmrwallet.fragment.send.PreShifter;
|
||||||
|
import com.m2049r.xmrwallet.fragment.send.Shifter;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.process.PreProcess;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.process.PreShiftProcess;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.process.Process;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.process.ShiftProcess;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.provider.exolix.ExolixApiImpl;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum ShiftService {
|
||||||
|
XMRTO(false, "xmr.to", "xmrto", null, null, 0, R.drawable.ic_xmrto_logo, ""),
|
||||||
|
SIDESHIFT(false, "SideShift.ai", "side", null, null, R.drawable.ic_sideshift_icon, R.drawable.ic_sideshift_wide, ""),
|
||||||
|
EXOLIX(true, "EXOLIX", "exolix", new ExolixApiImpl(), Type.ONESTEP, R.drawable.ic_exolix_icon, R.drawable.ic_exolix_wide, "XMR:BTC:LTC:ETH:USDT:SOL"),
|
||||||
|
UNKNOWN(false, "", null, null, null, 0, 0, "");
|
||||||
|
|
||||||
|
static final public ShiftService DEFAULT = EXOLIX;
|
||||||
|
final private boolean enabled;
|
||||||
|
final private String label;
|
||||||
|
final private String tag;
|
||||||
|
final private ShiftApi shiftApi;
|
||||||
|
final private Type type;
|
||||||
|
final private int iconId;
|
||||||
|
final private int logoId;
|
||||||
|
final private String assets;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
static public ShiftService findWithTag(String tag) {
|
||||||
|
if (tag == null) return UNKNOWN;
|
||||||
|
for (ShiftService service : values()) {
|
||||||
|
if (tag.equals(service.tag)) return service;
|
||||||
|
}
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
static private final Set<Crypto> possibleAssets = new HashSet<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
assert DEFAULT.enabled;
|
||||||
|
for (ShiftService service : values()) {
|
||||||
|
if (!service.enabled) continue;
|
||||||
|
final String[] assets = service.getAssets().split(":");
|
||||||
|
for (String anAsset : assets) {
|
||||||
|
possibleAssets.add(Crypto.withSymbol(anAsset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAssetSupported(@NonNull Crypto crypto) {
|
||||||
|
return possibleAssets.contains(crypto);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAssetSupported(@NonNull String symbol) {
|
||||||
|
final Crypto crypto = Crypto.withSymbol(symbol);
|
||||||
|
if (crypto != null) {
|
||||||
|
return isAssetSupported(crypto);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsAsset(@NonNull Crypto crypto) {
|
||||||
|
return assets.contains(crypto.getSymbol());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsAsset(@NonNull String symbol) {
|
||||||
|
final Crypto crypto = Crypto.withSymbol(symbol);
|
||||||
|
if (crypto != null) {
|
||||||
|
return supportsAsset(crypto);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShiftProcess createProcess(Shifter shifter) {
|
||||||
|
return new Process(this, shifter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreShiftProcess createPreProcess(PreShifter shifter) {
|
||||||
|
return new PreProcess(this, shifter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
ONESTEP, TWOSTEP;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Crypto ASSET = null; // keep asset to exchange globally
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 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 com.m2049r.xmrwallet.service.shift;
|
||||||
|
|
||||||
|
public enum ShiftType {
|
||||||
|
FIXED, FLOAT;
|
||||||
|
}
|
|
@ -14,12 +14,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.service.shift.sideshift.api;
|
package com.m2049r.xmrwallet.service.shift.api;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftType;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
public interface CreateOrder {
|
public interface CreateOrder {
|
||||||
String TAG = "side";
|
String getTag();
|
||||||
|
|
||||||
String getBtcCurrency();
|
String getBtcCurrency();
|
||||||
|
|
||||||
|
@ -35,8 +37,11 @@ public interface CreateOrder {
|
||||||
|
|
||||||
String getXmrAddress();
|
String getXmrAddress();
|
||||||
|
|
||||||
Date getCreatedAt(); // createdAt
|
Date getCreatedAt();
|
||||||
|
|
||||||
Date getExpiresAt(); // expiresAt
|
Date getExpiresAt();
|
||||||
|
|
||||||
|
String getQueryOrderId();
|
||||||
|
|
||||||
|
ShiftType getType();
|
||||||
}
|
}
|
|
@ -14,14 +14,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.service.shift.sideshift.api;
|
package com.m2049r.xmrwallet.service.shift.api;
|
||||||
|
|
||||||
public interface QueryOrderParameters {
|
public interface QueryOrderParameters {
|
||||||
|
|
||||||
double getLowerLimit();
|
double getLowerLimit(); // XMR
|
||||||
|
|
||||||
double getPrice();
|
double getPrice(); // BTC/XMR
|
||||||
|
|
||||||
double getUpperLimit();
|
double getUpperLimit(); // XMR
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017-2021 m2049r et al.
|
||||||
|
*
|
||||||
|
* 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 com.m2049r.xmrwallet.service.shift.api;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class QueryOrderStatus {
|
||||||
|
final String orderId;
|
||||||
|
final Status status;
|
||||||
|
final String btcCurrency;
|
||||||
|
final double btcAmount;
|
||||||
|
final String btcAddress;
|
||||||
|
final double xmrAmount;
|
||||||
|
final String xmrAddress;
|
||||||
|
@Nullable
|
||||||
|
final Date createdAt;
|
||||||
|
@Nullable
|
||||||
|
final Date expiresAt;
|
||||||
|
|
||||||
|
public double getPrice() {
|
||||||
|
return btcAmount / xmrAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isError() {
|
||||||
|
return status.isError();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTerminal() {
|
||||||
|
return status.isTerminal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWaiting() {
|
||||||
|
return status.isWaiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPending() {
|
||||||
|
return status.isPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSent() {
|
||||||
|
return status.isSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPaid() {
|
||||||
|
return status.isPaid();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
WAITING, // Waiting for mempool
|
||||||
|
PENDING, // Detected (waiting for confirmations)
|
||||||
|
SETTLING, // Settlement in progress
|
||||||
|
SETTLED, // Settlement completed
|
||||||
|
// no refunding in monerujo so these are ignored:
|
||||||
|
// REFUND, // Queued for refund
|
||||||
|
// REFUNDING, // Refund in progress
|
||||||
|
// REFUNDED // Refund completed
|
||||||
|
UNDEFINED,
|
||||||
|
EXPIRED,
|
||||||
|
ERROR; // Something went wrong and the user needs to interact with the provider
|
||||||
|
|
||||||
|
public boolean isError() {
|
||||||
|
return this == Status.UNDEFINED || this == Status.ERROR || this == Status.EXPIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTerminal() {
|
||||||
|
return (this == Status.SETTLED) || isError();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWaiting() {
|
||||||
|
return this == Status.WAITING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPending() {
|
||||||
|
return this == Status.PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSent() {
|
||||||
|
return this == Status.SETTLING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPaid() {
|
||||||
|
return this == Status.SETTLED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,13 +14,17 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.service.shift.sideshift.api;
|
package com.m2049r.xmrwallet.service.shift.api;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftType;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
public interface RequestQuote {
|
public interface RequestQuote {
|
||||||
|
|
||||||
double getBtcAmount(); // settleAmount
|
double getBtcAmount(); // what we want to receive
|
||||||
|
|
||||||
|
double getXmrAmount(); // the XMR we need to send
|
||||||
|
|
||||||
String getId(); // id
|
String getId(); // id
|
||||||
|
|
||||||
|
@ -28,7 +32,7 @@ public interface RequestQuote {
|
||||||
|
|
||||||
Date getExpiresAt(); // expiresAt
|
Date getExpiresAt(); // expiresAt
|
||||||
|
|
||||||
double getXmrAmount(); // depositAmount
|
|
||||||
|
|
||||||
double getPrice(); // rate
|
double getPrice(); // rate
|
||||||
|
|
||||||
|
ShiftType getType();
|
||||||
}
|
}
|
|
@ -14,15 +14,17 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.service.shift.sideshift.api;
|
package com.m2049r.xmrwallet.service.shift.api;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.data.CryptoAmount;
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||||
|
|
||||||
public interface SideShiftApi {
|
public interface ShiftApi {
|
||||||
int QUERY_INTERVAL = 5000; // ms
|
int QUERY_INTERVAL = 5000; // ms
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,22 +32,23 @@ public interface SideShiftApi {
|
||||||
*
|
*
|
||||||
* @param callback the callback with the OrderParameter object
|
* @param callback the callback with the OrderParameter object
|
||||||
*/
|
*/
|
||||||
void queryOrderParameters(@NonNull final ShiftCallback<QueryOrderParameters> callback);
|
void queryOrderParameters(@NonNull final CryptoAmount btcAmount, @NonNull final ShiftCallback<QueryOrderParameters> callback);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an order
|
* Creates an order
|
||||||
*
|
*
|
||||||
* @param xmrAmount the desired XMR amount
|
* @param btcAddress destination
|
||||||
|
* @param btcAmount the desired amount to send
|
||||||
*/
|
*/
|
||||||
void requestQuote(final double xmrAmount, @NonNull final ShiftCallback<RequestQuote> callback);
|
void requestQuote(@Nullable final String btcAddress, @NonNull final CryptoAmount btcAmount, @NonNull final ShiftCallback<RequestQuote> callback);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an order
|
* Creates an order
|
||||||
*
|
*
|
||||||
* @param quoteId the desired XMR amount
|
* @param quote the quote from {@link #requestQuote(String, CryptoAmount, ShiftCallback)}
|
||||||
* @param btcAddress the target bitcoin address
|
* @param btcAddress the target bitcoin address
|
||||||
*/
|
*/
|
||||||
void createOrder(final String quoteId, @NonNull final String btcAddress, @NonNull final ShiftCallback<CreateOrder> callback);
|
void createOrder(final RequestQuote quote, @NonNull final String btcAddress, @NonNull final ShiftCallback<CreateOrder> callback);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries the order status for given current order
|
* Queries the order status for given current order
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.m2049r.xmrwallet.service.shift.process;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.data.CryptoAmount;
|
||||||
|
import com.m2049r.xmrwallet.fragment.send.PreShifter;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PreProcess implements PreShiftProcess {
|
||||||
|
@Getter
|
||||||
|
final private ShiftService service;
|
||||||
|
final private PreShifter preshifter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
getOrderParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restart() {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getOrderParameters() {
|
||||||
|
Timber.d("getOrderParameters");
|
||||||
|
if (!preshifter.isActive()) return;
|
||||||
|
preshifter.showProgress();
|
||||||
|
final CryptoAmount btcAmount = preshifter.getAmount();
|
||||||
|
service.getShiftApi().queryOrderParameters(btcAmount, new ShiftCallback<QueryOrderParameters>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(final QueryOrderParameters orderParameters) {
|
||||||
|
if (!preshifter.isActive()) return;
|
||||||
|
onOrderParametersReceived(orderParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(final Exception ex) {
|
||||||
|
if (!preshifter.isActive()) return;
|
||||||
|
preshifter.onOrderParametersError(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onOrderParametersReceived(final QueryOrderParameters orderParameters) {
|
||||||
|
Timber.d("onOrderParmsReceived %f", orderParameters.getPrice());
|
||||||
|
preshifter.onOrderParametersReceived(orderParameters);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.m2049r.xmrwallet.service.shift.process;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
|
|
||||||
|
public interface PreShiftProcess {
|
||||||
|
ShiftService getService();
|
||||||
|
|
||||||
|
void run();
|
||||||
|
|
||||||
|
void restart();
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
package com.m2049r.xmrwallet.service.shift.process;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.data.Crypto;
|
||||||
|
import com.m2049r.xmrwallet.data.TxDataBtc;
|
||||||
|
import com.m2049r.xmrwallet.fragment.send.Shifter;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftType;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class Process implements ShiftProcess {
|
||||||
|
@Getter
|
||||||
|
final private ShiftService service;
|
||||||
|
final private Shifter shifter;
|
||||||
|
private TxDataBtc txDataBtc;
|
||||||
|
private RequestQuote quote = null;
|
||||||
|
private CreateOrder order = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(TxDataBtc txData) {
|
||||||
|
txDataBtc = txData;
|
||||||
|
switch (service.getType()) {
|
||||||
|
case TWOSTEP:
|
||||||
|
getQuote();
|
||||||
|
break;
|
||||||
|
case ONESTEP:
|
||||||
|
quote = new RequestQuote() {
|
||||||
|
@Override
|
||||||
|
public double getBtcAmount() {
|
||||||
|
return txDataBtc.getBtcAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getXmrAmount() {
|
||||||
|
return txDataBtc.getXmrAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getCreatedAt() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getExpiresAt() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getPrice() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ShiftType getType() {
|
||||||
|
return (txDataBtc.getShiftAmount().getCrypto() == Crypto.XMR) ? ShiftType.FLOAT : ShiftType.FIXED;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
createOrder();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restart() {
|
||||||
|
run(txDataBtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getQuote() {
|
||||||
|
shifter.invalidateShift();
|
||||||
|
if (!shifter.isActive()) return;
|
||||||
|
Timber.d("Request Quote");
|
||||||
|
quote = null;
|
||||||
|
order = null;
|
||||||
|
shifter.showProgress(Shifter.Stage.A);
|
||||||
|
|
||||||
|
ShiftCallback<RequestQuote> callback = new ShiftCallback<RequestQuote>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(RequestQuote requestQuote) {
|
||||||
|
if (!shifter.isActive()) return;
|
||||||
|
if (quote != null) {
|
||||||
|
Timber.w("another ongoing request quote request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onQuoteReceived(requestQuote);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception ex) {
|
||||||
|
if (!shifter.isActive()) return;
|
||||||
|
if (quote != null) {
|
||||||
|
Timber.w("another ongoing request quote request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shifter.onQuoteError(ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
service.getShiftApi().requestQuote(txDataBtc.getBtcAddress(), txDataBtc.getShiftAmount(), callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onQuoteReceived(final RequestQuote quote) {
|
||||||
|
Timber.d("onQuoteReceived %s", quote.getId());
|
||||||
|
// verify the shift is correct
|
||||||
|
if (!txDataBtc.validate(quote)) {
|
||||||
|
Timber.d("Failed to get quote");
|
||||||
|
shifter.showQuoteError();
|
||||||
|
return; // just stop for now
|
||||||
|
}
|
||||||
|
this.quote = quote;
|
||||||
|
txDataBtc.setAmount(this.quote.getXmrAmount());
|
||||||
|
shifter.onQuoteReceived(this.quote);
|
||||||
|
createOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void retryCreateOrder() {
|
||||||
|
createOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createOrder() {
|
||||||
|
Timber.d("createOrder(%s)", quote.getId());
|
||||||
|
if (!shifter.isActive()) return;
|
||||||
|
final String btcAddress = txDataBtc.getBtcAddress();
|
||||||
|
order = null;
|
||||||
|
shifter.showProgress(Shifter.Stage.B);
|
||||||
|
service.getShiftApi().createOrder(quote, btcAddress, new ShiftCallback<CreateOrder>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(final CreateOrder order) {
|
||||||
|
if (!shifter.isActive()) return;
|
||||||
|
if (quote == null) return;
|
||||||
|
if ((quote.getId() != null) && !order.getQuoteId().equals(quote.getId())) {
|
||||||
|
Timber.d("Quote ID does not match");
|
||||||
|
// ignore (we got a response to a stale request)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Process.this.order != null)
|
||||||
|
throw new IllegalStateException("order must be null here!");
|
||||||
|
onOrderReceived(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(final Exception ex) {
|
||||||
|
if (!shifter.isActive()) return;
|
||||||
|
shifter.onOrderError(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onOrderReceived(final CreateOrder order) {
|
||||||
|
Timber.d("onOrderReceived %s for %s", order.getOrderId(), order.getQuoteId());
|
||||||
|
// verify amount & destination
|
||||||
|
if (!order.getBtcCurrency().equalsIgnoreCase(txDataBtc.getBtcSymbol()))
|
||||||
|
throw new IllegalStateException("Destination Currency is wrong: " + order.getBtcCurrency()); // something is terribly wrong - die
|
||||||
|
if ((order.getType() == ShiftType.FIXED) && (order.getBtcAmount() != txDataBtc.getShiftAmount().getAmount()))
|
||||||
|
throw new IllegalStateException("Destination Amount is wrong: " + order.getBtcAmount()); // something is terribly wrong - die
|
||||||
|
if ((order.getType() == ShiftType.FLOAT) && (order.getXmrAmount() != txDataBtc.getShiftAmount().getAmount()))
|
||||||
|
throw new IllegalStateException("Source Amount is wrong: " + order.getXmrAmount()); // something is terribly wrong - die
|
||||||
|
if (!txDataBtc.validateAddress(order.getBtcAddress())) {
|
||||||
|
throw new IllegalStateException("Destination address is wrong: " + order.getBtcAddress()); // something is terribly wrong - die
|
||||||
|
}
|
||||||
|
this.order = order;
|
||||||
|
shifter.onOrderCreated(order);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.m2049r.xmrwallet.service.shift.process;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.data.TxDataBtc;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
|
|
||||||
|
public interface ShiftProcess {
|
||||||
|
ShiftService getService();
|
||||||
|
|
||||||
|
void run(TxDataBtc txData);
|
||||||
|
|
||||||
|
void restart();
|
||||||
|
|
||||||
|
void retryCreateOrder();
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017-2021 m2049r et al.
|
||||||
|
*
|
||||||
|
* 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 com.m2049r.xmrwallet.service.shift.provider;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.data.CryptoAmount;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftError;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
|
||||||
|
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
abstract public class ShiftApiImpl implements ShiftApi, ShiftApiCall {
|
||||||
|
|
||||||
|
protected abstract String getBaseUrl();
|
||||||
|
|
||||||
|
protected abstract String getApiUrl();
|
||||||
|
|
||||||
|
private final HttpUrl api;
|
||||||
|
|
||||||
|
public ShiftApiImpl() {
|
||||||
|
api = HttpUrl.parse(getApiUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queryOrderParameters(CryptoAmount btcAmount, @NonNull final ShiftCallback<QueryOrderParameters> callback) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestQuote(@Nullable final String btcAddress, @NonNull final CryptoAmount btcAmount, @NonNull final ShiftCallback<RequestQuote> callback) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createOrder(@NonNull final RequestQuote quote, @NonNull final String btcAddress,
|
||||||
|
@NonNull final ShiftCallback<CreateOrder> callback) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queryOrderStatus(@NonNull final String uuid,
|
||||||
|
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getQueryOrderUri(String orderId) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void get(@NonNull final String path, final String parameters, @NonNull final NetworkCallback callback) {
|
||||||
|
Timber.d("GET parameters=%s", parameters);
|
||||||
|
final HttpUrl.Builder builder = api.newBuilder().addPathSegments(path);
|
||||||
|
if (parameters != null)
|
||||||
|
for (String parm : parameters.split("&")) {
|
||||||
|
String[] p = parm.split("=");
|
||||||
|
builder.addQueryParameter(p[0], p[1]);
|
||||||
|
}
|
||||||
|
NetCipherHelper.Request request = new NetCipherHelper.Request(builder.build());
|
||||||
|
augment(request, null);
|
||||||
|
enqueue(request, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback) {
|
||||||
|
Timber.d("data=%s", data);
|
||||||
|
final HttpUrl url = api.newBuilder().addPathSegments(path).build();
|
||||||
|
final NetCipherHelper.Request request = new NetCipherHelper.Request(url, data);
|
||||||
|
augment(request, data);
|
||||||
|
enqueue(request, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void augment(@NonNull final NetCipherHelper.Request request, @Nullable final JSONObject data) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enqueue(NetCipherHelper.Request request, @NonNull final NetworkCallback callback) {
|
||||||
|
Timber.d("REQ: %s", request);
|
||||||
|
request.enqueue(new okhttp3.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull final Call call, @NonNull final IOException ex) {
|
||||||
|
callback.onError(ex, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
|
||||||
|
Timber.d("onResponse code=%d", response.code());
|
||||||
|
try {
|
||||||
|
if (response.body() == null) {
|
||||||
|
callback.onError(new IllegalStateException("Empty response from service"), null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String body = response.body().string();
|
||||||
|
if ((response.code() >= 200) && (response.code() <= 499)) {
|
||||||
|
try {
|
||||||
|
Timber.d(" SUCCESS %s", body);
|
||||||
|
final JSONObject json = new JSONObject(body);
|
||||||
|
final ShiftError error = createShiftError(ShiftError.Type.SERVICE, json);
|
||||||
|
if (error != null) {
|
||||||
|
callback.onError(new ShiftException(response.code(), error), json);
|
||||||
|
} else {
|
||||||
|
callback.onSuccess(json);
|
||||||
|
}
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
callback.onError(ex, null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Timber.d("!SUCCESS %s", body);
|
||||||
|
final JSONObject json = new JSONObject(body);
|
||||||
|
Timber.d(json.toString(2));
|
||||||
|
final ShiftError error = createShiftError(ShiftError.Type.INFRASTRUCTURE, json);
|
||||||
|
Timber.d("%s says %d/%s", getBaseUrl(), response.code(), error);
|
||||||
|
callback.onError(new ShiftException(response.code(), error), json);
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
callback.onError(new ShiftException(response.code()), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected ShiftError createShiftError(ShiftError.Type type, final JSONObject jsonObject);
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017-2021 m2049r et al.
|
||||||
|
*
|
||||||
|
* 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 com.m2049r.xmrwallet.service.shift.provider.exolix;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftType;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
|
||||||
|
import com.m2049r.xmrwallet.util.DateHelper;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
class CreateOrderImpl implements CreateOrder {
|
||||||
|
private final static long EXPIRE = 10 * 60 * 1000; // 10 minutes
|
||||||
|
private final String btcCurrency;
|
||||||
|
private final double btcAmount;
|
||||||
|
private final String btcAddress;
|
||||||
|
private final String orderId;
|
||||||
|
private final double xmrAmount;
|
||||||
|
private final String xmrAddress;
|
||||||
|
private final Date createdAt;
|
||||||
|
private final Date expiresAt;
|
||||||
|
private final ShiftType type;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getQueryOrderId() {
|
||||||
|
return orderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getQuoteId() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateOrderImpl(final JSONObject jsonObject) throws JSONException {
|
||||||
|
final JSONObject coinFrom = jsonObject.getJSONObject("coinFrom");
|
||||||
|
final JSONObject coinTo = jsonObject.getJSONObject("coinTo");
|
||||||
|
// sanity checks
|
||||||
|
final String depositMethod = coinFrom.getString("coinCode");
|
||||||
|
final String settleMethod = coinTo.getString("coinCode");
|
||||||
|
if (!"xmr".equalsIgnoreCase(depositMethod)
|
||||||
|
|| !ShiftService.ASSET.getSymbol().equalsIgnoreCase(settleMethod))
|
||||||
|
throw new IllegalStateException();
|
||||||
|
|
||||||
|
btcCurrency = settleMethod.toUpperCase();
|
||||||
|
btcAmount = jsonObject.getDouble("amountTo");
|
||||||
|
btcAddress = jsonObject.getString("withdrawalAddress");
|
||||||
|
|
||||||
|
xmrAmount = jsonObject.getDouble("amount");
|
||||||
|
xmrAddress = jsonObject.getString("depositAddress");
|
||||||
|
|
||||||
|
orderId = jsonObject.getString("id");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final String created = jsonObject.getString("createdAt");
|
||||||
|
createdAt = DateHelper.parse(created);
|
||||||
|
expiresAt = new Date(createdAt.getTime() + EXPIRE);
|
||||||
|
} catch (ParseException ex) {
|
||||||
|
throw new JSONException(ex.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
type = jsonObject.getString("rateType").equals("float") ? ShiftType.FLOAT : ShiftType.FIXED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void call(@NonNull final ShiftApiCall api,
|
||||||
|
@NonNull final String btcAddress,
|
||||||
|
@NonNull final RequestQuote quote,
|
||||||
|
@NonNull final ShiftCallback<CreateOrder> callback) {
|
||||||
|
try {
|
||||||
|
final JSONObject request = createRequest(btcAddress, quote);
|
||||||
|
api.post("transactions", request, new NetworkCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(JSONObject jsonObject) {
|
||||||
|
try {
|
||||||
|
callback.onSuccess(new CreateOrderImpl(jsonObject));
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception ex, JSONObject json) {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static JSONObject createRequest(@NonNull final String btcAddress, @NonNull final RequestQuote quote) throws JSONException {
|
||||||
|
final JSONObject jsonObject = new JSONObject();
|
||||||
|
if (quote.getType() == ShiftType.FLOAT) {
|
||||||
|
jsonObject.put("rateType", "float");
|
||||||
|
jsonObject.put("amount", quote.getXmrAmount());
|
||||||
|
} else { // default is FIXED
|
||||||
|
jsonObject.put("rateType", "fixed");
|
||||||
|
jsonObject.put("withdrawalAmount", quote.getBtcAmount());
|
||||||
|
}
|
||||||
|
jsonObject.put("coinFrom", "XMR");
|
||||||
|
jsonObject.put("coinTo", ShiftService.ASSET.getSymbol());
|
||||||
|
jsonObject.put("networkTo", ShiftService.ASSET.getNetwork());
|
||||||
|
jsonObject.put("withdrawalAddress", btcAddress);
|
||||||
|
return jsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return ShiftService.EXOLIX.getTag();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017-2021 m2049r et al.
|
||||||
|
*
|
||||||
|
* 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 com.m2049r.xmrwallet.service.shift.provider.exolix;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.BuildConfig;
|
||||||
|
import com.m2049r.xmrwallet.data.CryptoAmount;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftError;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.provider.ShiftApiImpl;
|
||||||
|
import com.m2049r.xmrwallet.util.IdHelper;
|
||||||
|
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public class ExolixApiImpl extends ShiftApiImpl {
|
||||||
|
@Getter
|
||||||
|
final private String baseUrl = "https://exolix.com";
|
||||||
|
@Getter
|
||||||
|
final private String apiUrl = baseUrl + "/api/v2";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queryOrderParameters(CryptoAmount btcAmount, @NonNull final ShiftCallback<QueryOrderParameters> callback) {
|
||||||
|
QueryOrderParametersImpl.call(this, btcAmount, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createOrder(@NonNull final RequestQuote quote, @NonNull final String btcAddress,
|
||||||
|
@NonNull final ShiftCallback<CreateOrder> callback) {
|
||||||
|
CreateOrderImpl.call(this, btcAddress, quote, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queryOrderStatus(@NonNull final String uuid,
|
||||||
|
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
|
||||||
|
QueryOrderStatusImpl.call(this, uuid, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getQueryOrderUri(String orderId) {
|
||||||
|
return Uri.parse(getBaseUrl() + "/transaction/" + orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ShiftError createShiftError(ShiftError.Type type, final JSONObject jsonObject) {
|
||||||
|
try {
|
||||||
|
if (jsonObject.has("message")) {
|
||||||
|
final String message = jsonObject.getString("message");
|
||||||
|
if (!"null".equals(message))
|
||||||
|
return new ShiftError(type, message);
|
||||||
|
}
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
return new ShiftError(ShiftError.Type.INFRASTRUCTURE, "unknown");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void augment(@NonNull final NetCipherHelper.Request request, @Nullable final JSONObject data) {
|
||||||
|
request.setAugmenter((b) -> IdHelper.addHeader(b, "Authorization", BuildConfig.ID_F));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017-2021 m2049r et al.
|
||||||
|
*
|
||||||
|
* 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 com.m2049r.xmrwallet.service.shift.provider.exolix;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.data.Crypto;
|
||||||
|
import com.m2049r.xmrwallet.data.CryptoAmount;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
|
||||||
|
import com.m2049r.xmrwallet.util.AmountHelper;
|
||||||
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
class QueryOrderParametersImpl implements QueryOrderParameters {
|
||||||
|
|
||||||
|
private final double lowerLimit;
|
||||||
|
private final double price;
|
||||||
|
private final double upperLimit;
|
||||||
|
|
||||||
|
QueryOrderParametersImpl(final JSONObject jsonObject) throws JSONException {
|
||||||
|
price = jsonObject.getDouble("rate");
|
||||||
|
lowerLimit = jsonObject.getDouble("minAmount"); // XMR
|
||||||
|
upperLimit = jsonObject.getDouble("maxAmount"); // XMR
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void call(@NonNull final ShiftApiCall api, CryptoAmount btcAmount,
|
||||||
|
@NonNull final ShiftCallback<QueryOrderParameters> callback) {
|
||||||
|
if (btcAmount.getAmount() == 0) { // just checking rate without real amount
|
||||||
|
btcAmount = new CryptoAmount(Crypto.withSymbol(Helper.BASE_CRYPTO), 1); // might as well check for 1 XMR
|
||||||
|
}
|
||||||
|
final CryptoAmount cryptoAmount = btcAmount;
|
||||||
|
final StringBuilder params = new StringBuilder();
|
||||||
|
if (btcAmount.getCrypto() == Crypto.XMR) { // we are sending XMR, so float
|
||||||
|
params.append("rateType=float");
|
||||||
|
params.append("&amount=").append(AmountHelper.format(cryptoAmount.getAmount()));
|
||||||
|
params.append("&coinFrom=XMR");
|
||||||
|
} else { // we are receiving non-XMR, i.e. paying something, so fixed
|
||||||
|
params.append("rateType=fixed");
|
||||||
|
params.append("&withdrawalAmount=").append(AmountHelper.format(cryptoAmount.getAmount()));
|
||||||
|
params.append("&coinFrom=XMR");
|
||||||
|
}
|
||||||
|
params.append("&coinTo=").append(ShiftService.ASSET.getSymbol());
|
||||||
|
params.append("&networkTo=").append(ShiftService.ASSET.getNetwork());
|
||||||
|
|
||||||
|
api.get("rate", params.toString(), new NetworkCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(JSONObject jsonObject) {
|
||||||
|
try {
|
||||||
|
callback.onSuccess(new QueryOrderParametersImpl(jsonObject));
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception ex, JSONObject json) {
|
||||||
|
if ((json != null) && json.has("minAmount")) {
|
||||||
|
try {
|
||||||
|
final double lowerLimit = json.getDouble((cryptoAmount.getCrypto() == Crypto.XMR) ? "minAmount" : "withdrawMin");
|
||||||
|
call(api, cryptoAmount.newWithAmount(1.5 * lowerLimit), callback); // try again with 150% of minimum
|
||||||
|
} catch (JSONException jex) {
|
||||||
|
callback.onError(jex);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017-2021 m2049r et al.
|
||||||
|
*
|
||||||
|
* 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 com.m2049r.xmrwallet.service.shift.provider.exolix;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||||
|
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
|
||||||
|
import com.m2049r.xmrwallet.util.DateHelper;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
class QueryOrderStatusImpl extends QueryOrderStatus {
|
||||||
|
|
||||||
|
private QueryOrderStatusImpl(String orderId, Status status, String btcCurrency, double btcAmount, String btcAddress, double xmrAmount, String xmrAddress, Date createdAt, Date expiresAt) {
|
||||||
|
super(orderId, status, btcCurrency, btcAmount, btcAddress, xmrAmount, xmrAddress, createdAt, expiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static private Status getStatus(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case "wait":
|
||||||
|
return Status.WAITING;
|
||||||
|
case "confirmation":
|
||||||
|
case "confirmed":
|
||||||
|
return Status.PENDING;
|
||||||
|
case "exchanging":
|
||||||
|
case "sending":
|
||||||
|
return Status.SETTLING;
|
||||||
|
case "success":
|
||||||
|
return Status.SETTLED;
|
||||||
|
case "overdue":
|
||||||
|
return Status.EXPIRED;
|
||||||
|
case "refunded":
|
||||||
|
default:
|
||||||
|
return Status.UNDEFINED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static private QueryOrderStatusImpl of(final JSONObject jsonObject) throws JSONException {
|
||||||
|
final JSONObject coinFrom = jsonObject.getJSONObject("coinFrom");
|
||||||
|
final JSONObject coinTo = jsonObject.getJSONObject("coinTo");
|
||||||
|
// sanity checks
|
||||||
|
final String depositMethod = coinFrom.getString("coinCode");
|
||||||
|
final String settleMethod = coinTo.getString("coinCode");
|
||||||
|
if (!"xmr".equalsIgnoreCase(depositMethod)
|
||||||
|
|| !ShiftService.ASSET.getSymbol().equalsIgnoreCase(settleMethod))
|
||||||
|
throw new IllegalStateException();
|
||||||
|
|
||||||
|
final double btcAmount = jsonObject.getDouble("amountTo");
|
||||||
|
final String btcAddress = jsonObject.getString("withdrawalAddress");
|
||||||
|
|
||||||
|
final double xmrAmount = jsonObject.getDouble("amount");
|
||||||
|
final String xmrAddress = jsonObject.getString("depositAddress");
|
||||||
|
|
||||||
|
final String orderId = jsonObject.getString("id");
|
||||||
|
|
||||||
|
Date createdAt;
|
||||||
|
Date expiresAt;
|
||||||
|
try {
|
||||||
|
final String created = jsonObject.getString("createdAt");
|
||||||
|
createdAt = DateHelper.parse(created);
|
||||||
|
expiresAt = new Date(createdAt.getTime() + 300000);
|
||||||
|
} catch (ParseException ex) {
|
||||||
|
throw new JSONException(ex.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
final String status = jsonObject.getString("status");
|
||||||
|
|
||||||
|
return new QueryOrderStatusImpl(
|
||||||
|
orderId,
|
||||||
|
getStatus(status),
|
||||||
|
settleMethod,
|
||||||
|
btcAmount,
|
||||||
|
btcAddress,
|
||||||
|
xmrAmount,
|
||||||
|
xmrAddress,
|
||||||
|
createdAt,
|
||||||
|
expiresAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void call(@NonNull final ShiftApiCall api, @NonNull final String orderId,
|
||||||
|
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
|
||||||
|
api.get("transactions/" + orderId, null, new NetworkCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(JSONObject jsonObject) {
|
||||||
|
try {
|
||||||
|
callback.onSuccess(QueryOrderStatusImpl.of(jsonObject));
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception ex, JSONObject json) {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,65 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2017-2021 m2049r et al.
|
|
||||||
*
|
|
||||||
* 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 com.m2049r.xmrwallet.service.shift.sideshift.api;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public interface QueryOrderStatus {
|
|
||||||
enum State {
|
|
||||||
WAITING, // Waiting for mempool
|
|
||||||
PENDING, // Detected (waiting for confirmations)
|
|
||||||
SETTLING, // Settlement in progress
|
|
||||||
SETTLED, // Settlement completed
|
|
||||||
// no refunding in monerujo so theese are ignored:
|
|
||||||
// REFUND, // Queued for refund
|
|
||||||
// REFUNDING, // Refund in progress
|
|
||||||
// REFUNDED // Refund completed
|
|
||||||
UNDEFINED
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isCreated();
|
|
||||||
|
|
||||||
boolean isTerminal();
|
|
||||||
|
|
||||||
boolean isWaiting();
|
|
||||||
|
|
||||||
boolean isPending();
|
|
||||||
|
|
||||||
boolean isSent();
|
|
||||||
|
|
||||||
boolean isPaid();
|
|
||||||
|
|
||||||
boolean isError();
|
|
||||||
|
|
||||||
QueryOrderStatus.State getState();
|
|
||||||
|
|
||||||
String getOrderId();
|
|
||||||
|
|
||||||
Date getCreatedAt();
|
|
||||||
|
|
||||||
Date getExpiresAt();
|
|
||||||
|
|
||||||
double getBtcAmount();
|
|
||||||
|
|
||||||
String getBtcAddress();
|
|
||||||
|
|
||||||
double getXmrAmount();
|
|
||||||
|
|
||||||
String getXmrAddress();
|
|
||||||
|
|
||||||
double getPrice();
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2017-2021 m2049r et al.
|
|
||||||
*
|
|
||||||
* 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 com.m2049r.xmrwallet.service.shift.sideshift.network;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.BuildConfig;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
|
|
||||||
import com.m2049r.xmrwallet.util.DateHelper;
|
|
||||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
class CreateOrderImpl implements CreateOrder {
|
|
||||||
@Getter
|
|
||||||
private final String btcCurrency;
|
|
||||||
@Getter
|
|
||||||
private final double btcAmount;
|
|
||||||
@Getter
|
|
||||||
private final String btcAddress;
|
|
||||||
@Getter
|
|
||||||
private final String quoteId;
|
|
||||||
@Getter
|
|
||||||
private final String orderId;
|
|
||||||
@Getter
|
|
||||||
private final double xmrAmount;
|
|
||||||
@Getter
|
|
||||||
private final String xmrAddress;
|
|
||||||
@Getter
|
|
||||||
private final Date createdAt;
|
|
||||||
@Getter
|
|
||||||
private final Date expiresAt;
|
|
||||||
|
|
||||||
CreateOrderImpl(final JSONObject jsonObject) throws JSONException {
|
|
||||||
// sanity checks
|
|
||||||
final String depositMethod = jsonObject.getString("depositMethodId");
|
|
||||||
final String settleMethod = jsonObject.getString("settleMethodId");
|
|
||||||
if (!"xmr".equals(depositMethod) || !ServiceHelper.ASSET.equals(settleMethod))
|
|
||||||
throw new IllegalStateException();
|
|
||||||
|
|
||||||
btcCurrency = settleMethod.toUpperCase();
|
|
||||||
btcAmount = jsonObject.getDouble("settleAmount");
|
|
||||||
JSONObject settleAddress = jsonObject.getJSONObject("settleAddress");
|
|
||||||
btcAddress = settleAddress.getString("address");
|
|
||||||
|
|
||||||
xmrAmount = jsonObject.getDouble("depositAmount");
|
|
||||||
JSONObject depositAddress = jsonObject.getJSONObject("depositAddress");
|
|
||||||
xmrAddress = depositAddress.getString("address");
|
|
||||||
|
|
||||||
quoteId = jsonObject.getString("quoteId");
|
|
||||||
|
|
||||||
orderId = jsonObject.getString("orderId");
|
|
||||||
|
|
||||||
try {
|
|
||||||
final String created = jsonObject.getString("createdAtISO");
|
|
||||||
createdAt = DateHelper.parse(created);
|
|
||||||
final String expires = jsonObject.getString("expiresAtISO");
|
|
||||||
expiresAt = DateHelper.parse(expires);
|
|
||||||
} catch (ParseException ex) {
|
|
||||||
throw new JSONException(ex.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void call(@NonNull final ShiftApiCall api, final String quoteId, @NonNull final String btcAddress,
|
|
||||||
@NonNull final ShiftCallback<CreateOrder> callback) {
|
|
||||||
try {
|
|
||||||
final JSONObject request = createRequest(quoteId, btcAddress);
|
|
||||||
api.call("orders", request, new NetworkCallback() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(JSONObject jsonObject) {
|
|
||||||
try {
|
|
||||||
callback.onSuccess(new CreateOrderImpl(jsonObject));
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Exception ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static JSONObject createRequest(final String quoteId, final String address) throws JSONException {
|
|
||||||
final JSONObject jsonObject = new JSONObject();
|
|
||||||
jsonObject.put("type", "fixed");
|
|
||||||
jsonObject.put("quoteId", quoteId);
|
|
||||||
jsonObject.put("settleAddress", address);
|
|
||||||
if (!BuildConfig.ID_A.isEmpty() && !"null".equals(BuildConfig.ID_A)) {
|
|
||||||
jsonObject.put("affiliateId", BuildConfig.ID_A);
|
|
||||||
}
|
|
||||||
return jsonObject;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2017-2021 m2049r et al.
|
|
||||||
*
|
|
||||||
* 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 com.m2049r.xmrwallet.service.shift.sideshift.network;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
|
|
||||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
class QueryOrderParametersImpl implements QueryOrderParameters {
|
|
||||||
|
|
||||||
private double lowerLimit;
|
|
||||||
private double price;
|
|
||||||
private double upperLimit;
|
|
||||||
|
|
||||||
public double getLowerLimit() {
|
|
||||||
return lowerLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getPrice() {
|
|
||||||
return price;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getUpperLimit() {
|
|
||||||
return upperLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryOrderParametersImpl(final JSONObject jsonObject) throws JSONException {
|
|
||||||
lowerLimit = jsonObject.getDouble("min");
|
|
||||||
price = jsonObject.getDouble("rate");
|
|
||||||
upperLimit = jsonObject.getDouble("max");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void call(@NonNull final ShiftApiCall api,
|
|
||||||
@NonNull final ShiftCallback<QueryOrderParameters> callback) {
|
|
||||||
api.call("pairs/xmr/" + ServiceHelper.ASSET, new NetworkCallback() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(JSONObject jsonObject) {
|
|
||||||
try {
|
|
||||||
callback.onSuccess(new QueryOrderParametersImpl(jsonObject));
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Exception ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2017-2021 m2049r et al.
|
|
||||||
*
|
|
||||||
* 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 com.m2049r.xmrwallet.service.shift.sideshift.network;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
|
|
||||||
import com.m2049r.xmrwallet.util.DateHelper;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
class QueryOrderStatusImpl implements QueryOrderStatus {
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private QueryOrderStatus.State state;
|
|
||||||
@Getter
|
|
||||||
private final String orderId;
|
|
||||||
@Getter
|
|
||||||
private final Date createdAt;
|
|
||||||
@Getter
|
|
||||||
private final Date expiresAt;
|
|
||||||
@Getter
|
|
||||||
private final double btcAmount;
|
|
||||||
@Getter
|
|
||||||
private final String btcAddress;
|
|
||||||
@Getter
|
|
||||||
private final double xmrAmount;
|
|
||||||
@Getter
|
|
||||||
private final String xmrAddress;
|
|
||||||
|
|
||||||
public boolean isCreated() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isTerminal() {
|
|
||||||
return (state.equals(State.SETTLED) || isError());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isError() {
|
|
||||||
return state.equals(State.UNDEFINED);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isWaiting() {
|
|
||||||
return state.equals(State.WAITING);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPending() {
|
|
||||||
return state.equals(State.PENDING);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSent() {
|
|
||||||
return state.equals(State.SETTLING);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPaid() {
|
|
||||||
return state.equals(State.SETTLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getPrice() {
|
|
||||||
return btcAmount / xmrAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryOrderStatusImpl(final JSONObject jsonObject) throws JSONException {
|
|
||||||
try {
|
|
||||||
String created = jsonObject.getString("createdAtISO");
|
|
||||||
createdAt = DateHelper.parse(created);
|
|
||||||
String expires = jsonObject.getString("expiresAtISO");
|
|
||||||
expiresAt = DateHelper.parse(expires);
|
|
||||||
} catch (ParseException ex) {
|
|
||||||
throw new JSONException(ex.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
orderId = jsonObject.getString("orderId");
|
|
||||||
|
|
||||||
btcAmount = jsonObject.getDouble("settleAmount");
|
|
||||||
JSONObject settleAddress = jsonObject.getJSONObject("settleAddress");
|
|
||||||
btcAddress = settleAddress.getString("address");
|
|
||||||
|
|
||||||
xmrAmount = jsonObject.getDouble("depositAmount");
|
|
||||||
JSONObject depositAddress = jsonObject.getJSONObject("depositAddress");
|
|
||||||
xmrAddress = settleAddress.getString("address");
|
|
||||||
|
|
||||||
JSONArray deposits = jsonObject.getJSONArray("deposits");
|
|
||||||
// we only create one deposit, so die if there are more than one:
|
|
||||||
if (deposits.length() > 1)
|
|
||||||
throw new IllegalStateException("more than one deposits");
|
|
||||||
|
|
||||||
state = State.UNDEFINED;
|
|
||||||
if (deposits.length() == 0) {
|
|
||||||
state = State.WAITING;
|
|
||||||
} else if (deposits.length() == 1) {
|
|
||||||
// sanity check
|
|
||||||
if (!orderId.equals(deposits.getJSONObject(0).getString("orderId")))
|
|
||||||
throw new IllegalStateException("deposit has different order id!");
|
|
||||||
String stateName = deposits.getJSONObject(0).getString("status");
|
|
||||||
try {
|
|
||||||
state = State.valueOf(stateName.toUpperCase());
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
state = State.UNDEFINED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void call(@NonNull final ShiftApiCall api, @NonNull final String orderId,
|
|
||||||
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
|
|
||||||
api.call("orders/" + orderId, new NetworkCallback() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(JSONObject jsonObject) {
|
|
||||||
try {
|
|
||||||
callback.onSuccess(new QueryOrderStatusImpl(jsonObject));
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Exception ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2017-2021 m2049r et al.
|
|
||||||
*
|
|
||||||
* 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 com.m2049r.xmrwallet.service.shift.sideshift.network;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
|
|
||||||
import com.m2049r.xmrwallet.util.DateHelper;
|
|
||||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.text.DecimalFormatSymbols;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
class RequestQuoteImpl implements RequestQuote {
|
|
||||||
@Getter
|
|
||||||
private final double btcAmount;
|
|
||||||
@Getter
|
|
||||||
private final String id;
|
|
||||||
@Getter
|
|
||||||
private final Date createdAt;
|
|
||||||
@Getter
|
|
||||||
private final Date expiresAt;
|
|
||||||
@Getter
|
|
||||||
private final double xmrAmount;
|
|
||||||
@Getter
|
|
||||||
private final double price;
|
|
||||||
|
|
||||||
// TODO do something with errors - they always seem to send us 500
|
|
||||||
|
|
||||||
RequestQuoteImpl(final JSONObject jsonObject) throws JSONException {
|
|
||||||
// sanity checks
|
|
||||||
final String depositMethod = jsonObject.getString("depositMethod");
|
|
||||||
final String settleMethod = jsonObject.getString("settleMethod");
|
|
||||||
if (!"xmr".equals(depositMethod) || !ServiceHelper.ASSET.equals(settleMethod))
|
|
||||||
throw new IllegalStateException();
|
|
||||||
|
|
||||||
btcAmount = jsonObject.getDouble("settleAmount");
|
|
||||||
id = jsonObject.getString("id");
|
|
||||||
|
|
||||||
try {
|
|
||||||
final String created = jsonObject.getString("createdAt");
|
|
||||||
createdAt = DateHelper.parse(created);
|
|
||||||
final String expires = jsonObject.getString("expiresAt");
|
|
||||||
expiresAt = DateHelper.parse(expires);
|
|
||||||
} catch (ParseException ex) {
|
|
||||||
throw new JSONException(ex.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
xmrAmount = jsonObject.getDouble("depositAmount");
|
|
||||||
price = jsonObject.getDouble("rate");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void call(@NonNull final ShiftApiCall api, final double btcAmount,
|
|
||||||
@NonNull final ShiftCallback<RequestQuote> callback) {
|
|
||||||
try {
|
|
||||||
final JSONObject request = createRequest(btcAmount);
|
|
||||||
api.call("quotes", request, new NetworkCallback() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(JSONObject jsonObject) {
|
|
||||||
try {
|
|
||||||
callback.onSuccess(new RequestQuoteImpl(jsonObject));
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Exception ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create JSON request object
|
|
||||||
*
|
|
||||||
* @param btcAmount how much XMR to shift to BTC
|
|
||||||
*/
|
|
||||||
|
|
||||||
static JSONObject createRequest(final double btcAmount) throws JSONException {
|
|
||||||
final JSONObject jsonObject = new JSONObject();
|
|
||||||
jsonObject.put("depositMethod", "xmr");
|
|
||||||
jsonObject.put("settleMethod", ServiceHelper.ASSET);
|
|
||||||
// #sideshift is silly and likes numbers as strings
|
|
||||||
String amount = AmountFormatter.format(btcAmount);
|
|
||||||
jsonObject.put("settleAmount", amount);
|
|
||||||
return jsonObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
static final DecimalFormat AmountFormatter;
|
|
||||||
|
|
||||||
static {
|
|
||||||
AmountFormatter = new DecimalFormat();
|
|
||||||
AmountFormatter.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US));
|
|
||||||
AmountFormatter.setMinimumIntegerDigits(1);
|
|
||||||
AmountFormatter.setMaximumFractionDigits(12);
|
|
||||||
AmountFormatter.setGroupingUsed(false);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2017-2021 m2049r et al.
|
|
||||||
*
|
|
||||||
* 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 com.m2049r.xmrwallet.service.shift.sideshift.network;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftError;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
|
|
||||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
|
||||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import okhttp3.Call;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class SideShiftApiImpl implements SideShiftApi, ShiftApiCall {
|
|
||||||
|
|
||||||
private final HttpUrl baseUrl;
|
|
||||||
|
|
||||||
public SideShiftApiImpl(final HttpUrl baseUrl) {
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void queryOrderParameters(@NonNull final ShiftCallback<QueryOrderParameters> callback) {
|
|
||||||
QueryOrderParametersImpl.call(this, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void requestQuote(final double btcAmount, @NonNull final ShiftCallback<RequestQuote> callback) {
|
|
||||||
RequestQuoteImpl.call(this, btcAmount, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void createOrder(final String quoteId, @NonNull final String btcAddress,
|
|
||||||
@NonNull final ShiftCallback<CreateOrder> callback) {
|
|
||||||
CreateOrderImpl.call(this, quoteId, btcAddress, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void queryOrderStatus(@NonNull final String uuid,
|
|
||||||
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
|
|
||||||
QueryOrderStatusImpl.call(this, uuid, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri getQueryOrderUri(String orderId) {
|
|
||||||
return Uri.parse("https://sideshift.ai/orders/" + orderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void call(@NonNull final String path, @NonNull final NetworkCallback callback) {
|
|
||||||
call(path, null, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback) {
|
|
||||||
final HttpUrl url = baseUrl.newBuilder()
|
|
||||||
.addPathSegments(path)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url, request);
|
|
||||||
httpRequest.enqueue(new okhttp3.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onFailure(final Call call, final IOException ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
|
|
||||||
Timber.d("onResponse code=%d", response.code());
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
try {
|
|
||||||
final JSONObject json = new JSONObject(response.body().string());
|
|
||||||
callback.onSuccess(json);
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
callback.onError(ex);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
final JSONObject json = new JSONObject(response.body().string());
|
|
||||||
Timber.d(json.toString(2));
|
|
||||||
final ShiftError error = new ShiftError(json);
|
|
||||||
Timber.w("%s says %d/%s", CreateOrder.TAG, response.code(), error.toString());
|
|
||||||
callback.onError(new ShiftException(response.code(), error));
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
callback.onError(new ShiftException(response.code()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.m2049r.xmrwallet.util;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.DecimalFormatSymbols;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class AmountHelper {
|
||||||
|
static final DecimalFormat AmountFormatter_12;
|
||||||
|
|
||||||
|
static {
|
||||||
|
AmountFormatter_12 = new DecimalFormat();
|
||||||
|
AmountFormatter_12.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US));
|
||||||
|
AmountFormatter_12.setMinimumIntegerDigits(1);
|
||||||
|
AmountFormatter_12.setMaximumFractionDigits(12);
|
||||||
|
AmountFormatter_12.setGroupingUsed(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String format(double amount) {
|
||||||
|
return AmountFormatter_12.format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String format_6(double amount) {
|
||||||
|
int n = (int) Math.ceil(Math.log10(amount));
|
||||||
|
int d = Math.max(2, 6 - n);
|
||||||
|
final String fmt = "%,." + d + "f";
|
||||||
|
return String.format(Locale.getDefault(), fmt, amount);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.m2049r.xmrwallet.util;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
public class Flasher {
|
||||||
|
public interface Light {
|
||||||
|
int getDrawableId();
|
||||||
|
}
|
||||||
|
|
||||||
|
final private static int ON_TIME = 80; //ms
|
||||||
|
final private static int DURATION = 100 + ON_TIME; //ms
|
||||||
|
final private static int OFF_TIME = 600; //ms
|
||||||
|
@Getter
|
||||||
|
final private Drawable drawable;
|
||||||
|
private long t;
|
||||||
|
private ValueAnimator animator;
|
||||||
|
|
||||||
|
private final int colorOff, colorOn;
|
||||||
|
private int colorCurrent;
|
||||||
|
|
||||||
|
public Flasher(Context ctx, Light light) {
|
||||||
|
colorOff = ThemeHelper.getThemedColor(ctx, android.R.attr.colorControlNormal);
|
||||||
|
colorOn = ThemeHelper.getThemedColor(ctx, android.R.attr.colorControlActivated);
|
||||||
|
drawable = getDrawable(ctx, light.getDrawableId());
|
||||||
|
drawable.setTint(colorOff);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flash(View view) {
|
||||||
|
if (view == null) return;
|
||||||
|
if (animator != null) animator.cancel();
|
||||||
|
|
||||||
|
final long now = System.currentTimeMillis();
|
||||||
|
t = now;
|
||||||
|
|
||||||
|
animator = ValueAnimator.ofArgb(colorOff, colorOn); // always blink nomatter what
|
||||||
|
animator.addUpdateListener(valueAnimator -> {
|
||||||
|
colorCurrent = (Integer) valueAnimator.getAnimatedValue();
|
||||||
|
drawable.setTint(colorCurrent);
|
||||||
|
});
|
||||||
|
animator.setDuration(ON_TIME);
|
||||||
|
animator.start();
|
||||||
|
view.postDelayed(() -> {
|
||||||
|
if (t == now) { // only turn it off if we turned it on last
|
||||||
|
animator = ValueAnimator.ofArgb(colorCurrent, colorOff);
|
||||||
|
animator.addUpdateListener(valueAnimator -> {
|
||||||
|
colorCurrent = (Integer) valueAnimator.getAnimatedValue();
|
||||||
|
drawable.setTint(colorCurrent);
|
||||||
|
});
|
||||||
|
animator.setDuration(Math.abs((long) (1. * OFF_TIME * ((colorCurrent - colorOff) / (colorOn - colorOff)))));
|
||||||
|
animator.start();
|
||||||
|
}
|
||||||
|
}, DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Drawable getDrawable(Context ctx, int drawableId) {
|
||||||
|
return ResourcesCompat.getDrawable(ctx.getResources(), drawableId, null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,12 +78,13 @@ import timber.log.Timber;
|
||||||
public class Helper {
|
public class Helper {
|
||||||
static public final String NOCRAZYPASS_FLAGFILE = ".nocrazypass";
|
static public final String NOCRAZYPASS_FLAGFILE = ".nocrazypass";
|
||||||
|
|
||||||
static public final String BASE_CRYPTO = Crypto.XMR.getSymbol();
|
static public final Crypto BASE_CRYPTO_CRYPTO = Crypto.XMR;
|
||||||
|
static public final String BASE_CRYPTO = BASE_CRYPTO_CRYPTO.getSymbol();
|
||||||
static public final int XMR_DECIMALS = 12;
|
static public final int XMR_DECIMALS = 12;
|
||||||
static public final long ONE_XMR = Math.round(Math.pow(10, Helper.XMR_DECIMALS));
|
static public final long ONE_XMR = Math.round(Math.pow(10, Helper.XMR_DECIMALS));
|
||||||
|
|
||||||
static public final boolean SHOW_EXCHANGERATES = true;
|
static public final boolean SHOW_EXCHANGERATES = true;
|
||||||
static public boolean ALLOW_SHIFT = false;
|
static public boolean ALLOW_SHIFT = true;
|
||||||
|
|
||||||
static private final String WALLET_DIR = "wallets";
|
static private final String WALLET_DIR = "wallets";
|
||||||
static private final String MONERO_DIR = "monero";
|
static private final String MONERO_DIR = "monero";
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.m2049r.xmrwallet.util;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.BuildConfig;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import okhttp3.Request;
|
||||||
|
|
||||||
|
public class IdHelper {
|
||||||
|
static public String idOrNot(String id) {
|
||||||
|
return isId(id) ? id : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static public boolean isId(String id) {
|
||||||
|
return (id != null) && !id.isEmpty() && !"null".equals(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public String asParameter(String name, String id) {
|
||||||
|
return isId(id) ? (name + "=" + id) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static public void jsonPut(JSONObject jsonObject, String name, String id) throws JSONException {
|
||||||
|
if (isId(id)) {
|
||||||
|
jsonObject.put(name, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static public void addHeader(Request.Builder builder, String name, String id) {
|
||||||
|
if (isId(id)) {
|
||||||
|
builder.addHeader(name, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,8 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
|
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
|
||||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
|
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
|
||||||
import com.burgstaller.okhttp.digest.CachingAuthenticator;
|
import com.burgstaller.okhttp.digest.CachingAuthenticator;
|
||||||
|
@ -39,10 +41,11 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import info.guardianproject.netcipher.client.StrongOkHttpClientBuilder;
|
import info.guardianproject.netcipher.client.StrongOkHttpClientBuilder;
|
||||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
import info.guardianproject.netcipher.proxy.MyOrbotHelper;
|
||||||
import info.guardianproject.netcipher.proxy.SignatureUtils;
|
import info.guardianproject.netcipher.proxy.SignatureUtils;
|
||||||
import info.guardianproject.netcipher.proxy.StatusCallback;
|
import info.guardianproject.netcipher.proxy.StatusCallback;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
import okhttp3.Callback;
|
import okhttp3.Callback;
|
||||||
|
@ -56,11 +59,11 @@ import timber.log.Timber;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class NetCipherHelper implements StatusCallback {
|
public class NetCipherHelper implements StatusCallback {
|
||||||
public static final String USER_AGENT = "Monerujo/1.0";
|
public static final String USER_AGENT = "Monerujo/1.0";
|
||||||
public static final int HTTP_TIMEOUT_CONNECT = 1000; //ms
|
public static final int HTTP_TIMEOUT_CONNECT = 2500; //ms
|
||||||
public static final int HTTP_TIMEOUT_READ = 2000; //ms
|
public static final int HTTP_TIMEOUT_READ = 5000; //ms
|
||||||
public static final int HTTP_TIMEOUT_WRITE = 1000; //ms
|
public static final int HTTP_TIMEOUT_WRITE = 2500; //ms
|
||||||
public static final int TOR_TIMEOUT_CONNECT = 5000; //ms
|
public static final int TOR_TIMEOUT_CONNECT = 10000; //ms
|
||||||
public static final int TOR_TIMEOUT = 2000; //ms
|
public static final int TOR_TIMEOUT = 5000; //ms
|
||||||
|
|
||||||
public interface OnStatusChangedListener {
|
public interface OnStatusChangedListener {
|
||||||
void connected();
|
void connected();
|
||||||
|
@ -73,7 +76,7 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
final private Context context;
|
final private Context context;
|
||||||
final private OrbotHelper orbot;
|
final private MyOrbotHelper orbot;
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private static NetCipherHelper Instance;
|
private static NetCipherHelper Instance;
|
||||||
|
@ -83,7 +86,7 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
synchronized (NetCipherHelper.class) {
|
synchronized (NetCipherHelper.class) {
|
||||||
if (Instance == null) {
|
if (Instance == null) {
|
||||||
final Context applicationContext = context.getApplicationContext();
|
final Context applicationContext = context.getApplicationContext();
|
||||||
Instance = new NetCipherHelper(applicationContext, OrbotHelper.get(context).statusTimeout(5000));
|
Instance = new NetCipherHelper(applicationContext, MyOrbotHelper.get(context).statusTimeout(5000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,9 +100,9 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
private OkHttpClient client;
|
private OkHttpClient client;
|
||||||
|
|
||||||
private void createTorClient(Intent statusIntent) {
|
private void createTorClient(Intent statusIntent) {
|
||||||
String orbotStatus = statusIntent.getStringExtra(OrbotHelper.EXTRA_STATUS);
|
String orbotStatus = statusIntent.getStringExtra(MyOrbotHelper.EXTRA_STATUS);
|
||||||
if (orbotStatus == null) throw new IllegalStateException("status is null");
|
if (orbotStatus == null) throw new IllegalStateException("status is null");
|
||||||
if (!orbotStatus.equals(OrbotHelper.STATUS_ON))
|
if (!orbotStatus.equals(MyOrbotHelper.STATUS_ON))
|
||||||
throw new IllegalStateException("Orbot is not ON");
|
throw new IllegalStateException("Orbot is not ON");
|
||||||
try {
|
try {
|
||||||
final OkHttpClient.Builder okBuilder = new OkHttpClient.Builder()
|
final OkHttpClient.Builder okBuilder = new OkHttpClient.Builder()
|
||||||
|
@ -110,7 +113,6 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
.withSocksProxy()
|
.withSocksProxy()
|
||||||
.applyTo(okBuilder, statusIntent)
|
.applyTo(okBuilder, statusIntent)
|
||||||
.build();
|
.build();
|
||||||
Helper.ALLOW_SHIFT = false; // no shifting with Tor
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new IllegalStateException(ex);
|
throw new IllegalStateException(ex);
|
||||||
}
|
}
|
||||||
|
@ -123,7 +125,6 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
.writeTimeout(HTTP_TIMEOUT_WRITE, TimeUnit.MILLISECONDS)
|
.writeTimeout(HTTP_TIMEOUT_WRITE, TimeUnit.MILLISECONDS)
|
||||||
.readTimeout(HTTP_TIMEOUT_READ, TimeUnit.MILLISECONDS)
|
.readTimeout(HTTP_TIMEOUT_READ, TimeUnit.MILLISECONDS)
|
||||||
.build();
|
.build();
|
||||||
Helper.ALLOW_SHIFT = true;
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new IllegalStateException(ex);
|
throw new IllegalStateException(ex);
|
||||||
}
|
}
|
||||||
|
@ -144,7 +145,7 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
.addStatusCallback(me);
|
.addStatusCallback(me);
|
||||||
|
|
||||||
// deal with org.torproject.android.intent.action.STATUS = STARTS_DISABLED
|
// deal with org.torproject.android.intent.action.STATUS = STARTS_DISABLED
|
||||||
me.context.registerReceiver(orbotStatusReceiver, new IntentFilter(OrbotHelper.ACTION_STATUS));
|
ContextCompat.registerReceiver(me.context, orbotStatusReceiver, new IntentFilter(MyOrbotHelper.ACTION_STATUS), ContextCompat.RECEIVER_EXPORTED);
|
||||||
|
|
||||||
me.startTor();
|
me.startTor();
|
||||||
}
|
}
|
||||||
|
@ -270,7 +271,7 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
hashes.add("A7:02:07:92:4F:61:FF:09:37:1D:54:84:14:5C:4B:EE:77:2C:55:C1:9E:EE:23:2F:57:70:E1:82:71:F7:CB:AE");
|
hashes.add("A7:02:07:92:4F:61:FF:09:37:1D:54:84:14:5C:4B:EE:77:2C:55:C1:9E:EE:23:2F:57:70:E1:82:71:F7:CB:AE");
|
||||||
|
|
||||||
return null != SignatureUtils.validateBroadcastIntent(context,
|
return null != SignatureUtils.validateBroadcastIntent(context,
|
||||||
OrbotHelper.getOrbotStartIntent(context),
|
MyOrbotHelper.getOrbotStartIntent(context),
|
||||||
hashes, false);
|
hashes, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,19 +294,36 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
@ToString
|
@ToString
|
||||||
static public class Request {
|
static public class Request {
|
||||||
final HttpUrl url;
|
final HttpUrl url;
|
||||||
final String json;
|
final JSONObject data;
|
||||||
final String username;
|
final String username;
|
||||||
final String password;
|
final String password;
|
||||||
|
@Setter
|
||||||
|
RequestAugmenter augmenter;
|
||||||
|
|
||||||
public Request(final HttpUrl url, final String json, final String username, final String password) {
|
final Method method;
|
||||||
this.url = url;
|
|
||||||
this.json = json;
|
public enum Method {
|
||||||
this.username = username;
|
GET, POST;
|
||||||
this.password = password;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Request(final HttpUrl url, final JSONObject json) {
|
public interface RequestAugmenter {
|
||||||
this(url, json == null ? null : json.toString(), null, null);
|
void augment(okhttp3.Request.Builder builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Request(final HttpUrl url, final JSONObject data, final String username, final String password) {
|
||||||
|
this.url = url;
|
||||||
|
this.data = data;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
if (data == null) {
|
||||||
|
method = Method.GET;
|
||||||
|
} else {
|
||||||
|
method = Method.POST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Request(final HttpUrl url, final JSONObject data) {
|
||||||
|
this(url, data, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Request(final HttpUrl url) {
|
public Request(final HttpUrl url) {
|
||||||
|
@ -344,11 +362,16 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
final okhttp3.Request.Builder builder =
|
final okhttp3.Request.Builder builder =
|
||||||
new okhttp3.Request.Builder()
|
new okhttp3.Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.header("User-Agent", USER_AGENT);
|
.header("User-Agent", USER_AGENT)
|
||||||
if (json != null) {
|
.header("Accept", "application/json");
|
||||||
builder.post(RequestBody.create(json, MediaType.parse("application/json")));
|
if (augmenter != null) augmenter.augment(builder);
|
||||||
} else {
|
switch (method) {
|
||||||
|
case GET:
|
||||||
builder.get();
|
builder.get();
|
||||||
|
break;
|
||||||
|
case POST:
|
||||||
|
builder.post(RequestBody.create(data.toString(), MediaType.parse("application/json")));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
@ -380,9 +403,9 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
private static final BroadcastReceiver orbotStatusReceiver = new BroadcastReceiver() {
|
private static final BroadcastReceiver orbotStatusReceiver = new BroadcastReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
Timber.d("%s/%s", intent.getAction(), intent.getStringExtra(OrbotHelper.EXTRA_STATUS));
|
Timber.d("%s/%s", intent.getAction(), intent.getStringExtra(MyOrbotHelper.EXTRA_STATUS));
|
||||||
if (OrbotHelper.ACTION_STATUS.equals(intent.getAction())) {
|
if (MyOrbotHelper.ACTION_STATUS.equals(intent.getAction())) {
|
||||||
if (OrbotHelper.STATUS_STARTS_DISABLED.equals(intent.getStringExtra(OrbotHelper.EXTRA_STATUS))) {
|
if (MyOrbotHelper.STATUS_STARTS_DISABLED.equals(intent.getStringExtra(MyOrbotHelper.EXTRA_STATUS))) {
|
||||||
getInstance().onNotEnabled();
|
getInstance().onNotEnabled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -390,6 +413,6 @@ public class NetCipherHelper implements StatusCallback {
|
||||||
};
|
};
|
||||||
|
|
||||||
public void installOrbot(Activity host) {
|
public void installOrbot(Activity host) {
|
||||||
host.startActivity(OrbotHelper.getOrbotInstallIntent(context));
|
host.startActivity(MyOrbotHelper.getOrbotInstallIntent(context));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,8 +143,8 @@ public class OpenAliasHelper {
|
||||||
for (String txt : txts) {
|
for (String txt : txts) {
|
||||||
BarcodeData bc = BarcodeData.parseOpenAlias(txt, dnssec);
|
BarcodeData bc = BarcodeData.parseOpenAlias(txt, dnssec);
|
||||||
if (bc != null) {
|
if (bc != null) {
|
||||||
if (!dataMap.containsKey(bc.asset)) {
|
if (!dataMap.containsKey(bc.getAsset())) {
|
||||||
dataMap.put(bc.asset, bc);
|
dataMap.put(bc.getAsset(), bc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,16 +7,6 @@ import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
public class ServiceHelper {
|
public class ServiceHelper {
|
||||||
public static String ASSET = null;
|
|
||||||
|
|
||||||
static public HttpUrl getXmrToBaseUrl() {
|
|
||||||
if ((WalletManager.getInstance() == null)
|
|
||||||
|| (WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet)) {
|
|
||||||
throw new IllegalStateException("Only mainnet not supported");
|
|
||||||
} else {
|
|
||||||
return HttpUrl.parse("https://sideshift.ai/api/v1/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static public ExchangeApi getExchangeApi() {
|
static public ExchangeApi getExchangeApi() {
|
||||||
return new com.m2049r.xmrwallet.service.exchange.krakenFiat.ExchangeApiImpl();
|
return new com.m2049r.xmrwallet.service.exchange.krakenFiat.ExchangeApiImpl();
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
package com.m2049r.xmrwallet.util.validator;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
public enum BitcoinAddressType {
|
|
||||||
BTC(Type.BTC, Type.BTC_BECH32_PREFIX),
|
|
||||||
LTC(Type.LTC, Type.LTC_BECH32_PREFIX),
|
|
||||||
DASH(Type.DASH, null),
|
|
||||||
DOGE(Type.DOGE, null);
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final byte[] production;
|
|
||||||
@Getter
|
|
||||||
private final byte[] testnet;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final String productionBech32Prefix;
|
|
||||||
@Getter
|
|
||||||
private final String testnetBech32Prefix;
|
|
||||||
|
|
||||||
public boolean hasBech32() {
|
|
||||||
return productionBech32Prefix != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBech32Prefix(boolean testnet) {
|
|
||||||
return testnet ? testnetBech32Prefix : productionBech32Prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
BitcoinAddressType(byte[][] types, String[] bech32Prefix) {
|
|
||||||
production = types[0];
|
|
||||||
testnet = types[1];
|
|
||||||
if (bech32Prefix != null) {
|
|
||||||
productionBech32Prefix = bech32Prefix[0];
|
|
||||||
testnetBech32Prefix = bech32Prefix[1];
|
|
||||||
} else {
|
|
||||||
productionBech32Prefix = null;
|
|
||||||
testnetBech32Prefix = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Java is silly and doesn't allow array initializers in the construction
|
|
||||||
private static class Type {
|
|
||||||
private static final byte[][] BTC = {{0x00, 0x05}, {0x6f, (byte) 0xc4}};
|
|
||||||
private static final String[] BTC_BECH32_PREFIX = {"bc", "tb"};
|
|
||||||
private static final byte[][] LTC = {{0x30, 0x05, 0x32}, {0x6f, (byte) 0xc4, 0x3a}};
|
|
||||||
private static final String[] LTC_BECH32_PREFIX = {"ltc", "tltc"};
|
|
||||||
private static final byte[][] DASH = {{0x4c, 0x10}, {(byte) 0x8c, 0x13}};
|
|
||||||
private static final byte[][] DOGE = {{0x1e, 0x16}, {0x71, (byte) 0xc4}};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,220 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2017 m2049r er al.
|
|
||||||
*
|
|
||||||
* 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 com.m2049r.xmrwallet.util.validator;
|
|
||||||
|
|
||||||
// mostly based on https://rosettacode.org/wiki/Bitcoin/address_validation#Java
|
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.data.Crypto;
|
|
||||||
import com.m2049r.xmrwallet.model.NetworkType;
|
|
||||||
import com.m2049r.xmrwallet.model.WalletManager;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class BitcoinAddressValidator {
|
|
||||||
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
||||||
|
|
||||||
public static Crypto validate(String address) {
|
|
||||||
for (BitcoinAddressType type : BitcoinAddressType.values()) {
|
|
||||||
if (validate(address, type))
|
|
||||||
return Crypto.valueOf(type.name());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// just for tests
|
|
||||||
public static boolean validateBTC(String addrress, boolean testnet) {
|
|
||||||
return validate(addrress, BitcoinAddressType.BTC, testnet);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean validate(String addrress, BitcoinAddressType type, boolean testnet) {
|
|
||||||
if (validate(addrress, testnet ? type.getTestnet() : type.getProduction()))
|
|
||||||
return true;
|
|
||||||
if (type.hasBech32())
|
|
||||||
return validateBech32Segwit(addrress, type, testnet);
|
|
||||||
else
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean validate(String addrress, BitcoinAddressType type) {
|
|
||||||
final boolean testnet = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet;
|
|
||||||
return validate(addrress, type, testnet);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean validate(String addrress, byte[] addressTypes) {
|
|
||||||
if (addrress.length() < 26 || addrress.length() > 35)
|
|
||||||
return false;
|
|
||||||
byte[] decoded = decodeBase58To25Bytes(addrress);
|
|
||||||
if (decoded == null)
|
|
||||||
return false;
|
|
||||||
int v = decoded[0] & 0xFF;
|
|
||||||
boolean nok = true;
|
|
||||||
for (byte b : addressTypes) {
|
|
||||||
nok = nok && (v != (b & 0xFF));
|
|
||||||
}
|
|
||||||
if (nok) return false;
|
|
||||||
|
|
||||||
byte[] hash1 = sha256(Arrays.copyOfRange(decoded, 0, 21));
|
|
||||||
byte[] hash2 = sha256(hash1);
|
|
||||||
|
|
||||||
return Arrays.equals(Arrays.copyOfRange(hash2, 0, 4), Arrays.copyOfRange(decoded, 21, 25));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] decodeBase58To25Bytes(String input) {
|
|
||||||
BigInteger num = BigInteger.ZERO;
|
|
||||||
for (char t : input.toCharArray()) {
|
|
||||||
int p = ALPHABET.indexOf(t);
|
|
||||||
if (p == -1)
|
|
||||||
return null;
|
|
||||||
num = num.multiply(BigInteger.valueOf(58)).add(BigInteger.valueOf(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] result = new byte[25];
|
|
||||||
byte[] numBytes = num.toByteArray();
|
|
||||||
if (num.bitLength() > 200) return null;
|
|
||||||
|
|
||||||
if (num.bitLength() == 200) {
|
|
||||||
System.arraycopy(numBytes, 1, result, 0, 25);
|
|
||||||
} else {
|
|
||||||
System.arraycopy(numBytes, 0, result, result.length - numBytes.length, numBytes.length);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] sha256(byte[] data) {
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
md.update(data);
|
|
||||||
return md.digest();
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new IllegalStateException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// validate Bech32 segwit
|
|
||||||
// see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki for spec
|
|
||||||
//
|
|
||||||
|
|
||||||
private static final String DATA_CHARS = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
||||||
|
|
||||||
public static boolean validateBech32Segwit(String bech32, BitcoinAddressType type, boolean testnet) {
|
|
||||||
if (!bech32.equals(bech32.toLowerCase()) && !bech32.equals(bech32.toUpperCase())) {
|
|
||||||
return false; // mixing upper and lower case not allowed
|
|
||||||
}
|
|
||||||
bech32 = bech32.toLowerCase();
|
|
||||||
|
|
||||||
if (!bech32.startsWith(type.getBech32Prefix(testnet))) return false;
|
|
||||||
|
|
||||||
final int hrpLength = type.getBech32Prefix(testnet).length();
|
|
||||||
|
|
||||||
if ((bech32.length() < (12 + hrpLength)) || (bech32.length() > (72 + hrpLength)))
|
|
||||||
return false;
|
|
||||||
int mod = (bech32.length() - hrpLength) % 8;
|
|
||||||
if ((mod == 6) || (mod == 1) || (mod == 3)) return false;
|
|
||||||
|
|
||||||
int sep = -1;
|
|
||||||
final byte[] bytes = bech32.getBytes(StandardCharsets.US_ASCII);
|
|
||||||
for (int i = 0; i < bytes.length; i++) {
|
|
||||||
if ((bytes[i] < 33) || (bytes[i] > 126)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (bytes[i] == 49) sep = i; // 49 := '1' in ASCII
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sep != hrpLength) return false;
|
|
||||||
if (sep > bytes.length - 7) {
|
|
||||||
return false; // min 6 bytes data
|
|
||||||
}
|
|
||||||
if (bytes.length < 8) { // hrp{min}=1 + sep=1 + data{min}=6 := 8
|
|
||||||
return false; // too short
|
|
||||||
}
|
|
||||||
if (bytes.length > 90) {
|
|
||||||
return false; // too long
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] hrp = Arrays.copyOfRange(bytes, 0, sep);
|
|
||||||
|
|
||||||
final byte[] data = Arrays.copyOfRange(bytes, sep + 1, bytes.length);
|
|
||||||
for (int i = 0; i < data.length; i++) {
|
|
||||||
int b = DATA_CHARS.indexOf(data[i]);
|
|
||||||
if (b < 0) return false; // invalid character
|
|
||||||
data[i] = (byte) b;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateBech32Data(data)) return false;
|
|
||||||
|
|
||||||
return verifyChecksum(hrp, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int polymod(byte[] values) {
|
|
||||||
final int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
|
|
||||||
int chk = 1;
|
|
||||||
for (byte v : values) {
|
|
||||||
byte b = (byte) (chk >> 25);
|
|
||||||
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
chk ^= ((b >> i) & 1) == 1 ? GEN[i] : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chk;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] hrpExpand(byte[] hrp) {
|
|
||||||
final byte[] expanded = new byte[(2 * hrp.length) + 1];
|
|
||||||
int i = 0;
|
|
||||||
for (byte b : hrp) {
|
|
||||||
expanded[i++] = (byte) (b >> 5);
|
|
||||||
}
|
|
||||||
expanded[i++] = 0;
|
|
||||||
for (byte b : hrp) {
|
|
||||||
expanded[i++] = (byte) (b & 0x1f);
|
|
||||||
}
|
|
||||||
return expanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean verifyChecksum(byte[] hrp, byte[] data) {
|
|
||||||
final byte[] hrpExpanded = hrpExpand(hrp);
|
|
||||||
final byte[] values = new byte[hrpExpanded.length + data.length];
|
|
||||||
System.arraycopy(hrpExpanded, 0, values, 0, hrpExpanded.length);
|
|
||||||
System.arraycopy(data, 0, values, hrpExpanded.length, data.length);
|
|
||||||
return (polymod(values) == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean validateBech32Data(final byte[] data) {
|
|
||||||
if ((data[0] < 0) || (data[0] > 16)) return false; // witness version
|
|
||||||
final int programLength = data.length - 1 - 6; // 1-byte version at beginning & 6-byte checksum at end
|
|
||||||
|
|
||||||
// since we are coming from our own decoder, we don't need to verify data is 5-bit bytes
|
|
||||||
|
|
||||||
final int convertedSize = programLength * 5 / 8;
|
|
||||||
final int remainderSize = programLength * 5 % 8;
|
|
||||||
|
|
||||||
if ((convertedSize < 2) || (convertedSize > 40)) return false;
|
|
||||||
|
|
||||||
if ((data[0] == 0) && (convertedSize != 20) && (convertedSize != 32)) return false;
|
|
||||||
|
|
||||||
if (remainderSize >= 5) return false;
|
|
||||||
// ignore checksum at end and get last byte of program
|
|
||||||
if ((data[data.length - 1 - 6] & ((1 << remainderSize) - 1)) != 0) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue