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
|
||||
/app/build
|
||||
/app/release
|
||||
/app/alpha
|
||||
/app/prod
|
||||
/app/alphaMainnet
|
||||
/app/prodMainnet
|
||||
/app/alphaStagenet
|
||||
/app/prodStagenet
|
||||
/app/alpha*
|
||||
/app/prod*
|
||||
/app/.cxx
|
||||
/monerujo.id
|
||||
/external-libs/VERSION
|
||||
|
|
|
@ -4,12 +4,12 @@ android {
|
|||
ndkVersion '17.2.4988734'
|
||||
defaultConfig {
|
||||
applicationId "com.m2049r.xmrwallet"
|
||||
buildToolsVersion = '34.0.0'
|
||||
compileSdk 34
|
||||
buildToolsVersion = '35.0.0'
|
||||
compileSdk 35
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 3311
|
||||
versionName "3.3.11 'Argentina'"
|
||||
targetSdkVersion 35
|
||||
versionCode 4104
|
||||
versionName "4.1.4 'Exolix'"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
|
@ -24,7 +24,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
flavorDimensions 'type', 'net'
|
||||
flavorDimensions = ['type', 'net']
|
||||
productFlavors {
|
||||
mainnet {
|
||||
dimension 'net'
|
||||
|
@ -58,7 +58,7 @@ android {
|
|||
applicationIdSuffix ".debug"
|
||||
}
|
||||
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 {
|
||||
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.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core:1.13.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
|
@ -143,7 +143,7 @@ dependencies {
|
|||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
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 "com.squareup.okhttp3:okhttp:4.12.0"
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
android:name="android.hardware.camera"
|
||||
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.READ_EXTERNAL_STORAGE" />
|
||||
<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.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
|
@ -98,7 +104,12 @@
|
|||
android:name=".service.WalletService"
|
||||
android:description="@string/service_description"
|
||||
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
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) 2017 m2049r
|
||||
* Copyright (c) 2017-2024 m2049r
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -18,8 +18,6 @@
|
|||
#include "monerujo.h"
|
||||
#include "wallet2_api.h"
|
||||
|
||||
//TODO explicit casting jlong, jint, jboolean to avoid warnings
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
|
@ -34,7 +32,6 @@ extern "C"
|
|||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG,__VA_ARGS__)
|
||||
|
||||
static JavaVM *cachedJVM;
|
||||
static jclass class_String;
|
||||
static jclass class_ArrayList;
|
||||
static jclass class_WalletListener;
|
||||
static jclass class_CoinsInfo;
|
||||
|
@ -42,17 +39,11 @@ static jclass class_TransactionInfo;
|
|||
static jclass class_Transfer;
|
||||
static jclass class_Ledger;
|
||||
static jclass class_WalletStatus;
|
||||
static jclass class_BluetoothService;
|
||||
static jclass class_SidekickService;
|
||||
|
||||
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) {
|
||||
cachedJVM = jvm;
|
||||
LOGI("JNI_OnLoad");
|
||||
|
@ -62,8 +53,6 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
|
|||
}
|
||||
//LOGI("JNI_OnLoad ok");
|
||||
|
||||
class_String = static_cast<jclass>(jenv->NewGlobalRef(
|
||||
jenv->FindClass("java/lang/String")));
|
||||
class_ArrayList = static_cast<jclass>(jenv->NewGlobalRef(
|
||||
jenv->FindClass("java/util/ArrayList")));
|
||||
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")));
|
||||
class_WalletStatus = static_cast<jclass>(jenv->NewGlobalRef(
|
||||
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;
|
||||
}
|
||||
#ifdef __cplusplus
|
||||
|
@ -1686,6 +1677,79 @@ int LedgerFind(char *buffer, size_t len) {
|
|||
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
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) 2017 m2049r
|
||||
* Copyright (c) 2017-2024 m2049r
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -19,6 +19,8 @@
|
|||
|
||||
#include <jni.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
/*
|
||||
#include <android/log.h>
|
||||
|
||||
|
@ -28,6 +30,10 @@
|
|||
#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") {
|
||||
jclass c = env->GetObjectClass(obj);
|
||||
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>
|
||||
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));
|
||||
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) {
|
||||
|
@ -62,18 +76,40 @@ enum {
|
|||
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) {
|
||||
cn_slow_hash(data, length, hash, 0 /*variant*/, 0 /*prehashed*/, 0 /*height*/);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
#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
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -16,7 +16,17 @@
|
|||
|
||||
package com.m2049r.xmrwallet;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.AlertDialog;
|
||||
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.Handler;
|
||||
import android.os.Looper;
|
||||
|
@ -24,7 +34,8 @@ import android.os.PowerManager;
|
|||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
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.dialog.ProgressDialog;
|
||||
|
@ -159,4 +170,87 @@ public class BaseActivity extends SecureActivity
|
|||
barcodeData = null;
|
||||
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.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
|
@ -67,8 +66,20 @@ public class GenerateFragment extends Fragment {
|
|||
static final String TYPE_KEY = "key";
|
||||
static final String TYPE_SEED = "seed";
|
||||
static final String TYPE_LEDGER = "ledger";
|
||||
static final String TYPE_SIDEKICK = "sidekick";
|
||||
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 PasswordEntryView etWalletPassword;
|
||||
private LinearLayout llFingerprintAuth;
|
||||
|
@ -195,6 +206,7 @@ public class GenerateFragment extends Fragment {
|
|||
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED);
|
||||
break;
|
||||
case TYPE_LEDGER:
|
||||
case TYPE_SIDEKICK:
|
||||
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
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() {
|
||||
String name = etWalletName.getEditText().getText().toString();
|
||||
boolean ok = true;
|
||||
if (name.length() == 0) {
|
||||
if (name.isEmpty()) {
|
||||
etWalletName.setError(getString(R.string.generate_wallet_name));
|
||||
ok = false;
|
||||
} else if (name.charAt(0) == '.') {
|
||||
|
@ -450,11 +462,12 @@ public class GenerateFragment extends Fragment {
|
|||
activityCallback.onGenerate(name, crazyPass, seed, offset, height);
|
||||
break;
|
||||
case TYPE_LEDGER:
|
||||
case TYPE_SIDEKICK:
|
||||
bGenerate.setEnabled(false);
|
||||
if (fingerprintAuthAllowed) {
|
||||
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
|
||||
}
|
||||
activityCallback.onGenerateLedger(name, crazyPass, height);
|
||||
activityCallback.onGenerateDevice(getDeviceType(type), name, crazyPass, height);
|
||||
break;
|
||||
case TYPE_KEY:
|
||||
case TYPE_VIEWONLY:
|
||||
|
@ -498,6 +511,8 @@ public class GenerateFragment extends Fragment {
|
|||
return getString(R.string.generate_wallet_type_seed);
|
||||
case TYPE_LEDGER:
|
||||
return getString(R.string.generate_wallet_type_ledger);
|
||||
case TYPE_SIDEKICK:
|
||||
return getString(R.string.generate_wallet_type_sidekick);
|
||||
case TYPE_VIEWONLY:
|
||||
return getString(R.string.generate_wallet_type_view);
|
||||
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 onGenerateLedger(String name, String password, long height);
|
||||
void onGenerateDevice(Wallet.Device device, String name, String password, long height);
|
||||
|
||||
void setTitle(String title);
|
||||
|
||||
|
@ -555,6 +570,9 @@ public class GenerateFragment extends Fragment {
|
|||
case TYPE_LEDGER:
|
||||
inflater.inflate(R.menu.create_wallet_ledger, menu);
|
||||
break;
|
||||
case TYPE_SIDEKICK:
|
||||
inflater.inflate(R.menu.create_wallet_sidekick, menu);
|
||||
break;
|
||||
case TYPE_VIEWONLY:
|
||||
inflater.inflate(R.menu.create_wallet_view, menu);
|
||||
break;
|
||||
|
@ -581,13 +599,11 @@ public class GenerateFragment extends Fragment {
|
|||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.label_ok), null)
|
||||
.setNegativeButton(getString(R.string.label_cancel),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
(dialog, id) -> {
|
||||
Helper.hideKeyboardAlways(activity);
|
||||
etWalletMnemonic.getEditText().getText().clear();
|
||||
dialog.cancel();
|
||||
ledgerDialog = null;
|
||||
}
|
||||
});
|
||||
|
||||
ledgerDialog = alertDialogBuilder.create();
|
||||
|
|
|
@ -99,7 +99,7 @@ public class GenerateReviewFragment extends Fragment {
|
|||
private String walletPath;
|
||||
private String walletName;
|
||||
|
||||
private OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) {
|
||||
private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
// nothing
|
||||
|
@ -164,6 +164,7 @@ public class GenerateReviewFragment extends Fragment {
|
|||
});
|
||||
|
||||
Bundle args = getArguments();
|
||||
assert args != null;
|
||||
type = args.getString(REQUEST_TYPE);
|
||||
walletPath = args.getString(REQUEST_PATH);
|
||||
localPassword = args.getString(REQUEST_PASSWORD);
|
||||
|
@ -254,7 +255,7 @@ public class GenerateReviewFragment extends Fragment {
|
|||
showProgress();
|
||||
if ((walletPath != null)
|
||||
&& (WalletManager.getInstance().queryWalletDevice(walletPath + ".keys", getPassword())
|
||||
== Wallet.Device.Device_Ledger)
|
||||
== Wallet.Device.Ledger)
|
||||
&& (progressCallback != null)) {
|
||||
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
|
||||
dialogOpened = true;
|
||||
|
@ -286,10 +287,11 @@ public class GenerateReviewFragment extends Fragment {
|
|||
height = wallet.getRestoreHeight();
|
||||
seed = wallet.getSeed(getSeedOffset());
|
||||
switch (wallet.getDeviceType()) {
|
||||
case Device_Ledger:
|
||||
case Ledger:
|
||||
viewKey = Ledger.Key();
|
||||
break;
|
||||
case Device_Software:
|
||||
case Software:
|
||||
case Sidekick:
|
||||
viewKey = wallet.getSecretViewKey();
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -22,13 +22,14 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LockFragment extends Fragment {
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
Timber.d("onCreateView");
|
||||
final FrameLayout frame = new FrameLayout(requireContext());
|
||||
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.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
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.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.BluetoothService;
|
||||
import com.m2049r.xmrwallet.service.WalletService;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
|
@ -76,13 +78,12 @@ import java.util.HashSet;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import lombok.Getter;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LoginActivity extends BaseActivity
|
||||
implements LoginFragment.Listener, GenerateFragment.Listener,
|
||||
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 NODES_PREFS_NAME = "nodes";
|
||||
|
@ -278,8 +279,15 @@ public class LoginActivity extends BaseActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLedger() {
|
||||
public boolean hasDevice(Wallet.Device type) {
|
||||
switch (type) {
|
||||
case Ledger:
|
||||
return Ledger.isConnected();
|
||||
case Sidekick:
|
||||
return BluetoothService.IsConnected();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -311,8 +319,8 @@ public class LoginActivity extends BaseActivity
|
|||
}
|
||||
});
|
||||
|
||||
if (isNetworkAvailable())
|
||||
loadFavouritesWithNetwork();
|
||||
|
||||
LegacyStorageHelper.migrateWallets(this);
|
||||
|
||||
if (savedInstanceState == null) startLoginFragment();
|
||||
|
@ -334,7 +342,7 @@ public class LoginActivity extends BaseActivity
|
|||
|
||||
@Override
|
||||
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();
|
||||
return false;
|
||||
}
|
||||
|
@ -611,6 +619,7 @@ public class LoginActivity extends BaseActivity
|
|||
try {
|
||||
GenerateReviewFragment detailsFragment = (GenerateReviewFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||
assert detailsFragment != null;
|
||||
AlertDialog dialog = detailsFragment.createChangePasswordDialog();
|
||||
if (dialog != null) {
|
||||
Helper.showKeyboard(dialog);
|
||||
|
@ -682,6 +691,7 @@ public class LoginActivity extends BaseActivity
|
|||
dismissProgressDialog();
|
||||
unregisterDetachReceiver();
|
||||
Ledger.disconnect();
|
||||
BluetoothService.Stop();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
@ -695,6 +705,7 @@ public class LoginActivity extends BaseActivity
|
|||
new AsyncWaitForService().execute();
|
||||
}
|
||||
if (!Ledger.isConnected()) attachLedger();
|
||||
if (BluetoothService.IsConnected()) onLedgerAction(); //TODO sidekick & show sidekick fab
|
||||
registerTor();
|
||||
}
|
||||
|
||||
|
@ -727,14 +738,14 @@ public class LoginActivity extends BaseActivity
|
|||
}
|
||||
}
|
||||
|
||||
void startWallet(String walletName, String walletPassword,
|
||||
boolean fingerprintUsed, boolean streetmode) {
|
||||
void startWallet(String walletName, String walletPassword, boolean fingerprintUsed, StartMode mode) {
|
||||
Timber.d("startWallet()");
|
||||
|
||||
Intent intent = new Intent(getApplicationContext(), WalletActivity.class);
|
||||
intent.putExtra(WalletActivity.REQUEST_ID, walletName);
|
||||
intent.putExtra(WalletActivity.REQUEST_PW, walletPassword);
|
||||
intent.putExtra(WalletActivity.REQUEST_FINGERPRINT_USED, fingerprintUsed);
|
||||
intent.putExtra(WalletActivity.REQUEST_STREETMODE, streetmode);
|
||||
intent.putExtra(WalletActivity.REQUEST_STREETMODE, mode == StartMode.Street);
|
||||
if (uri != null) {
|
||||
intent.putExtra(WalletActivity.REQUEST_URI, uri);
|
||||
uri = null; // use only once
|
||||
|
@ -783,6 +794,11 @@ public class LoginActivity extends BaseActivity
|
|||
Timber.d("SettingsFragment placed");
|
||||
}
|
||||
|
||||
void startSidekickConnectFragment() {
|
||||
replaceFragment(new SidekickConnectFragment(), null, null);
|
||||
Timber.d("SidekickConnectFragment placed");
|
||||
}
|
||||
|
||||
void replaceFragment(Fragment newFragment, String stackName, Bundle extras) {
|
||||
if (extras != null) {
|
||||
newFragment.setArguments(extras);
|
||||
|
@ -821,7 +837,7 @@ public class LoginActivity extends BaseActivity
|
|||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
acquireWakeLock();
|
||||
if (walletCreator.isLedger()) {
|
||||
if (walletCreator.device() == Wallet.Device.Ledger) {
|
||||
showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
|
||||
} else {
|
||||
showProgressDialog(R.string.generate_wallet_creating);
|
||||
|
@ -881,6 +897,7 @@ public class LoginActivity extends BaseActivity
|
|||
try {
|
||||
GenerateFragment genFragment = (GenerateFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||
assert genFragment != null;
|
||||
genFragment.walletGenerateError();
|
||||
} catch (ClassCastException ex) {
|
||||
Timber.e("walletGenerateError() but not in GenerateFragment");
|
||||
|
@ -890,8 +907,7 @@ public class LoginActivity extends BaseActivity
|
|||
interface WalletCreator {
|
||||
boolean createWallet(File aFile, String password);
|
||||
|
||||
boolean isLedger();
|
||||
|
||||
Wallet.Device device();
|
||||
}
|
||||
|
||||
boolean checkAndCloseWallet(Wallet aWallet) {
|
||||
|
@ -909,8 +925,8 @@ public class LoginActivity extends BaseActivity
|
|||
createWallet(name, password,
|
||||
new WalletCreator() {
|
||||
@Override
|
||||
public boolean isLedger() {
|
||||
return false;
|
||||
public Wallet.Device device() {
|
||||
return Wallet.Device.Software;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -933,8 +949,8 @@ public class LoginActivity extends BaseActivity
|
|||
createWallet(name, password,
|
||||
new WalletCreator() {
|
||||
@Override
|
||||
public boolean isLedger() {
|
||||
return false;
|
||||
public Wallet.Device device() {
|
||||
return Wallet.Device.Software;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -947,20 +963,19 @@ public class LoginActivity extends BaseActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onGenerateLedger(final String name, final String password,
|
||||
final long restoreHeight) {
|
||||
public void onGenerateDevice(final Wallet.Device device, final String name, final String password, long restoreHeight) {
|
||||
createWallet(name, password,
|
||||
new WalletCreator() {
|
||||
@Override
|
||||
public boolean isLedger() {
|
||||
return true;
|
||||
public Wallet.Device device() {
|
||||
return device;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean createWallet(File aFile, String password) {
|
||||
Wallet newWallet = WalletManager.getInstance()
|
||||
.createWalletFromDevice(aFile, password,
|
||||
restoreHeight, "Ledger");
|
||||
restoreHeight, device);
|
||||
return checkAndCloseWallet(newWallet);
|
||||
}
|
||||
});
|
||||
|
@ -973,8 +988,8 @@ public class LoginActivity extends BaseActivity
|
|||
createWallet(name, password,
|
||||
new WalletCreator() {
|
||||
@Override
|
||||
public boolean isLedger() {
|
||||
return false;
|
||||
public Wallet.Device device() {
|
||||
return Wallet.Device.Software;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1105,6 +1120,9 @@ public class LoginActivity extends BaseActivity
|
|||
} else if (id == R.id.action_create_help_ledger) {
|
||||
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_ledger);
|
||||
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) {
|
||||
HelpFragment.display(getSupportFragmentManager(), R.string.help_details);
|
||||
return true;
|
||||
|
@ -1117,6 +1135,9 @@ public class LoginActivity extends BaseActivity
|
|||
} else if (id == R.id.action_help_node) {
|
||||
HelpFragment.display(getSupportFragmentManager(), R.string.help_node);
|
||||
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) {
|
||||
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||
if ((WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) &&
|
||||
|
@ -1124,6 +1145,9 @@ public class LoginActivity extends BaseActivity
|
|||
((NodeFragment) f).restoreDefaultNodes();
|
||||
}
|
||||
return true;
|
||||
} else if (id == R.id.action_sidekick) {
|
||||
checkBtPermissions();
|
||||
return true;
|
||||
} else if (id == R.id.action_ledger_seed) {
|
||||
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||
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
|
||||
private class AsyncOpenWallet extends AsyncTask<Void, Void, Boolean> {
|
||||
final static int OK = 0;
|
||||
|
@ -1172,7 +1201,7 @@ public class LoginActivity extends BaseActivity
|
|||
if (result) {
|
||||
Timber.d("selected wallet is .%s.", node.getName());
|
||||
// now it's getting real, onValidateFields if wallet exists
|
||||
promptAndStart(walletName, streetmode);
|
||||
promptAndStart(walletName, streetmode ? StartMode.Street : StartMode.Normal);
|
||||
} else {
|
||||
if (node.getResponseCode() == 0) { // IOException
|
||||
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) {
|
||||
String keyPath = new File(Helper.getWalletRoot(LoginActivity.this),
|
||||
walletName + ".keys").getAbsolutePath();
|
||||
private boolean checkDevice(String walletName, String password) {
|
||||
String keyPath = new File(Helper.getWalletRoot(this), walletName + ".keys").getAbsolutePath();
|
||||
// check if we need connected hardware
|
||||
Wallet.Device device = WalletManager.getInstance().queryWalletDevice(keyPath, password);
|
||||
if (device == Wallet.Device.Device_Ledger) {
|
||||
if (!hasLedger()) {
|
||||
final Wallet.Device device = WalletManager.getInstance().queryWalletDevice(keyPath, password);
|
||||
if (!hasDevice(device)) {
|
||||
if (device == Wallet.Device.Ledger) {
|
||||
toast(R.string.open_wallet_ledger_missing);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {// device could be undefined meaning the password is wrong
|
||||
// this gets dealt with later
|
||||
return true;
|
||||
} else if (device == Wallet.Device.Sidekick) {
|
||||
toast(R.string.open_wallet_sidekick_missing);
|
||||
}
|
||||
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);
|
||||
if (WalletManager.getInstance().walletExists(walletFile)) {
|
||||
Helper.promptPassword(LoginActivity.this, walletName, false,
|
||||
|
@ -1210,7 +1239,7 @@ public class LoginActivity extends BaseActivity
|
|||
@Override
|
||||
public void act(String walletName, String password, boolean fingerprintUsed) {
|
||||
if (checkDevice(walletName, password))
|
||||
startWallet(walletName, password, fingerprintUsed, streetmode);
|
||||
startWallet(walletName, password, fingerprintUsed, mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1234,7 +1263,7 @@ public class LoginActivity extends BaseActivity
|
|||
if (usbManager.hasPermission(device)) {
|
||||
connectLedger(usbManager, device);
|
||||
} else {
|
||||
registerReceiver(usbPermissionReceiver, new IntentFilter(ACTION_USB_PERMISSION));
|
||||
ContextCompat.registerReceiver(this, usbPermissionReceiver, new IntentFilter(ACTION_USB_PERMISSION), ContextCompat.RECEIVER_EXPORTED);
|
||||
usbManager.requestPermission(device,
|
||||
PendingIntent.getBroadcast(this, 0,
|
||||
new Intent(ACTION_USB_PERMISSION),
|
||||
|
@ -1339,7 +1368,7 @@ public class LoginActivity extends BaseActivity
|
|||
private void registerDetachReceiver() {
|
||||
detachReceiver = new BroadcastReceiver() {
|
||||
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();
|
||||
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
Timber.i("Ledger detached!");
|
||||
|
@ -1353,8 +1382,7 @@ public class LoginActivity extends BaseActivity
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
registerReceiver(detachReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED));
|
||||
ContextCompat.registerReceiver(this, detachReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED), ContextCompat.RECEIVER_EXPORTED);
|
||||
}
|
||||
|
||||
public void onLedgerAction() {
|
||||
|
@ -1378,6 +1406,34 @@ public class LoginActivity extends BaseActivity
|
|||
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
|
||||
//
|
||||
|
|
|
@ -46,6 +46,7 @@ import com.google.android.material.progressindicator.CircularProgressIndicator;
|
|||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
import com.m2049r.xmrwallet.dialog.HelpFragment;
|
||||
import com.m2049r.xmrwallet.layout.WalletInfoAdapter;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
|
@ -61,6 +62,7 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import lombok.Getter;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInteractionListener,
|
||||
|
@ -115,7 +117,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
|
||||
Set<NodeInfo> getOrPopulateFavourites();
|
||||
|
||||
boolean hasLedger();
|
||||
boolean hasDevice(Wallet.Device type);
|
||||
|
||||
boolean isNetworkAvailable();
|
||||
|
||||
void runOnNetCipher(Runnable runnable);
|
||||
}
|
||||
|
@ -146,10 +150,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
activityCallback.setToolbarButton(Toolbar.BUTTON_SETTINGS);
|
||||
activityCallback.showNet();
|
||||
showNetwork();
|
||||
//activityCallback.runOnNetCipher(this::pingSelectedNode);
|
||||
}
|
||||
|
||||
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
|
||||
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
animateFAB();
|
||||
|
@ -172,6 +175,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
fabSeed = view.findViewById(R.id.fabSeed);
|
||||
fabImport = view.findViewById(R.id.fabImport);
|
||||
fabLedger = view.findViewById(R.id.fabLedger);
|
||||
fabSidekick = view.findViewById(R.id.fabSidekick);
|
||||
|
||||
fabNewL = view.findViewById(R.id.fabNewL);
|
||||
fabViewL = view.findViewById(R.id.fabViewL);
|
||||
|
@ -179,6 +183,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
fabSeedL = view.findViewById(R.id.fabSeedL);
|
||||
fabImportL = view.findViewById(R.id.fabImportL);
|
||||
fabLedgerL = view.findViewById(R.id.fabLedgerL);
|
||||
fabSidekickL = view.findViewById(R.id.fabSidekickL);
|
||||
|
||||
fab_pulse = AnimationUtils.loadAnimation(getContext(), R.anim.fab_pulse);
|
||||
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);
|
||||
fabImport.setOnClickListener(this);
|
||||
fabLedger.setOnClickListener(this);
|
||||
fabSidekick.setOnClickListener(this);
|
||||
fabScreen.setOnClickListener(this);
|
||||
|
||||
RecyclerView recyclerView = view.findViewById(R.id.list);
|
||||
|
@ -202,6 +208,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
recyclerView.setAdapter(adapter);
|
||||
|
||||
ViewGroup llNotice = view.findViewById(R.id.llNotice);
|
||||
|
||||
Notice.showAll(llNotice, ".*_login");
|
||||
|
||||
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
|
||||
Set<String> removedWallets = getActivity()
|
||||
Set<String> removedWallets = requireActivity()
|
||||
.getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getAll().keySet();
|
||||
for (WalletManager.WalletInfo s : walletList) {
|
||||
|
@ -304,17 +311,14 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Getter
|
||||
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 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_pulse;
|
||||
|
||||
public boolean isFabOpen() {
|
||||
return fabOpen;
|
||||
}
|
||||
|
||||
private void setFabOpen(boolean value) {
|
||||
fabOpen = value;
|
||||
onBackPressedCallback.setEnabled(value);
|
||||
|
@ -328,6 +332,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
if (fabLedgerL.getVisibility() == View.VISIBLE) {
|
||||
fabLedgerL.startAnimation(fab_close);
|
||||
fabLedger.setClickable(false);
|
||||
} else if (fabSidekickL.getVisibility() == View.VISIBLE) {
|
||||
fabSidekickL.startAnimation(fab_close);
|
||||
fabSidekick.setClickable(false);
|
||||
} else {
|
||||
fabNewL.startAnimation(fab_close);
|
||||
fabNew.setClickable(false);
|
||||
|
@ -345,17 +352,25 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
fabScreen.setClickable(true);
|
||||
fabScreen.startAnimation(fab_open_screen);
|
||||
fab.startAnimation(rotate_forward);
|
||||
if (activityCallback.hasLedger()) {
|
||||
fabLedgerL.setVisibility(View.VISIBLE);
|
||||
if ((activityCallback.hasDevice(Wallet.Device.Ledger)
|
||||
|| activityCallback.hasDevice(Wallet.Device.Sidekick))) {
|
||||
fabNewL.setVisibility(View.GONE);
|
||||
fabViewL.setVisibility(View.GONE);
|
||||
fabKeyL.setVisibility(View.GONE);
|
||||
fabSeedL.setVisibility(View.GONE);
|
||||
fabImportL.setVisibility(View.GONE);
|
||||
|
||||
if (activityCallback.hasDevice(Wallet.Device.Ledger)) {
|
||||
fabLedgerL.setVisibility(View.VISIBLE);
|
||||
fabLedgerL.startAnimation(fab_open);
|
||||
fabLedger.setClickable(true);
|
||||
} else { // Sidekick
|
||||
fabSidekickL.setVisibility(View.VISIBLE);
|
||||
fabSidekickL.startAnimation(fab_open);
|
||||
fabSidekick.setClickable(true);
|
||||
}
|
||||
} else {
|
||||
fabSidekickL.setVisibility(View.GONE);
|
||||
fabLedgerL.setVisibility(View.GONE);
|
||||
fabNewL.setVisibility(View.VISIBLE);
|
||||
fabViewL.setVisibility(View.VISIBLE);
|
||||
|
@ -404,6 +419,10 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
Timber.d("FAB_LEDGER");
|
||||
animateFAB();
|
||||
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) {
|
||||
animateFAB();
|
||||
}
|
||||
|
@ -426,7 +445,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
}
|
||||
|
||||
private void setSubtext(String status) {
|
||||
final Context ctx = getContext();
|
||||
final Context ctx = requireContext();
|
||||
final Spanned text = Html.fromHtml(ctx.getString(R.string.status,
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),
|
||||
|
|
|
@ -32,6 +32,7 @@ import android.widget.TextView;
|
|||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
@ -68,7 +69,7 @@ public class NodeFragment extends Fragment
|
|||
|
||||
static private int NODES_TO_FIND = 10;
|
||||
|
||||
static private NumberFormat FORMATTER = NumberFormat.getInstance();
|
||||
static private final NumberFormat FORMATTER = NumberFormat.getInstance();
|
||||
|
||||
private SwipeRefreshLayout pullToRefresh;
|
||||
private TextView tvPull;
|
||||
|
@ -104,7 +105,7 @@ public class NodeFragment extends Fragment
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof Listener) {
|
||||
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
|
||||
public void handleOnBackPressed() {
|
||||
Toast.makeText(requireActivity(), getString(R.string.node_refresh_wait), Toast.LENGTH_LONG).show();
|
||||
|
@ -210,7 +211,7 @@ public class NodeFragment extends Fragment
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.node_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
@ -289,8 +290,7 @@ public class NodeFragment extends Fragment
|
|||
} else if (params[0] == SCAN) {
|
||||
// otherwise scan the network
|
||||
Timber.d("scanning");
|
||||
Set<NodeInfo> seedList = new HashSet<>();
|
||||
seedList.addAll(nodeList);
|
||||
Set<NodeInfo> seedList = new HashSet<>(nodeList);
|
||||
nodeList.clear();
|
||||
Timber.d("seed %d", seedList.size());
|
||||
Dispatcher d = new Dispatcher(info -> publishProgress(info));
|
||||
|
@ -455,7 +455,7 @@ public class NodeFragment extends Fragment
|
|||
|
||||
private void closeDialog() {
|
||||
if (editDialog == null) throw new IllegalStateException();
|
||||
Helper.hideKeyboardAlways(getActivity());
|
||||
Helper.hideKeyboardAlways(requireActivity());
|
||||
editDialog.dismiss();
|
||||
editDialog = null;
|
||||
NodeFragment.this.editDialog = null;
|
||||
|
|
|
@ -32,12 +32,10 @@ import android.view.KeyEvent;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
@ -84,8 +82,6 @@ public class ReceiveFragment extends Fragment {
|
|||
private ImageView ivQrCode;
|
||||
private ImageView ivQrCodeFull;
|
||||
private EditText etDummy;
|
||||
private ImageButton bCopyAddress;
|
||||
private MenuItem shareItem;
|
||||
|
||||
private Wallet wallet = null;
|
||||
private boolean isMyWallet = false;
|
||||
|
@ -116,11 +112,10 @@ public class ReceiveFragment extends Fragment {
|
|||
tvQrCode = view.findViewById(R.id.tvQrCode);
|
||||
ivQrCodeFull = view.findViewById(R.id.qrCodeFull);
|
||||
etDummy = view.findViewById(R.id.etDummy);
|
||||
bCopyAddress = view.findViewById(R.id.bCopyAddress);
|
||||
|
||||
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
|
||||
bCopyAddress.setOnClickListener(v -> copyAddress());
|
||||
view.findViewById(R.id.bCopyAddress).setOnClickListener(v -> copyAddress());
|
||||
|
||||
evAmount.setOnNewAmountListener(xmr -> {
|
||||
Timber.d("new amount = %s", xmr);
|
||||
|
@ -211,8 +206,7 @@ public class ReceiveFragment extends Fragment {
|
|||
inflater.inflate(R.menu.receive_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
shareItem = menu.findItem(R.id.menu_item_share);
|
||||
shareItem.setOnMenuItemClickListener(item -> {
|
||||
menu.findItem(R.id.menu_item_share).setOnMenuItemClickListener(item -> {
|
||||
if (shareRequested) return true;
|
||||
shareRequested = true;
|
||||
if (!qrValid) {
|
||||
|
@ -238,7 +232,7 @@ public class ReceiveFragment extends Fragment {
|
|||
private boolean saveQrCode() {
|
||||
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.mkdirs()) throw new IllegalStateException("cannot create images folder");
|
||||
File png = new File(cachePath, "QR.png");
|
||||
|
@ -452,7 +446,7 @@ public class ReceiveFragment extends Fragment {
|
|||
.withEndAction(resetSize).start();
|
||||
}
|
||||
subaddress = newSubaddress;
|
||||
final Context context = getContext();
|
||||
final Context context = requireContext();
|
||||
Spanned label = Html.fromHtml(context.getString(R.string.receive_subaddress,
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 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.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
|
@ -43,7 +44,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
|
|||
private ZXingScannerView mScannerView;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
Timber.d("onCreateView");
|
||||
mScannerView = new ZXingScannerView(getActivity());
|
||||
return mScannerView;
|
||||
|
@ -85,7 +86,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof OnScannedListener) {
|
||||
this.onScannedListener = (OnScannedListener) context;
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
@ -60,7 +61,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
|||
private SettingsFragment.Listener activity;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof SettingsFragment.Listener) {
|
||||
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
|
||||
protected void 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);
|
||||
dialogOpened = true;
|
||||
}
|
||||
|
@ -226,7 +226,7 @@ public class SubaddressFragment extends Fragment implements SubaddressInfoAdapte
|
|||
|
||||
// Callbacks from SubaddressInfoAdapter
|
||||
@Override
|
||||
public void onInteraction(final View view, final Subaddress subaddress) {
|
||||
public void onInteraction(final View view, @NonNull final Subaddress subaddress) {
|
||||
if (managerMode)
|
||||
activityCallback.showSubaddress(view, subaddress.getAddressIndex());
|
||||
else
|
||||
|
|
|
@ -52,7 +52,6 @@ public class SubaddressInfoFragment extends Fragment
|
|||
private Subaddress subaddress;
|
||||
|
||||
private TextInputLayout etName;
|
||||
private TextView tvAddress;
|
||||
private TextView tvTxLabel;
|
||||
|
||||
@Override
|
||||
|
@ -61,7 +60,6 @@ public class SubaddressInfoFragment extends Fragment
|
|||
View view = inflater.inflate(R.layout.fragment_subaddressinfo, container, false);
|
||||
|
||||
etName = view.findViewById(R.id.etName);
|
||||
tvAddress = view.findViewById(R.id.tvAddress);
|
||||
tvTxLabel = view.findViewById(R.id.tvTxLabel);
|
||||
|
||||
final RecyclerView list = view.findViewById(R.id.list);
|
||||
|
@ -71,11 +69,13 @@ public class SubaddressInfoFragment extends Fragment
|
|||
final Wallet wallet = activityCallback.getWallet();
|
||||
|
||||
Bundle b = getArguments();
|
||||
assert b != null;
|
||||
final int subaddressIndex = b.getInt("subaddressIndex");
|
||||
subaddress = wallet.getSubaddressObject(subaddressIndex);
|
||||
|
||||
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()));
|
||||
|
||||
etName.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
||||
|
|
|
@ -20,7 +20,6 @@ import android.annotation.SuppressLint;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
|
@ -34,6 +33,7 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
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.Transfer;
|
||||
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.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
@ -61,6 +63,7 @@ public class TxFragment extends Fragment {
|
|||
|
||||
static public final String ARG_INFO = "info";
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
|
||||
|
||||
public TxFragment() {
|
||||
|
@ -106,6 +109,7 @@ public class TxFragment extends Fragment {
|
|||
tvTxAmountBtc = view.findViewById(R.id.tvTxAmountBtc);
|
||||
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
|
||||
tvXmrToKeyLabel = view.findViewById(R.id.tvXmrToKeyLabel);
|
||||
|
||||
tvXmrToLogo = view.findViewById(R.id.tvXmrToLogo);
|
||||
|
||||
tvAccount = view.findViewById(R.id.tvAccount);
|
||||
|
@ -127,18 +131,20 @@ public class TxFragment extends Fragment {
|
|||
tvWarning = view.findViewById(R.id.tvWarning);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
info = getArguments().getParcelable(ARG_INFO);
|
||||
final Bundle args = getArguments();
|
||||
assert args != null;
|
||||
info = args.getParcelable(ARG_INFO);
|
||||
show();
|
||||
return view;
|
||||
}
|
||||
|
||||
void shareTxInfo() {
|
||||
if (this.info == null) return;
|
||||
StringBuffer sb = new StringBuffer();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append(getString(R.string.tx_timestamp)).append(":\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() {
|
||||
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,
|
||||
info.accountIndex, info.addressIndex,
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
|
||||
|
@ -264,16 +270,17 @@ public class TxFragment extends Fragment {
|
|||
tvTxFee.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final Context ctx = requireContext();
|
||||
if (info.isFailed) {
|
||||
tvTxAmount.setText(getString(R.string.tx_list_amount_failed, Wallet.getDisplayAmount(info.amount)));
|
||||
tvTxFee.setText(getString(R.string.tx_list_failed_text));
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
|
||||
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.neutralColor));
|
||||
} 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) {
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor));
|
||||
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor));
|
||||
} else {
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.negativeColor));
|
||||
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.negativeColor));
|
||||
}
|
||||
Set<String> destinations = new HashSet<>();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
@ -328,31 +335,29 @@ public class TxFragment extends Fragment {
|
|||
void showBtcInfo() {
|
||||
if (userNotes.xmrtoKey != null) {
|
||||
cvXmrTo.setVisibility(View.VISIBLE);
|
||||
String key = userNotes.xmrtoKey;
|
||||
if ("xmrto".equals(userNotes.xmrtoTag)) { // legacy xmr.to service :(
|
||||
key = "xmrto-" + key;
|
||||
}
|
||||
tvTxXmrToKey.setText(key);
|
||||
|
||||
ShiftService service = ShiftService.findWithTag(userNotes.xmrtoTag);
|
||||
tvXmrToKeyLabel.setText(getString(R.string.label_send_btc_xmrto_key_lb, service.getLabel()));
|
||||
if (service.getIconId() == 0)
|
||||
tvXmrToLogo.setVisibility(View.GONE);
|
||||
else
|
||||
tvXmrToLogo.setImageResource(service.getLogoId());
|
||||
|
||||
tvTxXmrToKey.setText(userNotes.xmrtoKey);
|
||||
|
||||
tvDestinationBtc.setText(userNotes.xmrtoDestination);
|
||||
tvTxAmountBtc.setText(userNotes.xmrtoAmount + " " + userNotes.xmrtoCurrency);
|
||||
switch (userNotes.xmrtoTag) {
|
||||
case "xmrto":
|
||||
tvXmrToSupport.setVisibility(View.GONE);
|
||||
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
|
||||
tvXmrToLogo.setImageResource(R.drawable.ic_xmrto_logo);
|
||||
break;
|
||||
case "side": // defaults in layout - just add underline
|
||||
|
||||
ShiftApi shiftApi = service.getShiftApi();
|
||||
if (shiftApi != null) {
|
||||
tvXmrToSupport.setText(getString(R.string.label_send_btc_xmrto_info, service.getLabel()));
|
||||
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
||||
tvXmrToSupport.setOnClickListener(v -> {
|
||||
Uri uri = Uri.parse("https://sideshift.ai/orders/" + userNotes.xmrtoKey);
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||
startActivity(intent);
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, shiftApi.getQueryOrderUri(userNotes.xmrtoKey)));
|
||||
});
|
||||
break;
|
||||
default:
|
||||
} else {
|
||||
tvXmrToSupport.setVisibility(View.GONE);
|
||||
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
|
||||
tvXmrToLogo.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
cvXmrTo.setVisibility(View.GONE);
|
||||
|
@ -369,7 +374,7 @@ public class TxFragment extends Fragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.tx_info_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
@ -397,7 +402,7 @@ public class TxFragment extends Fragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof TxFragment.Listener) {
|
||||
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");
|
||||
* 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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
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_STREETMODE = "streetmode";
|
||||
public static final String REQUEST_URI = "uri";
|
||||
public static final String REQUEST_SIDEKICK = "sidekick";
|
||||
|
||||
private NavigationView accountsView;
|
||||
private DrawerLayout drawer;
|
||||
|
@ -202,19 +203,9 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
|||
return getWallet().getSubaddress(major, minor);
|
||||
}
|
||||
|
||||
private void startWalletService() {
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
private void startWalletService(String walletId) {
|
||||
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);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopWalletService() {
|
||||
|
@ -330,7 +321,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
public void onWalletChangePassword() {
|
||||
try {
|
||||
GenerateReviewFragment detailsFragment = (GenerateReviewFragment) getCurrentFragment();
|
||||
|
@ -356,14 +346,24 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
|||
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);
|
||||
toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
|
||||
toolbar.setOnButtonListener(new Toolbar.OnButtonListener() {
|
||||
@Override
|
||||
public void onButton(int type) {
|
||||
toolbar.setOnButtonListener(type -> {
|
||||
switch (type) {
|
||||
case Toolbar.BUTTON_BACK:
|
||||
onDisposeRequest();
|
||||
|
@ -383,7 +383,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
|||
default:
|
||||
Timber.e("Button " + type + "pressed - how can this be?");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawer = findViewById(R.id.drawer_layout);
|
||||
|
@ -398,11 +397,16 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
|||
showNet();
|
||||
|
||||
Fragment walletFragment = new WalletFragment();
|
||||
final FragmentTransaction tx =
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.fragment_container, walletFragment, WalletFragment.class.getName()).commit();
|
||||
Timber.d("fragment added");
|
||||
.add(R.id.fragment_container, walletFragment, WalletFragment.class.getName());
|
||||
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.");
|
||||
}
|
||||
|
||||
|
@ -616,7 +620,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
|||
|
||||
@Override
|
||||
public void onWalletOpen(final Wallet.Device device) {
|
||||
if (device == Wallet.Device.Device_Ledger) {
|
||||
if (device == Wallet.Device.Ledger) {
|
||||
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);
|
||||
startService(intent);
|
||||
Timber.d("CREATE TX request sent");
|
||||
if (getWallet().getDeviceType() == Wallet.Device.Device_Ledger)
|
||||
if (getWallet().getDeviceType() == Wallet.Device.Ledger)
|
||||
showLedgerProgressDialog(LedgerProgressDialog.TYPE_SEND);
|
||||
} else {
|
||||
Timber.e("Service not bound");
|
||||
|
@ -1132,11 +1136,12 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
|||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
switch (getWallet().getDeviceType()) {
|
||||
case Device_Ledger:
|
||||
case Ledger:
|
||||
showLedgerProgressDialog(LedgerProgressDialog.TYPE_ACCOUNT);
|
||||
dialogOpened = true;
|
||||
break;
|
||||
case Device_Software:
|
||||
case Software:
|
||||
case Sidekick:
|
||||
showProgressDialog(R.string.accounts_progress_new);
|
||||
dialogOpened = true;
|
||||
break;
|
||||
|
@ -1176,7 +1181,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onSubaddressSelected(@Nullable final Subaddress subaddress) {
|
||||
public void onSubaddressSelected(@NonNull final Subaddress subaddress) {
|
||||
selectedSubaddressIndex = subaddress.getAddressIndex();
|
||||
getOnBackPressedDispatcher().onBackPressed();
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ public class WalletFragment extends Fragment
|
|||
flExchange = view.findViewById(R.id.flExchange);
|
||||
((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable().
|
||||
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);
|
||||
|
||||
tvProgress = view.findViewById(R.id.tvProgress);
|
||||
|
|
|
@ -22,27 +22,19 @@ import android.content.res.Configuration;
|
|||
import android.os.Build;
|
||||
|
||||
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.util.LocaleHelper;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
import com.m2049r.xmrwallet.util.NightmodeHelper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class XmrWalletApplication extends Application {
|
||||
|
||||
@Override
|
||||
@OptIn(markerClass = FragmentStateManagerControl.class)
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
FragmentManager.enableNewStateManager(false);
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(new Timber.DebugTree());
|
||||
}
|
||||
|
|
|
@ -24,43 +24,38 @@ import java.net.URI;
|
|||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import timber.log.Timber;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class BarcodeData {
|
||||
|
||||
public enum Security {
|
||||
NORMAL,
|
||||
OA_NO_DNSSEC,
|
||||
OA_DNSSEC
|
||||
}
|
||||
|
||||
final public Crypto asset;
|
||||
final public List<Crypto> ambiguousAssets;
|
||||
final public String address;
|
||||
final public String addressName;
|
||||
final public String amount;
|
||||
final public String description;
|
||||
final public Security security;
|
||||
final private Set<Crypto> possibleAssets = new HashSet<>();
|
||||
private String address = null;
|
||||
private String addressName = null;
|
||||
private String amount = null;
|
||||
private String description = null;
|
||||
private Security security = null;
|
||||
|
||||
public BarcodeData(List<Crypto> assets, String address) {
|
||||
if (assets.isEmpty())
|
||||
throw new IllegalArgumentException("no assets specified");
|
||||
this.addressName = null;
|
||||
this.description = null;
|
||||
this.amount = null;
|
||||
this.security = Security.NORMAL;
|
||||
security = Security.NORMAL;
|
||||
this.address = address;
|
||||
if (assets.size() == 1) {
|
||||
this.asset = assets.get(0);
|
||||
this.ambiguousAssets = null;
|
||||
} else {
|
||||
this.asset = null;
|
||||
this.ambiguousAssets = assets;
|
||||
}
|
||||
possibleAssets.addAll(assets);
|
||||
}
|
||||
|
||||
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) {
|
||||
this.ambiguousAssets = null;
|
||||
this.asset = asset;
|
||||
possibleAssets.add(asset);
|
||||
this.address = address;
|
||||
this.addressName = addressName;
|
||||
this.description = description;
|
||||
|
@ -82,7 +76,7 @@ public class BarcodeData {
|
|||
}
|
||||
|
||||
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();
|
||||
sb.append(Crypto.XMR.getUriScheme())
|
||||
.append(':')
|
||||
|
@ -227,6 +221,20 @@ public class BarcodeData {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -5,35 +21,30 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
|
||||
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
|
||||
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
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),
|
||||
BTC("BTC", true, "bitcoin:amount:label:message", R.id.ibBTC, R.drawable.ic_xmrto_btc, R.drawable.ic_xmrto_btc_off, address -> {
|
||||
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC);
|
||||
}),
|
||||
DASH("DASH", true, "dash:amount:label:message", R.id.ibDASH, R.drawable.ic_xmrto_dash, R.drawable.ic_xmrto_dash_off, address -> {
|
||||
return BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
|
||||
}),
|
||||
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);
|
||||
});
|
||||
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", "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}$")),
|
||||
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}$")),
|
||||
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}$")),
|
||||
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}$"));
|
||||
|
||||
@Getter
|
||||
@NonNull
|
||||
private final String symbol;
|
||||
@Getter
|
||||
private final boolean casefull;
|
||||
@NonNull
|
||||
private final String network;
|
||||
@Getter
|
||||
@NonNull
|
||||
private final String label;
|
||||
@NonNull
|
||||
private final String uriSpec;
|
||||
@Getter
|
||||
|
@ -43,7 +54,7 @@ public enum Crypto {
|
|||
@Getter
|
||||
private final int iconDisabledId;
|
||||
@NonNull
|
||||
private final Validator validator;
|
||||
private final Pattern regex;
|
||||
|
||||
@Nullable
|
||||
public static Crypto withScheme(@NonNull String scheme) {
|
||||
|
@ -62,10 +73,6 @@ public enum Crypto {
|
|||
return null;
|
||||
}
|
||||
|
||||
interface Validator {
|
||||
boolean validate(String address);
|
||||
}
|
||||
|
||||
// TODO maybe cache these segments
|
||||
String getUriScheme() {
|
||||
return uriSpec.split(":")[0];
|
||||
|
@ -83,7 +90,8 @@ public enum Crypto {
|
|||
return uriSpec.split(":")[3];
|
||||
}
|
||||
|
||||
boolean validate(String address) {
|
||||
return validator.validate(address);
|
||||
public boolean validate(String 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.Getter;
|
||||
|
||||
// Nodes stolen from https://moneroworld.com/#nodes
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DefaultNodes {
|
||||
MONERUJO("nodex.monerujo.io:18081"),
|
||||
XMRTO("node.xmr.to:18081"),
|
||||
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"),
|
||||
AGORIST("xmr.agor.ist:18089/mainnet/agor.ist"),
|
||||
BOLDSUCK("xmr-de.boldsuck.org:18080/mainnet/boldsuck.org"),
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ public class Node {
|
|||
if ((nodeString == null) || nodeString.isEmpty())
|
||||
throw new IllegalArgumentException("daemon is empty");
|
||||
String daemonAddress;
|
||||
String a[] = nodeString.split("@");
|
||||
String[] a = nodeString.split("@");
|
||||
if (a.length == 1) { // no credentials
|
||||
daemonAddress = a[0];
|
||||
username = "";
|
||||
|
@ -169,7 +169,7 @@ public class Node {
|
|||
throw new IllegalArgumentException("Too many '/' or too few");
|
||||
|
||||
daemonAddress = daParts[0];
|
||||
String da[] = daemonAddress.split(":");
|
||||
String[] da = daemonAddress.split(":");
|
||||
if ((da.length > 2) || (da.length < 1))
|
||||
throw new IllegalArgumentException("Too many ':' or too few");
|
||||
String host = da[0];
|
||||
|
|
|
@ -201,8 +201,13 @@ public class NodeInfo extends Node {
|
|||
.port(port)
|
||||
.addPathSegment("json_rpc")
|
||||
.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());
|
||||
} catch (JSONException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean testRpcService(int port) {
|
||||
|
|
|
@ -20,22 +20,21 @@ import android.os.Parcel;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public class TxDataBtc extends TxData {
|
||||
@Getter
|
||||
@Setter
|
||||
private ShiftService shiftService;
|
||||
private String btcSymbol; // the actual non-XMR thing we're sending
|
||||
@Getter
|
||||
@Setter
|
||||
private String xmrtoOrderId; // shown in success screen
|
||||
@Getter
|
||||
@Setter
|
||||
private String btcAddress;
|
||||
@Getter
|
||||
@Setter
|
||||
private double btcAmount;
|
||||
private CryptoAmount shiftAmount; // what we want to send
|
||||
private String xmrtoQueryOrderToken; // used for queryOrder API
|
||||
|
||||
public TxDataBtc() {
|
||||
super();
|
||||
|
@ -47,7 +46,9 @@ public class TxDataBtc extends TxData {
|
|||
out.writeString(btcSymbol);
|
||||
out.writeString(xmrtoOrderId);
|
||||
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
|
||||
|
@ -66,7 +67,8 @@ public class TxDataBtc extends TxData {
|
|||
btcSymbol = in.readString();
|
||||
xmrtoOrderId = in.readString();
|
||||
btcAddress = in.readString();
|
||||
btcAmount = in.readDouble();
|
||||
shiftAmount = new CryptoAmount(Crypto.valueOf(in.readString()), in.readDouble());
|
||||
xmrtoQueryOrderToken = in.readString();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -79,19 +81,33 @@ public class TxDataBtc extends TxData {
|
|||
sb.append(btcSymbol);
|
||||
sb.append(",btcAddress:");
|
||||
sb.append(btcAddress);
|
||||
sb.append(",btcAmount:");
|
||||
sb.append(btcAmount);
|
||||
sb.append(",amount:");
|
||||
sb.append(shiftAmount);
|
||||
sb.append(",xmrtoQueryOrderToken:");
|
||||
sb.append(xmrtoQueryOrderToken);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public boolean validateAddress(@NonNull String address) {
|
||||
if ((btcSymbol == null) || (btcAddress == null)) return false;
|
||||
final Crypto crypto = Crypto.withSymbol(btcSymbol);
|
||||
if (crypto == null) return false;
|
||||
if (crypto.isCasefull()) { // compare as-is
|
||||
return address.equals(btcAddress);
|
||||
} else { // normalize & compare (e.g. ETH with and without checksum capitals
|
||||
return address.toLowerCase().equals(btcAddress.toLowerCase());
|
||||
return address.equalsIgnoreCase(btcAddress);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
|
||||
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
|
@ -61,7 +61,7 @@ public class UserNotes {
|
|||
|
||||
public void setXmrtoOrder(CreateOrder order) {
|
||||
if (order != null) {
|
||||
xmrtoTag = order.TAG;
|
||||
xmrtoTag = order.getTag();
|
||||
xmrtoKey = order.getOrderId();
|
||||
xmrtoAmount = Helper.getDisplayAmount(order.getBtcAmount());
|
||||
xmrtoCurrency = order.getBtcCurrency();
|
||||
|
|
|
@ -20,10 +20,10 @@ import android.app.Dialog;
|
|||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
@ -57,13 +57,14 @@ public class AboutFragment extends DialogFragment {
|
|||
AboutFragment.newInstance().show(ft, TAG);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
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.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)
|
||||
.setNegativeButton(R.string.about_close,
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
@ -77,7 +78,7 @@ public class AboutFragment extends DialogFragment {
|
|||
|
||||
private String getLicencesHtml() {
|
||||
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();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null)
|
||||
|
|
|
@ -20,10 +20,10 @@ import android.app.Dialog;
|
|||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
@ -49,13 +49,14 @@ public class CreditsFragment extends DialogFragment {
|
|||
CreditsFragment.newInstance().show(ft, TAG);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
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)));
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.about_close,
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
|
||||
package com.m2049r.xmrwallet.dialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
|
@ -65,9 +65,10 @@ public class HelpFragment extends DialogFragment {
|
|||
|
||||
private Spanned getHtml(String html, double textSize) {
|
||||
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
|
||||
final Drawable drawable = ContextCompat.getDrawable(requireActivity(), imageId > 0 ? imageId : R.drawable.ic_favorite_24dp);
|
||||
assert drawable != null;
|
||||
final double f = textSize / drawable.getIntrinsicHeight();
|
||||
drawable.setBounds(0, 0, (int) (f * drawable.getIntrinsicWidth()), (int) textSize);
|
||||
return drawable;
|
||||
|
@ -82,7 +83,7 @@ public class HelpFragment extends DialogFragment {
|
|||
@NonNull
|
||||
@Override
|
||||
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;
|
||||
boolean torButton = false;
|
||||
|
|
|
@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.dialog;
|
|||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
|
@ -66,7 +65,7 @@ public class PocketChangeFragment extends DialogFragment implements Slider.OnCha
|
|||
@NonNull
|
||||
@Override
|
||||
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;
|
||||
int progress = 0;
|
||||
Bundle arguments = getArguments();
|
||||
|
|
|
@ -20,10 +20,10 @@ import android.app.Dialog;
|
|||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
@ -49,13 +49,14 @@ public class PrivacyFragment extends DialogFragment {
|
|||
PrivacyFragment.newInstance().show(ft, TAG);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
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)));
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.about_close,
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
|
|
@ -17,6 +17,7 @@ package com.m2049r.xmrwallet.dialog;
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -31,6 +32,7 @@ import com.m2049r.xmrwallet.R;
|
|||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
|
@ -56,7 +58,7 @@ public class ProgressDialog extends AlertDialog {
|
|||
|
||||
@Override
|
||||
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);
|
||||
tvMessage = view.findViewById(R.id.tvMessage);
|
||||
rlProgressBar = view.findViewById(R.id.rlProgressBar);
|
||||
|
@ -78,7 +80,7 @@ public class ProgressDialog extends AlertDialog {
|
|||
super.onCreate(savedInstanceState);
|
||||
|
||||
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.model.PendingTransaction;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
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.HashSet;
|
||||
|
@ -64,15 +61,11 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
|
||||
public static SendAddressWizardFragment newInstance(Listener listener) {
|
||||
SendAddressWizardFragment instance = new SendAddressWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
instance.sendListener = listener;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Listener sendListener;
|
||||
|
||||
public void setSendListener(Listener listener) {
|
||||
this.sendListener = listener;
|
||||
}
|
||||
private Listener sendListener;
|
||||
|
||||
public interface Listener {
|
||||
void setBarcodeData(BarcodeData data);
|
||||
|
@ -103,13 +96,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
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
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
@ -122,7 +108,9 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
ibCrypto = new HashMap<>();
|
||||
for (Crypto crypto : Crypto.values()) {
|
||||
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);
|
||||
button.setOnClickListener(v -> {
|
||||
if (possibleCryptos.contains(crypto)) {
|
||||
|
@ -131,9 +119,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
} else {
|
||||
// show help what to do:
|
||||
if (button.getId() != R.id.ibXMR) {
|
||||
final String name = getResources().getStringArray(R.array.cryptos)[crypto.ordinal()];
|
||||
final String symbol = getCryptoForButton(button).getSymbol();
|
||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, name, symbol)));
|
||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, crypto.getNetwork(), crypto.getLabel(), ShiftService.DEFAULT.getLabel())));
|
||||
tvXmrTo.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
|
||||
|
@ -143,9 +129,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
button.setImageResource(crypto.getIconDisabledId());
|
||||
button.setImageAlpha(128);
|
||||
button.setEnabled(false);
|
||||
button.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
if (!Helper.ALLOW_SHIFT) {
|
||||
|
@ -175,49 +159,26 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
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();
|
||||
selectedCrypto = null;
|
||||
final String address = etAddress.getEditText().getText().toString();
|
||||
if (isIntegratedAddress(address)) {
|
||||
Timber.d("isIntegratedAddress");
|
||||
possibleCryptos.add(Crypto.XMR);
|
||||
selectedCrypto = Crypto.XMR;
|
||||
etAddress.setError(getString(R.string.info_paymentid_integrated));
|
||||
sendListener.setMode(SendFragment.Mode.XMR);
|
||||
} else if (isStandardAddress(address)) {
|
||||
Timber.d("isStandardAddress");
|
||||
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 ((bc != null) && (bc.filter(ShiftService.getPossibleAssets()))) {
|
||||
possibleCryptos.clear();
|
||||
possibleCryptos.addAll(bc.getPossibleAssets());
|
||||
selectedCrypto = bc.getAsset();
|
||||
if (checkAddress()) {
|
||||
if (bc.getSecurity() == BarcodeData.Security.OA_NO_DNSSEC)
|
||||
etAddress.setError(getString(R.string.send_address_no_dnssec));
|
||||
else if (bc.getSecurity() == BarcodeData.Security.OA_DNSSEC)
|
||||
etAddress.setError(getString(R.string.send_address_openalias));
|
||||
}
|
||||
}
|
||||
if (!possibleCryptos.isEmpty()) // found something in need of shifting!
|
||||
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());
|
||||
updateCryptoButtons(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -231,7 +192,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
|
||||
final ImageButton bPasteAddress = view.findViewById(R.id.bPasteAddress);
|
||||
bPasteAddress.setOnClickListener(v -> {
|
||||
final String clip = Helper.getClipBoardText(getActivity());
|
||||
final String clip = Helper.getClipBoardText(requireActivity());
|
||||
if (clip == null) return;
|
||||
// clean it up
|
||||
final String address = clip.replaceAll("( +)|(\\r?\\n?)", "");
|
||||
|
@ -248,9 +209,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
|
||||
etNotes = view.findViewById(R.id.etNotes);
|
||||
etNotes.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT);
|
||||
etNotes.getEditText().
|
||||
|
||||
setOnEditorActionListener((v, actionId, event) -> {
|
||||
etNotes.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
etDummy.requestFocus();
|
||||
|
@ -271,13 +230,19 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
|
||||
private void selectedCrypto(Crypto crypto) {
|
||||
final ImageButton button = ibCrypto.get(crypto);
|
||||
assert button != null;
|
||||
button.setImageResource(crypto.getIconEnabledId());
|
||||
button.setImageAlpha(255);
|
||||
button.setEnabled(true);
|
||||
if (selectedCrypto == Crypto.XMR)
|
||||
sendListener.setMode(SendFragment.Mode.XMR);
|
||||
else
|
||||
sendListener.setMode(SendFragment.Mode.BTC);
|
||||
}
|
||||
|
||||
private void possibleCrypto(Crypto crypto) {
|
||||
final ImageButton button = ibCrypto.get(crypto);
|
||||
assert button != null;
|
||||
button.setImageResource(crypto.getIconDisabledId());
|
||||
button.setImageAlpha(255);
|
||||
button.setEnabled(true);
|
||||
|
@ -285,6 +250,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
|
||||
private void impossibleCrypto(Crypto crypto) {
|
||||
final ImageButton button = ibCrypto.get(crypto);
|
||||
if (button == null) return; // not all buttons exist for all providers
|
||||
button.setImageResource(crypto.getIconDisabledId());
|
||||
button.setImageAlpha(128);
|
||||
button.setEnabled(true);
|
||||
|
@ -302,7 +268,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
}
|
||||
}
|
||||
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);
|
||||
} else if ((selectedCrypto == null) && (possibleCryptos.size() > 1)) {
|
||||
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);
|
||||
if (barcodeData == null) barcodeData = dataMap.get(Crypto.BTC);
|
||||
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);
|
||||
} else {
|
||||
etAddress.setError(getString(R.string.send_address_not_openalias));
|
||||
|
@ -369,18 +335,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
&& 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() {
|
||||
if (possibleCryptos.size() > 1) { // address ambiguous
|
||||
for (Crypto crypto : Crypto.values()) {
|
||||
|
@ -412,10 +366,10 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString());
|
||||
((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol());
|
||||
txData.setDestination(null);
|
||||
ServiceHelper.ASSET = selectedCrypto.getSymbol().toLowerCase();
|
||||
ShiftService.ASSET = selectedCrypto;
|
||||
} else {
|
||||
txData.setDestination(etAddress.getEditText().getText().toString());
|
||||
ServiceHelper.ASSET = null;
|
||||
ShiftService.ASSET = null;
|
||||
}
|
||||
txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString()));
|
||||
txData.setPriority(PendingTransaction.Priority.Priority_Default);
|
||||
|
@ -445,49 +399,33 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
|||
}
|
||||
|
||||
public void processScannedData(BarcodeData barcodeData) {
|
||||
barcodeData.filter(ShiftService.getPossibleAssets());
|
||||
sendListener.setBarcodeData(barcodeData);
|
||||
if (isResumed())
|
||||
processScannedData();
|
||||
}
|
||||
|
||||
public void processScannedData() {
|
||||
BarcodeData barcodeData = sendListener.getBarcodeData();
|
||||
final BarcodeData barcodeData = sendListener.getBarcodeData();
|
||||
if (barcodeData != null) {
|
||||
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");
|
||||
barcodeData = null;
|
||||
sendListener.setBarcodeData(barcodeData);
|
||||
sendListener.setBarcodeData(null);
|
||||
return;
|
||||
}
|
||||
if (barcodeData.address != null) {
|
||||
etAddress.getEditText().setText(barcodeData.address);
|
||||
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));
|
||||
}
|
||||
if (barcodeData.getAddress() != null) {
|
||||
etAddress.getEditText().setText(barcodeData.getAddress());
|
||||
} else {
|
||||
etAddress.getEditText().getText().clear();
|
||||
etAddress.setError(null);
|
||||
}
|
||||
|
||||
String scannedNotes = barcodeData.addressName;
|
||||
String scannedNotes = barcodeData.getAddressName();
|
||||
if (scannedNotes == null) {
|
||||
scannedNotes = barcodeData.description;
|
||||
} else if (barcodeData.description != null) {
|
||||
scannedNotes = scannedNotes + ": " + barcodeData.description;
|
||||
scannedNotes = barcodeData.getDescription();
|
||||
} else if (barcodeData.getDescription() != null) {
|
||||
scannedNotes = scannedNotes + ": " + barcodeData.getDescription();
|
||||
}
|
||||
if (scannedNotes != null) {
|
||||
etNotes.getEditText().setText(scannedNotes);
|
||||
|
|
|
@ -23,8 +23,6 @@ import android.view.ViewGroup;
|
|||
import android.widget.ImageButton;
|
||||
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.data.BarcodeData;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
|
@ -38,17 +36,13 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
|||
|
||||
public static SendAmountWizardFragment newInstance(Listener listener) {
|
||||
SendAmountWizardFragment instance = new SendAmountWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
instance.sendListener = listener;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Listener sendListener;
|
||||
private Listener sendListener;
|
||||
|
||||
public void setSendListener(Listener listener) {
|
||||
this.sendListener = listener;
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
public interface Listener {
|
||||
SendFragment.Listener getActivityCallback();
|
||||
|
||||
TxData getTxData();
|
||||
|
@ -139,7 +133,7 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
|||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
Helper.showKeyboard(getActivity());
|
||||
Helper.showKeyboard(requireActivity());
|
||||
final long funds = getTotalFunds();
|
||||
maxFunds = 1.0 * funds / Helper.ONE_XMR;
|
||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||
|
@ -150,8 +144,8 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
|||
getString(R.string.unknown_amount)));
|
||||
}
|
||||
final BarcodeData data = sendListener.popBarcodeData();
|
||||
if ((data != null) && (data.amount != null)) {
|
||||
etAmount.setAmount(data.amount);
|
||||
if ((data != null) && (data.getAmount() != null)) {
|
||||
etAmount.setAmount(data.getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,48 +17,49 @@
|
|||
package com.m2049r.xmrwallet.fragment.send;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
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.model.Wallet;
|
||||
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.QueryOrderParameters;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
|
||||
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.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.widget.ExchangeOtherEditText;
|
||||
import com.m2049r.xmrwallet.widget.SendProgressView;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
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) {
|
||||
SendBtcAmountWizardFragment instance = new SendBtcAmountWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
return instance;
|
||||
return new SendBtcAmountWizardFragment(listener);
|
||||
}
|
||||
|
||||
SendAmountWizardFragment.Listener sendListener;
|
||||
|
||||
public SendBtcAmountWizardFragment setSendListener(SendAmountWizardFragment.Listener listener) {
|
||||
this.sendListener = listener;
|
||||
return this;
|
||||
private SendBtcAmountWizardFragment(@NonNull SendAmountWizardFragment.Listener listener) {
|
||||
super();
|
||||
sendListener = listener;
|
||||
}
|
||||
|
||||
private final SendAmountWizardFragment.Listener sendListener;
|
||||
|
||||
private TextView tvFunds;
|
||||
private ExchangeOtherEditText etAmount;
|
||||
|
||||
|
@ -72,8 +73,6 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
|
||||
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
||||
|
||||
sendListener = (SendAmountWizardFragment.Listener) getParentFragment();
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false);
|
||||
|
||||
tvFunds = view.findViewById(R.id.tvFunds);
|
||||
|
@ -83,6 +82,8 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
|
||||
tvXmrToParms = view.findViewById(R.id.tvXmrToParms);
|
||||
|
||||
((ImageView) view.findViewById(R.id.shiftIcon)).setImageResource(service.getIconId());
|
||||
|
||||
etAmount = view.findViewById(R.id.etAmount);
|
||||
etAmount.requestFocus();
|
||||
|
||||
|
@ -98,32 +99,19 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
if (orderParameters == null) {
|
||||
return false; // this should never happen
|
||||
}
|
||||
if (sendListener != null) {
|
||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
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);
|
||||
}
|
||||
}
|
||||
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
txDataBtc.setShiftAmount(etAmount.getPrimaryAmount());
|
||||
return true;
|
||||
}
|
||||
|
||||
double maxBtc = 0;
|
||||
double minBtc = 0;
|
||||
private double maxBtc = 0;
|
||||
private double minBtc = 0;
|
||||
|
||||
@Override
|
||||
public void onPauseFragment() {
|
||||
super.onPauseFragment();
|
||||
llXmrToParms.setVisibility(View.INVISIBLE);
|
||||
etAmount.setListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -131,8 +119,8 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
final String btcSymbol = ((TxDataBtc) sendListener.getTxData()).getBtcSymbol();
|
||||
if (!btcSymbol.toLowerCase().equals(ServiceHelper.ASSET))
|
||||
throw new IllegalStateException("Asset Symbol is wrong!");
|
||||
if (!btcSymbol.equalsIgnoreCase(ShiftService.ASSET.getSymbol()))
|
||||
throw new IllegalStateException("Asset Symbol is wrong (" + btcSymbol + "!=" + ShiftService.ASSET.getSymbol() + ")");
|
||||
final long funds = getTotalFunds();
|
||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||
tvFunds.setText(getString(R.string.send_available,
|
||||
|
@ -143,48 +131,131 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
getString(R.string.unknown_amount)));
|
||||
}
|
||||
etAmount.setAmount("");
|
||||
etAmount.setListener(this);
|
||||
final BarcodeData data = sendListener.popBarcodeData();
|
||||
if (data != null) {
|
||||
if (data.amount != null) {
|
||||
etAmount.setAmount(data.amount);
|
||||
if (data.getAmount() != null) {
|
||||
etAmount.setAmount(data.getAmount());
|
||||
}
|
||||
}
|
||||
etAmount.setBaseCurrency(btcSymbol);
|
||||
callXmrTo();
|
||||
updateShift();
|
||||
}
|
||||
|
||||
long getTotalFunds() {
|
||||
return sendListener.getActivityCallback().getTotalFunds();
|
||||
}
|
||||
|
||||
private final ShiftService service = ShiftService.DEFAULT;
|
||||
private final PreShiftProcess preShiftProcess = service.createPreProcess(this);
|
||||
|
||||
private QueryOrderParameters orderParameters = null;
|
||||
|
||||
private void processOrderParms(final QueryOrderParameters orderParameters) {
|
||||
this.orderParameters = orderParameters;
|
||||
getView().post(() -> {
|
||||
private void reset() {
|
||||
orderParameters = null;
|
||||
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();
|
||||
etAmount.setExchangeRate(1 / price);
|
||||
maxBtc = price * orderParameters.getUpperLimit();
|
||||
minBtc = price * orderParameters.getLowerLimit();
|
||||
this.orderParameters = orderParameters;
|
||||
requireView().post(() -> {
|
||||
etAmount.setExchangeRate(1 / price);
|
||||
Timber.d("minBtc=%f / maxBtc=%f", minBtc, maxBtc);
|
||||
NumberFormat df = NumberFormat.getInstance(Locale.US);
|
||||
df.setMaximumFractionDigits(6);
|
||||
String min = df.format(minBtc);
|
||||
String max = df.format(maxBtc);
|
||||
String rate = df.format(price);
|
||||
final String min = AmountHelper.format_6(minBtc);
|
||||
final String max = AmountHelper.format_6(maxBtc);
|
||||
final String rate = AmountHelper.format_6(price);
|
||||
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
|
||||
min, max, rate, txDataBtc.getBtcSymbol()));
|
||||
final Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
|
||||
min, max, rate, txDataBtc.getBtcSymbol(), service.getLabel()));
|
||||
tvXmrToParms.setText(xmrParmText);
|
||||
|
||||
final long funds = getTotalFunds();
|
||||
double availableXmr = 1.0 * funds / Helper.ONE_XMR;
|
||||
final double availableXmr = 1.0 * funds / Helper.ONE_XMR;
|
||||
|
||||
String availBtcString;
|
||||
String availXmrString;
|
||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||
availBtcString = df.format(availableXmr * price);
|
||||
availXmrString = df.format(availableXmr);
|
||||
availBtcString = AmountHelper.format_6(availableXmr * price);
|
||||
availXmrString = AmountHelper.format_6(availableXmr);
|
||||
} else {
|
||||
availBtcString = getString(R.string.unknown_amount);
|
||||
availXmrString = availBtcString;
|
||||
|
@ -198,66 +269,28 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
});
|
||||
}
|
||||
|
||||
private void processOrderParmsError(final Exception ex) {
|
||||
etAmount.setExchangeRate(0);
|
||||
orderParameters = null;
|
||||
maxBtc = 0;
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return true;
|
||||
} // TODO Test what happens if we swtich away while querying
|
||||
|
||||
private void callXmrTo() {
|
||||
@Override
|
||||
public void showProgress() {
|
||||
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
|
||||
public void onError(final Exception e) {
|
||||
processOrderParmsError(e);
|
||||
public void onExchangeRequested() {
|
||||
final long now = System.currentTimeMillis();
|
||||
lastRequest = now;
|
||||
handler.postDelayed(() -> {
|
||||
if (now == lastRequest) { // otherwise we are superseded
|
||||
updateShift();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private SideShiftApi xmrToApi = null;
|
||||
|
||||
private SideShiftApi getXmrToApi() {
|
||||
if (xmrToApi == null) {
|
||||
synchronized (this) {
|
||||
if (xmrToApi == null) {
|
||||
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
return xmrToApi;
|
||||
}, EXCHANGE_TIME);
|
||||
}
|
||||
}
|
|
@ -24,39 +24,36 @@ import android.widget.Button;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
import com.m2049r.xmrwallet.data.TxDataBtc;
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
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.ShiftException;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
|
||||
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
|
||||
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.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.widget.SendProgressView;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
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) {
|
||||
SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
instance.sendListener = listener;
|
||||
return instance;
|
||||
}
|
||||
|
||||
SendConfirmWizardFragment.Listener sendListener;
|
||||
|
||||
public void setSendListener(SendConfirmWizardFragment.Listener listener) {
|
||||
this.sendListener = listener;
|
||||
}
|
||||
private SendConfirmWizardFragment.Listener sendListener;
|
||||
|
||||
private View llStageA;
|
||||
private SendProgressView evStageA;
|
||||
|
@ -77,11 +74,12 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
|||
private TextView tvTxChange;
|
||||
private View llPocketChange;
|
||||
|
||||
private TextView tvTxXmrToKeyLabel;
|
||||
private TextView tvTxXmrToInfo;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState)));
|
||||
|
||||
View view = inflater.inflate(
|
||||
|
@ -93,6 +91,10 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
|||
tvTxBtcRate = view.findViewById(R.id.tvTxBtcRate);
|
||||
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
|
||||
|
||||
tvTxXmrToKeyLabel = view.findViewById(R.id.tvTxXmrToKeyLabel);
|
||||
tvTxXmrToInfo = view.findViewById(R.id.tvTxXmrToInfo);
|
||||
|
||||
|
||||
tvTxFee = view.findViewById(R.id.tvTxFee);
|
||||
tvTxTotal = view.findViewById(R.id.tvTxTotal);
|
||||
tvTxChange = view.findViewById(R.id.tvTxChange);
|
||||
|
@ -106,7 +108,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
|||
evStageC = view.findViewById(R.id.evStageC);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -125,68 +127,74 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
|||
return view;
|
||||
}
|
||||
|
||||
int inProgress = 0;
|
||||
final static int STAGE_X = 0;
|
||||
final static int STAGE_A = 1;
|
||||
final static int STAGE_B = 2;
|
||||
final static int STAGE_C = 3;
|
||||
@NonNull
|
||||
Shifter.Stage inProgress = Stage.X;
|
||||
|
||||
private void showProgress(int stage, String progressText) {
|
||||
Timber.d("showProgress(%d)", stage);
|
||||
inProgress = stage;
|
||||
@Override
|
||||
public void showProgress(@NonNull Shifter.Stage stage) {
|
||||
Timber.d("showProgress(%s)", stage);
|
||||
requireView().post(() -> {
|
||||
switch (stage) {
|
||||
case STAGE_A:
|
||||
evStageA.showProgress(progressText);
|
||||
case A:
|
||||
evStageA.showProgress(getString(R.string.label_send_progress_xmrto_create));
|
||||
break;
|
||||
case STAGE_B:
|
||||
evStageB.showProgress(progressText);
|
||||
case B:
|
||||
evStageB.showProgress(getString(R.string.label_send_progress_xmrto_query));
|
||||
break;
|
||||
case STAGE_C:
|
||||
evStageC.showProgress(progressText);
|
||||
case C:
|
||||
evStageC.showProgress(getString(R.string.label_send_progress_create_tx));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("unknown stage " + stage);
|
||||
throw new IllegalArgumentException("invalid stage " + stage);
|
||||
}
|
||||
inProgress = stage;
|
||||
});
|
||||
}
|
||||
|
||||
public void hideProgress() {
|
||||
Timber.d("hideProgress(%d)", inProgress);
|
||||
Timber.d("hideProgress(%s)", inProgress);
|
||||
switch (inProgress) {
|
||||
case STAGE_A:
|
||||
case A:
|
||||
evStageA.hideProgress();
|
||||
llStageA.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case STAGE_B:
|
||||
case B:
|
||||
evStageB.hideProgress();
|
||||
llStageA.setVisibility(View.VISIBLE); // show Stage A info when B is ready
|
||||
llStageB.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case STAGE_C:
|
||||
case C:
|
||||
evStageC.hideProgress();
|
||||
llStageC.setVisibility(View.VISIBLE);
|
||||
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) {
|
||||
case STAGE_A:
|
||||
case A:
|
||||
evStageA.showMessage(code, message, solution);
|
||||
break;
|
||||
case STAGE_B:
|
||||
case B:
|
||||
evStageB.showMessage(code, message, solution);
|
||||
break;
|
||||
case STAGE_C:
|
||||
case C:
|
||||
evStageC.showMessage(code, message, solution);
|
||||
break;
|
||||
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;
|
||||
|
||||
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();
|
||||
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();
|
||||
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
|
||||
requireActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
private String orderId = null;
|
||||
|
||||
@Override
|
||||
// callback from wallet when PendingTransaction created (started by prepareSend() here
|
||||
public void transactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
|
||||
if (isResumed
|
||||
&& (inProgress == STAGE_C)
|
||||
&& (xmrtoOrder != null)
|
||||
&& (xmrtoOrder.getOrderId().equals(txTag))) {
|
||||
&& (inProgress == Stage.C)
|
||||
&& (orderId != null)
|
||||
&& (orderId.equals(txTag))) {
|
||||
this.pendingTransaction = pendingTransaction;
|
||||
getView().post(() -> {
|
||||
requireView().post(() -> {
|
||||
Timber.d("transactionCreated");
|
||||
hideProgress();
|
||||
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
|
||||
tvTxTotal.setText(Wallet.getDisplayAmount(
|
||||
|
@ -243,29 +251,35 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
|||
if (pendingTransaction != 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,
|
||||
getString(R.string.text_noretry_monero));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isResumed = false;
|
||||
|
||||
@Override
|
||||
public void onPauseFragment() {
|
||||
isResumed = false;
|
||||
shiftProcess = null;
|
||||
stopSendTimer();
|
||||
sendListener.disposeTransaction();
|
||||
pendingTransaction = null;
|
||||
inProgress = STAGE_X;
|
||||
inProgress = Stage.X;
|
||||
//TODO: maybe reset the progress messages
|
||||
updateSendButton();
|
||||
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
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
|
@ -273,8 +287,9 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
|||
if (sendListener.getMode() != SendFragment.Mode.BTC) {
|
||||
throw new IllegalStateException("Mode is not BTC!");
|
||||
}
|
||||
if (!((TxDataBtc) sendListener.getTxData()).getBtcSymbol().toLowerCase().equals(ServiceHelper.ASSET))
|
||||
throw new IllegalStateException("Asset Symbol is wrong!");
|
||||
final String btcSymbol = getTxData().getBtcSymbol();
|
||||
if (!btcSymbol.equalsIgnoreCase(ShiftService.ASSET.getSymbol()))
|
||||
throw new IllegalStateException("Asset Symbol is wrong (" + btcSymbol + "!=" + ShiftService.ASSET.getSymbol() + ")");
|
||||
Helper.hideKeyboard(getActivity());
|
||||
llStageA.setVisibility(View.INVISIBLE);
|
||||
evStageA.hideProgress();
|
||||
|
@ -282,9 +297,17 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
|||
evStageB.hideProgress();
|
||||
llStageC.setVisibility(View.INVISIBLE);
|
||||
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;
|
||||
if ((pendingTransaction == null) && (inProgress == STAGE_X)) {
|
||||
stageA();
|
||||
if ((pendingTransaction == null) && (inProgress == Stage.X)) {
|
||||
Timber.d("Starting ShiftProcess");
|
||||
shiftProcess.run(getTxData());
|
||||
} // otherwise just sit there blank
|
||||
// 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 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));
|
||||
if (sendCountdown > 0) {
|
||||
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() {
|
||||
getView().removeCallbacks(updateRunnable);
|
||||
requireView().removeCallbacks(updateRunnable);
|
||||
}
|
||||
|
||||
void updateSendButton() {
|
||||
|
@ -344,7 +367,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
|||
}
|
||||
|
||||
public void fail(String walletName) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
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()
|
||||
// or createTransactionFailed()
|
||||
void prepareSend() {
|
||||
public void prepareSend(CreateOrder order) {
|
||||
if (!isResumed) return;
|
||||
if ((xmrtoOrder == null)) {
|
||||
throw new IllegalStateException("xmrtoOrder is null");
|
||||
if ((order == null)) {
|
||||
throw new IllegalStateException("order is null");
|
||||
}
|
||||
showProgress(3, getString(R.string.label_send_progress_create_tx));
|
||||
final TxData txData = sendListener.getTxData();
|
||||
txData.setDestination(xmrtoOrder.getXmrAddress());
|
||||
txData.setAmount(xmrtoOrder.getXmrAmount());
|
||||
getActivityCallback().onPrepareSend(xmrtoOrder.getOrderId(), txData);
|
||||
showProgress(Stage.C);
|
||||
orderId = order.getOrderId();
|
||||
final TxDataBtc txData = getTxData();
|
||||
txData.setDestination(order.getXmrAddress());
|
||||
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() {
|
||||
return sendListener.getActivityCallback();
|
||||
}
|
||||
|
||||
private RequestQuote xmrtoQuote = null;
|
||||
private ShiftProcess shiftProcess;
|
||||
|
||||
private void processStageA(final RequestQuote requestQuote) {
|
||||
Timber.d("processCreateOrder %s", requestQuote.getId());
|
||||
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());
|
||||
public void showQuote(double btcAmount, double xmrAmount, double price) {
|
||||
final String symbol = getTxData().getBtcSymbol();
|
||||
tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount,
|
||||
btcAmount, xmrAmountTotal, txDataBtc.getBtcSymbol()));
|
||||
final String xmrPriceBtc = df.format(xmrtoQuote.getPrice());
|
||||
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, xmrPriceBtc, txDataBtc.getBtcSymbol()));
|
||||
AmountHelper.format(btcAmount), AmountHelper.format(xmrAmount), symbol));
|
||||
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, AmountHelper.format(price), symbol));
|
||||
}
|
||||
|
||||
// Shifter
|
||||
public void onQuoteReceived(RequestQuote quote) {
|
||||
requireView().post(() -> {
|
||||
Timber.d("onQuoteReceived");
|
||||
showQuote(quote.getBtcAmount(), quote.getXmrAmount(), quote.getPrice());
|
||||
hideProgress();
|
||||
});
|
||||
stageB(requestQuote.getId());
|
||||
}
|
||||
|
||||
private void processStageAError(final Exception ex) {
|
||||
public void onQuoteError(final Exception ex) {
|
||||
Timber.e("processStageAError %s", ex.getLocalizedMessage());
|
||||
getView().post(() -> {
|
||||
requireView().post(() -> {
|
||||
if (ex instanceof ShiftException) {
|
||||
ShiftException xmrEx = (ShiftException) ex;
|
||||
ShiftError xmrErr = xmrEx.getError();
|
||||
if (xmrErr != null) {
|
||||
if (xmrErr.isRetryable()) {
|
||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
||||
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
|
||||
getString(R.string.text_retry));
|
||||
evStageA.setOnClickListener(v -> {
|
||||
evStageA.setOnClickListener(null);
|
||||
stageA();
|
||||
shiftProcess.restart();
|
||||
});
|
||||
} else {
|
||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
||||
getString(R.string.text_noretry));
|
||||
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
|
||||
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||
}
|
||||
} 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_noretry));
|
||||
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||
}
|
||||
} else {
|
||||
evStageA.showMessage(getString(R.string.label_generic_xmrto_error),
|
||||
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
|
||||
public void onSuccess(RequestQuote requestQuote) {
|
||||
if (!isResumed) return;
|
||||
if (xmrtoQuote != null) {
|
||||
Timber.w("another ongoing request quote request");
|
||||
return;
|
||||
}
|
||||
processStageA(requestQuote);
|
||||
public boolean isActive() {
|
||||
return isResumed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
if (!isResumed) return;
|
||||
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(() -> {
|
||||
public void onOrderCreated(CreateOrder order) {
|
||||
requireView().post(() -> {
|
||||
showQuote(order.getBtcAmount(), order.getXmrAmount(), order.getBtcAmount() / order.getXmrAmount());
|
||||
tvTxXmrToKey.setText(order.getOrderId());
|
||||
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();
|
||||
Timber.d("Expires @ %s", order.getExpiresAt().toString());
|
||||
final int timeout = (int) (order.getExpiresAt().getTime() - order.getCreatedAt().getTime()) / 1000 - 60; // -1 minute buffer
|
||||
startSendTimer(timeout);
|
||||
prepareSend();
|
||||
prepareSend(order);
|
||||
});
|
||||
}
|
||||
|
||||
private void processStageBError(final Exception ex) {
|
||||
Timber.e("processCreateOrderError %s", ex.getLocalizedMessage());
|
||||
getView().post(() -> {
|
||||
public void onOrderError(final Exception ex) {
|
||||
Timber.e("onOrderError %s", ex.getLocalizedMessage());
|
||||
requireView().post(() -> {
|
||||
if (ex instanceof ShiftException) {
|
||||
ShiftException xmrEx = (ShiftException) ex;
|
||||
ShiftError xmrErr = xmrEx.getError();
|
||||
if (xmrErr != null) {
|
||||
if (xmrErr.isRetryable()) {
|
||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
||||
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
|
||||
getString(R.string.text_retry));
|
||||
evStageB.setOnClickListener(v -> {
|
||||
evStageB.setOnClickListener(null);
|
||||
stageB(xmrtoOrder.getOrderId());
|
||||
shiftProcess.retryCreateOrder();
|
||||
});
|
||||
} else {
|
||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
||||
getString(R.string.text_noretry));
|
||||
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(), null);
|
||||
}
|
||||
} 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_noretry));
|
||||
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
|
||||
}
|
||||
} else {
|
||||
evStageB.showMessage(getString(R.string.label_generic_xmrto_error),
|
||||
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
|
||||
public void onError(Exception ex) {
|
||||
if (!isResumed) return;
|
||||
processStageBError(ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private SideShiftApi xmrToApi = null;
|
||||
|
||||
private SideShiftApi getXmrToApi() {
|
||||
if (xmrToApi == null) {
|
||||
synchronized (this) {
|
||||
if (xmrToApi == null) {
|
||||
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
return xmrToApi;
|
||||
public void invalidateShift() {
|
||||
orderId = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.fragment.send;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -32,16 +31,17 @@ import android.widget.Toast;
|
|||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
import com.m2049r.xmrwallet.data.PendingTx;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
import com.m2049r.xmrwallet.data.TxDataBtc;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftService;
|
||||
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
|
||||
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
|
@ -51,15 +51,11 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
|||
|
||||
public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) {
|
||||
SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
instance.sendListener = listener;
|
||||
return instance;
|
||||
}
|
||||
|
||||
SendSuccessWizardFragment.Listener sendListener;
|
||||
|
||||
public void setSendListener(SendSuccessWizardFragment.Listener listener) {
|
||||
this.sendListener = listener;
|
||||
}
|
||||
private SendSuccessWizardFragment.Listener sendListener;
|
||||
|
||||
ImageButton bCopyTxId;
|
||||
private TextView tvTxId;
|
||||
|
@ -80,6 +76,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
|||
Bundle savedInstanceState) {
|
||||
|
||||
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
||||
final ShiftService shiftService = getTxData().getShiftService();
|
||||
|
||||
View view = inflater.inflate(
|
||||
R.layout.fragment_send_btc_success, container, false);
|
||||
|
@ -87,7 +84,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
|||
bCopyTxId = view.findViewById(R.id.bCopyTxId);
|
||||
bCopyTxId.setEnabled(false);
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -105,23 +102,22 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
|||
pbXmrto = view.findViewById(R.id.pbXmrto);
|
||||
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.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();
|
||||
});
|
||||
|
||||
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
|
||||
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
||||
tvXmrToSupport.setText(getString(R.string.label_send_btc_xmrto_info, shiftService.getLabel()));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isResumed = false;
|
||||
|
||||
@Override
|
||||
|
@ -130,7 +126,15 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
|||
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
|
||||
public void onResumeFragment() {
|
||||
|
@ -139,8 +143,8 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
|||
Helper.hideKeyboard(getActivity());
|
||||
isResumed = true;
|
||||
|
||||
btcData = (TxDataBtc) sendListener.getTxData();
|
||||
tvTxAddress.setText(btcData.getDestination());
|
||||
final TxDataBtc txData = getTxData();
|
||||
tvTxAddress.setText(txData.getDestination());
|
||||
|
||||
final PendingTx committedTx = sendListener.getCommittedTx();
|
||||
if (committedTx != null) {
|
||||
|
@ -148,45 +152,41 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
|||
bCopyTxId.setEnabled(true);
|
||||
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
|
||||
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
|
||||
if (btcData != null) {
|
||||
NumberFormat df = NumberFormat.getInstance(Locale.US);
|
||||
df.setMaximumFractionDigits(12);
|
||||
String btcAmount = df.format(btcData.getBtcAmount());
|
||||
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, btcData.getBtcSymbol()));
|
||||
String btcAmount = df.format(txData.getBtcAmount());
|
||||
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, txData.getBtcSymbol()));
|
||||
//TODO btcData.getBtcAddress();
|
||||
tvTxXmrToKey.setText(btcData.getXmrtoOrderId());
|
||||
final Crypto crypto = Crypto.withSymbol(btcData.getBtcSymbol());
|
||||
tvTxXmrToKey.setText(txData.getXmrtoOrderId());
|
||||
final Crypto crypto = Crypto.withSymbol(txData.getBtcSymbol());
|
||||
assert crypto != null;
|
||||
ivXmrToIcon.setImageResource(crypto.getIconEnabledId());
|
||||
tvXmrToSupport.setOnClickListener(v -> {
|
||||
Uri orderUri = getXmrToApi().getQueryOrderUri(btcData.getXmrtoOrderId());
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, orderUri);
|
||||
startActivity(intent);
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, txData.getShiftService().getShiftApi().getQueryOrderUri(txData.getXmrtoOrderId())));
|
||||
});
|
||||
queryOrder();
|
||||
} else {
|
||||
throw new IllegalStateException("btcData is null");
|
||||
}
|
||||
}
|
||||
sendListener.enableDone();
|
||||
}
|
||||
|
||||
private void processQueryOrder(final QueryOrderStatus status) {
|
||||
Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getOrderId());
|
||||
if (!btcData.getXmrtoOrderId().equals(status.getOrderId()))
|
||||
Timber.d("processQueryOrder %s for %s", status.getStatus().toString(), status.getOrderId());
|
||||
if (!getTxData().getXmrtoOrderId().equals(status.getOrderId()))
|
||||
throw new IllegalStateException("UUIDs do not match!");
|
||||
if (isResumed && (getView() != null))
|
||||
getView().post(() -> {
|
||||
showXmrToStatus(status);
|
||||
if (!status.isTerminal()) {
|
||||
getView().postDelayed(this::queryOrder, SideShiftApi.QUERY_INTERVAL);
|
||||
getView().postDelayed(this::queryOrder, ShiftApi.QUERY_INTERVAL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void queryOrder() {
|
||||
final TxDataBtc btcData = getTxData();
|
||||
Timber.d("queryOrder(%s)", btcData.getXmrtoOrderId());
|
||||
if (!isResumed) return;
|
||||
getXmrToApi().queryOrderStatus(btcData.getXmrtoOrderId(), new ShiftCallback<QueryOrderStatus>() {
|
||||
btcData.getShiftService().getShiftApi().queryOrderStatus(btcData.getXmrtoQueryOrderToken(), new ShiftCallback<QueryOrderStatus>() {
|
||||
@Override
|
||||
public void onSuccess(QueryOrderStatus status) {
|
||||
if (!isAdded()) return;
|
||||
|
@ -197,45 +197,54 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
|||
public void onError(final Exception ex) {
|
||||
if (!isResumed) return;
|
||||
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) {
|
||||
Toast.makeText(getActivity(), ((ShiftException) ex).getError().getErrorMsg(), Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(getActivity(), ((ShiftException) ex).getErrorMessage(), Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void showXmrToStatus(final QueryOrderStatus status) {
|
||||
int statusResource = 0;
|
||||
final TxDataBtc txData = getTxData();
|
||||
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;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), android.R.attr.colorError),
|
||||
ThemeHelper.getThemedColor(requireContext(), R.attr.negativeColor),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} 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;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor),
|
||||
ThemeHelper.getThemedColor(requireContext(), R.attr.positiveColor),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} else if (status.isWaiting()) {
|
||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid));
|
||||
statusResource = R.drawable.ic_pending;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
|
||||
ThemeHelper.getThemedColor(requireContext(), R.attr.neutralColor),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} else if (status.isPending()) {
|
||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid));
|
||||
statusResource = R.drawable.ic_pending;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
|
||||
ThemeHelper.getThemedColor(requireContext(), R.attr.neutralColor),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} else {
|
||||
throw new IllegalStateException("status is broken: " + status.toString());
|
||||
throw new IllegalStateException("status is broken: " + status);
|
||||
}
|
||||
ivXmrToStatus.setImageResource(statusResource);
|
||||
if (status.isTerminal()) {
|
||||
|
@ -246,17 +255,4 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
|||
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) {
|
||||
SendConfirmWizardFragment instance = new SendConfirmWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
instance.sendListener = listener;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Listener sendListener;
|
||||
private Listener sendListener;
|
||||
|
||||
public SendConfirmWizardFragment setSendListener(Listener listener) {
|
||||
this.sendListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
public interface Listener {
|
||||
SendFragment.Listener getActivityCallback();
|
||||
|
||||
TxData getTxData();
|
||||
|
@ -137,7 +132,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
|
|||
|
||||
void send() {
|
||||
sendListener.commitTransaction();
|
||||
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
|
||||
requireActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -153,7 +148,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
|
|||
}
|
||||
|
||||
private void showAlert(String title, String message) {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(getActivity());
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
|
||||
builder.setCancelable(true).
|
||||
setTitle(title).
|
||||
setMessage(message).
|
||||
|
@ -235,7 +230,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
|
|||
}
|
||||
|
||||
public void fail(String walletName) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
bSend.setEnabled(true); // allow to try again
|
||||
});
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ import com.m2049r.xmrwallet.widget.Toolbar;
|
|||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import lombok.Getter;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SendFragment extends Fragment
|
||||
|
@ -67,6 +68,7 @@ public class SendFragment extends Fragment
|
|||
|
||||
final static public int MIXIN = 0;
|
||||
|
||||
@Getter
|
||||
private Listener activityCallback;
|
||||
|
||||
public interface Listener {
|
||||
|
@ -273,7 +275,7 @@ public class SendFragment extends Fragment
|
|||
return false;
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
public enum Mode {
|
||||
XMR, BTC
|
||||
}
|
||||
|
||||
|
@ -293,7 +295,7 @@ public class SendFragment extends Fragment
|
|||
default:
|
||||
throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!");
|
||||
}
|
||||
getView().post(() -> pagerAdapter.notifyDataSetChanged());
|
||||
requireView().post(() -> pagerAdapter.notifyDataSetChanged());
|
||||
Timber.d("New Mode = %s", mode.toString());
|
||||
}
|
||||
}
|
||||
|
@ -478,11 +480,6 @@ public class SendFragment extends Fragment
|
|||
bDone.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public Listener getActivityCallback() {
|
||||
return activityCallback;
|
||||
}
|
||||
|
||||
|
||||
// callbacks from send service
|
||||
|
||||
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
|
||||
|
@ -502,12 +499,9 @@ public class SendFragment extends Fragment
|
|||
activityCallback.onDisposeRequest();
|
||||
}
|
||||
|
||||
@Getter
|
||||
PendingTx pendingTx;
|
||||
|
||||
public PendingTx getPendingTx() {
|
||||
return pendingTx;
|
||||
}
|
||||
|
||||
public void onCreateTransactionFailed(String errorText) {
|
||||
final SendConfirm confirm = getSendConfirm();
|
||||
if (confirm != null) {
|
||||
|
|
|
@ -36,18 +36,13 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
|
|||
|
||||
public static SendSuccessWizardFragment newInstance(Listener listener) {
|
||||
SendSuccessWizardFragment instance = new SendSuccessWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
instance.sendListener = listener;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Listener sendListener;
|
||||
private Listener sendListener;
|
||||
|
||||
public SendSuccessWizardFragment setSendListener(Listener listener) {
|
||||
this.sendListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
public interface Listener {
|
||||
TxData getTxData();
|
||||
|
||||
PendingTx getCommittedTx();
|
||||
|
@ -62,7 +57,6 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
|
|||
ImageButton bCopyTxId;
|
||||
private TextView tvTxId;
|
||||
private TextView tvTxAddress;
|
||||
private TextView tvTxPaymentId;
|
||||
private TextView tvTxAmount;
|
||||
private TextView tvTxFee;
|
||||
|
||||
|
@ -78,13 +72,12 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
|
|||
bCopyTxId = view.findViewById(R.id.bCopyTxId);
|
||||
bCopyTxId.setEnabled(false);
|
||||
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();
|
||||
});
|
||||
|
||||
tvTxId = view.findViewById(R.id.tvTxId);
|
||||
tvTxAddress = view.findViewById(R.id.tvTxAddress);
|
||||
tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId);
|
||||
tvTxAmount = view.findViewById(R.id.tvTxAmount);
|
||||
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;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -131,12 +132,13 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
|||
|
||||
private boolean itemsClickable = true;
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
public void allowClick(boolean clickable) {
|
||||
itemsClickable = clickable;
|
||||
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 View pbBookmark;
|
||||
final TextView tvName;
|
||||
|
@ -195,7 +197,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
|||
@Override
|
||||
public void onClick(View view) {
|
||||
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
|
||||
final NodeInfo node = nodeItems.get(position);
|
||||
if (node.isOnion()) {
|
||||
|
@ -218,7 +220,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
|
|||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
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
|
||||
return listener.onLongInteraction(view, nodeItems.get(position));
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
|
|||
return items.size();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Subaddress getItem(int position) {
|
||||
return items.get(position);
|
||||
}
|
||||
|
@ -108,7 +109,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
|
|||
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 tvAddress;
|
||||
final TextView tvAmount;
|
||||
|
@ -143,7 +144,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
|
|||
@Override
|
||||
public void onClick(View view) {
|
||||
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
|
||||
listener.onInteraction(view, getItem(position));
|
||||
}
|
||||
|
@ -153,7 +154,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
|
|||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
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
|
||||
return listener.onLongInteraction(view, getItem(position));
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package com.m2049r.xmrwallet.layout;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
|
@ -49,6 +50,7 @@ import java.util.TimeZone;
|
|||
import timber.log.Timber;
|
||||
|
||||
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 int outboundColour;
|
||||
|
@ -86,7 +88,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
|||
}
|
||||
|
||||
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> {
|
||||
|
@ -156,7 +158,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
|||
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 TextView tvAmount;
|
||||
final TextView tvFailed;
|
||||
|
@ -291,7 +293,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
|||
@Override
|
||||
public void onClick(View view) {
|
||||
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
|
||||
listener.onInteraction(view, infoItems.get(position));
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package com.m2049r.xmrwallet.layout;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
|
@ -44,6 +45,7 @@ import timber.log.Timber;
|
|||
|
||||
public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.ViewHolder> {
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
public interface OnInteractionListener {
|
||||
|
@ -120,7 +122,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
|
|||
diffResult.dispatchUpdatesTo(this);
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
final TextView tvName;
|
||||
final ImageButton ibOptions;
|
||||
WalletManager.WalletInfo infoItem;
|
||||
|
@ -164,7 +166,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
|
|||
@Override
|
||||
public void onClick(View view) {
|
||||
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
|
||||
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;
|
||||
|
||||
public class Ledger {
|
||||
public class Ledger implements Hardware {
|
||||
static final public boolean ENABLED = true;
|
||||
// 5:20 is same as wallet2.cpp::restore()
|
||||
static public final int LOOKAHEAD_ACCOUNTS = 5;
|
||||
static public final int LOOKAHEAD_SUBADDRESSES = 20;
|
||||
static public final String SUBADDRESS_LOOKAHEAD = LOOKAHEAD_ACCOUNTS + ":" + LOOKAHEAD_SUBADDRESSES;
|
||||
|
||||
private static final byte PROTOCOL_VERSION = 0x03;
|
||||
public static final int SW_OK = 0x9000;
|
||||
|
|
|
@ -111,7 +111,11 @@ public class Wallet {
|
|||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
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 subaddressLookahead;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.model;
|
|||
|
||||
import com.m2049r.xmrwallet.XmrWalletApplication;
|
||||
import com.m2049r.xmrwallet.data.Node;
|
||||
import com.m2049r.xmrwallet.ledger.Ledger;
|
||||
import com.m2049r.xmrwallet.util.RestoreHeight;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -161,10 +160,12 @@ public class WalletManager {
|
|||
String spendKeyString);
|
||||
|
||||
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,
|
||||
getNetworkType().getValue(), deviceName, restoreHeight,
|
||||
Ledger.SUBADDRESS_LOOKAHEAD);
|
||||
getNetworkType().getValue(), device.name(), restoreHeight,
|
||||
lookahead);
|
||||
Wallet wallet = new Wallet(walletHandle);
|
||||
manageWallet(wallet);
|
||||
return wallet;
|
||||
|
|
|
@ -19,6 +19,8 @@ package com.m2049r.xmrwallet.onboarding;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
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
|
||||
* 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 {
|
||||
// from src/cryptonote_config.h
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package com.m2049r.xmrwallet.service;
|
||||
|
||||
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
|
@ -581,8 +583,12 @@ public class WalletService extends Service {
|
|||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setContentIntent(pendingIntent)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private String createNotificationChannel() {
|
||||
|
|
|
@ -142,9 +142,13 @@ public class ExchangeApiImpl implements ExchangeApi {
|
|||
} catch (ParserConfigurationException | SAXException ex) {
|
||||
Timber.w(ex);
|
||||
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
} 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);
|
||||
httpRequest.enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(final Call call, final IOException ex) {
|
||||
public void onFailure(@NonNull final Call call, @NonNull final IOException ex) {
|
||||
callback.onError(ex);
|
||||
}
|
||||
|
||||
@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()) {
|
||||
try {
|
||||
final JSONObject json = new JSONObject(response.body().string());
|
||||
|
@ -108,9 +108,13 @@ public class ExchangeApiImpl implements ExchangeApi {
|
|||
}
|
||||
} catch (JSONException ex) {
|
||||
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
} 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));
|
||||
} catch (JSONException ex) {
|
||||
callback.onError(ex);
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
} 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 onError(Exception ex);
|
||||
|
||||
void onError(Exception ex, JSONObject json);
|
||||
}
|
||||
|
|
|
@ -21,8 +21,7 @@ import androidx.annotation.NonNull;
|
|||
import org.json.JSONObject;
|
||||
|
||||
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 call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback);
|
||||
void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback);
|
||||
}
|
||||
|
|
|
@ -18,32 +18,23 @@ package com.m2049r.xmrwallet.service.shift;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ShiftError {
|
||||
@Getter
|
||||
private final Error errorType;
|
||||
private final Type type;
|
||||
@Getter
|
||||
private final String errorMsg;
|
||||
|
||||
public enum Error {
|
||||
public enum Type {
|
||||
SERVICE,
|
||||
INFRASTRUCTURE
|
||||
}
|
||||
|
||||
public boolean isRetryable() {
|
||||
return errorType == Error.INFRASTRUCTURE;
|
||||
}
|
||||
|
||||
public ShiftError(final JSONObject jsonObject) throws JSONException {
|
||||
final JSONObject errorObject = jsonObject.getJSONObject("error");
|
||||
errorType = Error.SERVICE;
|
||||
errorMsg = errorObject.getString("message");
|
||||
return type == Type.INFRASTRUCTURE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package com.m2049r.xmrwallet.service.shift;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
|
@ -24,10 +26,14 @@ public class ShiftException extends Exception {
|
|||
@Getter
|
||||
private final int code;
|
||||
@Getter
|
||||
@Nullable
|
||||
private final ShiftError error;
|
||||
|
||||
public ShiftException(int code) {
|
||||
this.code = code;
|
||||
this.error = null;
|
||||
this(code, 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.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
public interface CreateOrder {
|
||||
String TAG = "side";
|
||||
String getTag();
|
||||
|
||||
String getBtcCurrency();
|
||||
|
||||
|
@ -35,8 +37,11 @@ public interface CreateOrder {
|
|||
|
||||
String getXmrAddress();
|
||||
|
||||
Date getCreatedAt(); // createdAt
|
||||
Date getCreatedAt();
|
||||
|
||||
Date getExpiresAt(); // expiresAt
|
||||
Date getExpiresAt();
|
||||
|
||||
String getQueryOrderId();
|
||||
|
||||
ShiftType getType();
|
||||
}
|
|
@ -14,14 +14,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.service.shift.sideshift.api;
|
||||
package com.m2049r.xmrwallet.service.shift.api;
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
public interface RequestQuote {
|
||||
|
||||
double getBtcAmount(); // settleAmount
|
||||
double getBtcAmount(); // what we want to receive
|
||||
|
||||
double getXmrAmount(); // the XMR we need to send
|
||||
|
||||
String getId(); // id
|
||||
|
||||
|
@ -28,7 +32,7 @@ public interface RequestQuote {
|
|||
|
||||
Date getExpiresAt(); // expiresAt
|
||||
|
||||
double getXmrAmount(); // depositAmount
|
||||
|
||||
double getPrice(); // rate
|
||||
|
||||
ShiftType getType();
|
||||
}
|
|
@ -14,15 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.service.shift.sideshift.api;
|
||||
package com.m2049r.xmrwallet.service.shift.api;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.m2049r.xmrwallet.data.CryptoAmount;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||
|
||||
public interface SideShiftApi {
|
||||
public interface ShiftApi {
|
||||
int QUERY_INTERVAL = 5000; // ms
|
||||
|
||||
/**
|
||||
|
@ -30,22 +32,23 @@ public interface SideShiftApi {
|
|||
*
|
||||
* @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
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @param quoteId the desired XMR amount
|
||||
* @param quote the quote from {@link #requestQuote(String, CryptoAmount, ShiftCallback)}
|
||||
* @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
|
|
@ -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 {
|
||||
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 long ONE_XMR = Math.round(Math.pow(10, Helper.XMR_DECIMALS));
|
||||
|
||||
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 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.IntentFilter;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
|
||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
|
||||
import com.burgstaller.okhttp.digest.CachingAuthenticator;
|
||||
|
@ -39,10 +41,11 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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.StatusCallback;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
|
@ -56,11 +59,11 @@ import timber.log.Timber;
|
|||
@RequiredArgsConstructor
|
||||
public class NetCipherHelper implements StatusCallback {
|
||||
public static final String USER_AGENT = "Monerujo/1.0";
|
||||
public static final int HTTP_TIMEOUT_CONNECT = 1000; //ms
|
||||
public static final int HTTP_TIMEOUT_READ = 2000; //ms
|
||||
public static final int HTTP_TIMEOUT_WRITE = 1000; //ms
|
||||
public static final int TOR_TIMEOUT_CONNECT = 5000; //ms
|
||||
public static final int TOR_TIMEOUT = 2000; //ms
|
||||
public static final int HTTP_TIMEOUT_CONNECT = 2500; //ms
|
||||
public static final int HTTP_TIMEOUT_READ = 5000; //ms
|
||||
public static final int HTTP_TIMEOUT_WRITE = 2500; //ms
|
||||
public static final int TOR_TIMEOUT_CONNECT = 10000; //ms
|
||||
public static final int TOR_TIMEOUT = 5000; //ms
|
||||
|
||||
public interface OnStatusChangedListener {
|
||||
void connected();
|
||||
|
@ -73,7 +76,7 @@ public class NetCipherHelper implements StatusCallback {
|
|||
}
|
||||
|
||||
final private Context context;
|
||||
final private OrbotHelper orbot;
|
||||
final private MyOrbotHelper orbot;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static NetCipherHelper Instance;
|
||||
|
@ -83,7 +86,7 @@ public class NetCipherHelper implements StatusCallback {
|
|||
synchronized (NetCipherHelper.class) {
|
||||
if (Instance == null) {
|
||||
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 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.equals(OrbotHelper.STATUS_ON))
|
||||
if (!orbotStatus.equals(MyOrbotHelper.STATUS_ON))
|
||||
throw new IllegalStateException("Orbot is not ON");
|
||||
try {
|
||||
final OkHttpClient.Builder okBuilder = new OkHttpClient.Builder()
|
||||
|
@ -110,7 +113,6 @@ public class NetCipherHelper implements StatusCallback {
|
|||
.withSocksProxy()
|
||||
.applyTo(okBuilder, statusIntent)
|
||||
.build();
|
||||
Helper.ALLOW_SHIFT = false; // no shifting with Tor
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
|
@ -123,7 +125,6 @@ public class NetCipherHelper implements StatusCallback {
|
|||
.writeTimeout(HTTP_TIMEOUT_WRITE, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(HTTP_TIMEOUT_READ, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
Helper.ALLOW_SHIFT = true;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
|
@ -144,7 +145,7 @@ public class NetCipherHelper implements StatusCallback {
|
|||
.addStatusCallback(me);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
@ -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");
|
||||
|
||||
return null != SignatureUtils.validateBroadcastIntent(context,
|
||||
OrbotHelper.getOrbotStartIntent(context),
|
||||
MyOrbotHelper.getOrbotStartIntent(context),
|
||||
hashes, false);
|
||||
}
|
||||
|
||||
|
@ -293,19 +294,36 @@ public class NetCipherHelper implements StatusCallback {
|
|||
@ToString
|
||||
static public class Request {
|
||||
final HttpUrl url;
|
||||
final String json;
|
||||
final JSONObject data;
|
||||
final String username;
|
||||
final String password;
|
||||
@Setter
|
||||
RequestAugmenter augmenter;
|
||||
|
||||
public Request(final HttpUrl url, final String json, final String username, final String password) {
|
||||
this.url = url;
|
||||
this.json = json;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
final Method method;
|
||||
|
||||
public enum Method {
|
||||
GET, POST;
|
||||
}
|
||||
|
||||
public Request(final HttpUrl url, final JSONObject json) {
|
||||
this(url, json == null ? null : json.toString(), null, null);
|
||||
public interface RequestAugmenter {
|
||||
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) {
|
||||
|
@ -344,11 +362,16 @@ public class NetCipherHelper implements StatusCallback {
|
|||
final okhttp3.Request.Builder builder =
|
||||
new okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", USER_AGENT);
|
||||
if (json != null) {
|
||||
builder.post(RequestBody.create(json, MediaType.parse("application/json")));
|
||||
} else {
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", "application/json");
|
||||
if (augmenter != null) augmenter.augment(builder);
|
||||
switch (method) {
|
||||
case GET:
|
||||
builder.get();
|
||||
break;
|
||||
case POST:
|
||||
builder.post(RequestBody.create(data.toString(), MediaType.parse("application/json")));
|
||||
break;
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
@ -380,9 +403,9 @@ public class NetCipherHelper implements StatusCallback {
|
|||
private static final BroadcastReceiver orbotStatusReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Timber.d("%s/%s", intent.getAction(), intent.getStringExtra(OrbotHelper.EXTRA_STATUS));
|
||||
if (OrbotHelper.ACTION_STATUS.equals(intent.getAction())) {
|
||||
if (OrbotHelper.STATUS_STARTS_DISABLED.equals(intent.getStringExtra(OrbotHelper.EXTRA_STATUS))) {
|
||||
Timber.d("%s/%s", intent.getAction(), intent.getStringExtra(MyOrbotHelper.EXTRA_STATUS));
|
||||
if (MyOrbotHelper.ACTION_STATUS.equals(intent.getAction())) {
|
||||
if (MyOrbotHelper.STATUS_STARTS_DISABLED.equals(intent.getStringExtra(MyOrbotHelper.EXTRA_STATUS))) {
|
||||
getInstance().onNotEnabled();
|
||||
}
|
||||
}
|
||||
|
@ -390,6 +413,6 @@ public class NetCipherHelper implements StatusCallback {
|
|||
};
|
||||
|
||||
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) {
|
||||
BarcodeData bc = BarcodeData.parseOpenAlias(txt, dnssec);
|
||||
if (bc != null) {
|
||||
if (!dataMap.containsKey(bc.asset)) {
|
||||
dataMap.put(bc.asset, bc);
|
||||
if (!dataMap.containsKey(bc.getAsset())) {
|
||||
dataMap.put(bc.getAsset(), bc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,16 +7,6 @@ import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
|||
import okhttp3.HttpUrl;
|
||||
|
||||
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() {
|
||||
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