mirror of https://github.com/m2049r/xmrwallet.git
parent
dab5e08910
commit
c23597066a
|
@ -43,28 +43,38 @@ dependencies {
|
||||||
compile 'com.android.support:cardview-v7:25.4.0'
|
compile 'com.android.support:cardview-v7:25.4.0'
|
||||||
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
compile 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||||
compile 'me.dm7.barcodescanner:zxing:1.9.8'
|
compile 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||||
|
|
||||||
|
compile "com.squareup.okhttp3:okhttp:$rootProject.ext.okHttpVersion"
|
||||||
|
|
||||||
|
testCompile "junit:junit:$rootProject.ext.junitVersion"
|
||||||
|
testCompile "org.mockito:mockito-all:$rootProject.ext.mockitoVersion"
|
||||||
|
testCompile "com.squareup.okhttp3:mockwebserver:$rootProject.ext.okHttpVersion"
|
||||||
|
testCompile 'org.json:json:20140107'
|
||||||
|
testCompile 'net.jodah:concurrentunit:0.4.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyVerification {
|
dependencyVerification {
|
||||||
verify = [
|
verify = [
|
||||||
'com.android.support:appcompat-v7:70551e62660db15b790c5275f56b9de4dd9407d1494d07c8f3dd5698f3638677',
|
|
||||||
'com.android.support:design:3f409bf2019967ffc344cfaf11e52131fac982468a1707aaeb25bf3c52838966',
|
'com.android.support:design:3f409bf2019967ffc344cfaf11e52131fac982468a1707aaeb25bf3c52838966',
|
||||||
|
'com.android.support:appcompat-v7:70551e62660db15b790c5275f56b9de4dd9407d1494d07c8f3dd5698f3638677',
|
||||||
|
'com.android.support:transition:848270144fb180efd2bf928a00ed176dbbc5290badfd638390ffba90088df8b3',
|
||||||
|
'me.dm7.barcodescanner:zxing:d43973c9527c23fa8e6d338c6a2c458e373ce1ac6bcaa3bc41d11ae49116000d',
|
||||||
|
'me.dm7.barcodescanner:core:a5c8a704089b58029db166172ed8e55d756877d010a85a0b1c94fdc96ffb8f9a',
|
||||||
'com.android.support:support-v4:ee44c481a1f4d6978568e223e8125379b52b2ececdd53450e09ebae144bd377d',
|
'com.android.support:support-v4:ee44c481a1f4d6978568e223e8125379b52b2ececdd53450e09ebae144bd377d',
|
||||||
'com.android.support:recyclerview-v7:a2fe121f9d01ed8980e97095b4a3fe9700a0aa0a7d4b0f8c594f765ad8455a0d',
|
'com.android.support:recyclerview-v7:a2fe121f9d01ed8980e97095b4a3fe9700a0aa0a7d4b0f8c594f765ad8455a0d',
|
||||||
'com.android.support:cardview-v7:f3fbbe1fcfdbec7333c6a2c516c5fd511a909d1975271818e268d6fe297d8c70',
|
'com.android.support:cardview-v7:f3fbbe1fcfdbec7333c6a2c516c5fd511a909d1975271818e268d6fe297d8c70',
|
||||||
'com.android.support.constraint:constraint-layout:b0c688cc2b7172608f8153a689d746da40f71e52d7e2fe2bfd9df2f92db77085',
|
'com.android.support.constraint:constraint-layout:b0c688cc2b7172608f8153a689d746da40f71e52d7e2fe2bfd9df2f92db77085',
|
||||||
'me.dm7.barcodescanner:zxing:d43973c9527c23fa8e6d338c6a2c458e373ce1ac6bcaa3bc41d11ae49116000d',
|
|
||||||
'com.android.support:support-annotations:a774272036941b4e912eb426d70c848bde7f06a3bf5fb491f75a427dc6595270',
|
|
||||||
'com.android.support:support-vector-drawable:077009d13882ee96f061e4bc2dbe7cce7ae1762d8297592a787ff741afbfb1f2',
|
|
||||||
'com.android.support:animated-vector-drawable:628ab1d56a6ee4cbedf32617af8b2a1fe02964ed0628e8f898cc09ddba6e1835',
|
'com.android.support:animated-vector-drawable:628ab1d56a6ee4cbedf32617af8b2a1fe02964ed0628e8f898cc09ddba6e1835',
|
||||||
'com.android.support:transition:848270144fb180efd2bf928a00ed176dbbc5290badfd638390ffba90088df8b3',
|
'com.android.support:support-vector-drawable:077009d13882ee96f061e4bc2dbe7cce7ae1762d8297592a787ff741afbfb1f2',
|
||||||
'com.android.support:support-compat:54019c63614ce08b02d7b9605490cd2b29ba5b2505f394a9517450b5f72b30ca',
|
'com.android.support:support-fragment:316d35d4d2d2902057efad104a73e4bdb50bee260a7075678185b8cd71170945',
|
||||||
|
'com.android.support:support-core-ui:e72ae29b823889686cff6fcb948d6745c2baf6d4c2af4fdffa1ec1e42e3833a3',
|
||||||
'com.android.support:support-media-compat:566a161d9cb0083ef62a53e46b71ce5b3d455b8635b1a0a4ae28d96d4b583de8',
|
'com.android.support:support-media-compat:566a161d9cb0083ef62a53e46b71ce5b3d455b8635b1a0a4ae28d96d4b583de8',
|
||||||
'com.android.support:support-core-utils:34b8437dfa95ff28d29cf57ffa3b1354a9fa9bfe4059f0fd5ce2f5e4326a1748',
|
'com.android.support:support-core-utils:34b8437dfa95ff28d29cf57ffa3b1354a9fa9bfe4059f0fd5ce2f5e4326a1748',
|
||||||
'com.android.support:support-core-ui:e72ae29b823889686cff6fcb948d6745c2baf6d4c2af4fdffa1ec1e42e3833a3',
|
'com.android.support:support-compat:54019c63614ce08b02d7b9605490cd2b29ba5b2505f394a9517450b5f72b30ca',
|
||||||
'com.android.support:support-fragment:316d35d4d2d2902057efad104a73e4bdb50bee260a7075678185b8cd71170945',
|
'com.android.support:support-annotations:a774272036941b4e912eb426d70c848bde7f06a3bf5fb491f75a427dc6595270',
|
||||||
'com.android.support.constraint:constraint-layout-solver:8c62525a9bc5cff5633a96cb9b32fffeccaf41b8841aa87fc22607070dea9b8d',
|
'com.android.support.constraint:constraint-layout-solver:8c62525a9bc5cff5633a96cb9b32fffeccaf41b8841aa87fc22607070dea9b8d',
|
||||||
'me.dm7.barcodescanner:core:a5c8a704089b58029db166172ed8e55d756877d010a85a0b1c94fdc96ffb8f9a',
|
|
||||||
'com.google.zxing:core:bba7724e02a997cec38213af77133ee8e24b0d5cf5fa7ecbc16a4fa93f11ee0d',
|
'com.google.zxing:core:bba7724e02a997cec38213af77133ee8e24b0d5cf5fa7ecbc16a4fa93f11ee0d',
|
||||||
|
'com.squareup.okio:okio:734269c3ebc5090e3b23566db558f421f0b4027277c79ad5d176b8ec168bb850',
|
||||||
|
'com.squareup.okhttp3:okhttp:7265adbd6f028aade307f58569d814835cd02bc9beffb70c25f72c9de50d61c4',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ package com.m2049r.xmrwallet;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
@ -36,19 +38,23 @@ import android.widget.ProgressBar;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.layout.AsyncExchangeRate;
|
|
||||||
import com.m2049r.xmrwallet.layout.Toolbar;
|
import com.m2049r.xmrwallet.layout.Toolbar;
|
||||||
import com.m2049r.xmrwallet.layout.TransactionInfoAdapter;
|
import com.m2049r.xmrwallet.layout.TransactionInfoAdapter;
|
||||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||||
import com.m2049r.xmrwallet.model.Wallet;
|
import com.m2049r.xmrwallet.model.Wallet;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.kraken.ExchangeApiImpl;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
public class WalletFragment extends Fragment
|
public class WalletFragment extends Fragment
|
||||||
implements TransactionInfoAdapter.OnInteractionListener,
|
implements TransactionInfoAdapter.OnInteractionListener {
|
||||||
AsyncExchangeRate.Listener {
|
|
||||||
public static final String TAG = "WalletFragment";
|
public static final String TAG = "WalletFragment";
|
||||||
private TransactionInfoAdapter adapter;
|
private TransactionInfoAdapter adapter;
|
||||||
private NumberFormat formatter = NumberFormat.getInstance();
|
private NumberFormat formatter = NumberFormat.getInstance();
|
||||||
|
@ -141,48 +147,6 @@ public class WalletFragment extends Fragment
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
String balanceCurrency = "XMR";
|
|
||||||
double balanceRate = 1.0;
|
|
||||||
|
|
||||||
void refreshBalance() {
|
|
||||||
if (sCurrency.getSelectedItemPosition() == 0) { // XMR
|
|
||||||
double amountXmr = Double.parseDouble(Wallet.getDisplayAmount(unlockedBalance)); // assume this cannot fail!
|
|
||||||
tvBalance.setText(Helper.getFormattedAmount(amountXmr, true));
|
|
||||||
} else { // not XMR
|
|
||||||
String currency = (String) sCurrency.getSelectedItem();
|
|
||||||
if (!currency.equals(balanceCurrency) || (balanceRate <= 0)) {
|
|
||||||
showExchanging();
|
|
||||||
new AsyncExchangeRate(this).execute("XMR", currency);
|
|
||||||
} else {
|
|
||||||
exchange("XMR", balanceCurrency, balanceRate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isExchanging = false;
|
|
||||||
|
|
||||||
void showExchanging() {
|
|
||||||
isExchanging = true;
|
|
||||||
tvBalance.setVisibility(View.GONE);
|
|
||||||
flExchange.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void hideExchanging() {
|
|
||||||
isExchanging = false;
|
|
||||||
tvBalance.setVisibility(View.VISIBLE);
|
|
||||||
flExchange.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callbacks from AsyncExchangeRate
|
|
||||||
|
|
||||||
// callback from AsyncExchangeRate when it can't get exchange rate
|
|
||||||
public void exchangeFailed() {
|
|
||||||
sCurrency.setSelection(0, true); // default to XMR
|
|
||||||
double amountXmr = Double.parseDouble(Wallet.getDisplayAmount(unlockedBalance)); // assume this cannot fail!
|
|
||||||
tvBalance.setText(Helper.getFormattedAmount(amountXmr, true));
|
|
||||||
hideExchanging();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateBalance() {
|
void updateBalance() {
|
||||||
if (isExchanging) return; // wait for exchange to finish - it will fire this itself then.
|
if (isExchanging) return; // wait for exchange to finish - it will fire this itself then.
|
||||||
// at this point selection is XMR in case of error
|
// at this point selection is XMR in case of error
|
||||||
|
@ -197,24 +161,90 @@ public class WalletFragment extends Fragment
|
||||||
tvBalance.setText(displayB);
|
tvBalance.setText(displayB);
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback from AsyncExchangeRate when we have a rate
|
String balanceCurrency = "XMR";
|
||||||
public void exchange(String currencyA, String currencyB, double rate) {
|
double balanceRate = 1.0;
|
||||||
|
|
||||||
|
private final ExchangeApi exchangeApi = new ExchangeApiImpl(Helper.getOkHttpClient());
|
||||||
|
|
||||||
|
void refreshBalance() {
|
||||||
|
if (sCurrency.getSelectedItemPosition() == 0) { // XMR
|
||||||
|
double amountXmr = Double.parseDouble(Wallet.getDisplayAmount(unlockedBalance)); // assume this cannot fail!
|
||||||
|
tvBalance.setText(Helper.getFormattedAmount(amountXmr, true));
|
||||||
|
} else { // not XMR
|
||||||
|
String currency = (String) sCurrency.getSelectedItem();
|
||||||
|
if (!currency.equals(balanceCurrency) || (balanceRate <= 0)) {
|
||||||
|
showExchanging();
|
||||||
|
exchangeApi.queryExchangeRate("XMR", currency,
|
||||||
|
new ExchangeCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||||
|
if (isAdded())
|
||||||
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
exchange(exchangeRate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(final Exception e) {
|
||||||
|
Log.e(TAG, e.getLocalizedMessage());
|
||||||
|
if (isAdded())
|
||||||
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
exchangeFailed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateBalance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isExchanging = false;
|
||||||
|
|
||||||
|
void showExchanging() {
|
||||||
|
isExchanging = true;
|
||||||
|
tvBalance.setVisibility(View.GONE);
|
||||||
|
flExchange.setVisibility(View.VISIBLE);
|
||||||
|
sCurrency.setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideExchanging() {
|
||||||
|
isExchanging = false;
|
||||||
|
tvBalance.setVisibility(View.VISIBLE);
|
||||||
|
flExchange.setVisibility(View.GONE);
|
||||||
|
sCurrency.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exchangeFailed() {
|
||||||
|
sCurrency.setSelection(0, true); // default to XMR
|
||||||
|
double amountXmr = Double.parseDouble(Wallet.getDisplayAmount(unlockedBalance)); // assume this cannot fail!
|
||||||
|
tvBalance.setText(Helper.getFormattedAmount(amountXmr, true));
|
||||||
hideExchanging();
|
hideExchanging();
|
||||||
if (!"XMR".equals(currencyA)) {
|
}
|
||||||
|
|
||||||
|
public void exchange(final ExchangeRate exchangeRate) {
|
||||||
|
hideExchanging();
|
||||||
|
if (!"XMR".equals(exchangeRate.getBaseCurrency())) {
|
||||||
Log.e(TAG, "Not XMR");
|
Log.e(TAG, "Not XMR");
|
||||||
sCurrency.setSelection(0, true);
|
sCurrency.setSelection(0, true);
|
||||||
balanceCurrency = "XMR";
|
balanceCurrency = "XMR";
|
||||||
balanceRate = 1.0;
|
balanceRate = 1.0;
|
||||||
} else {
|
} else {
|
||||||
int spinnerPosition = ((ArrayAdapter) sCurrency.getAdapter()).getPosition(currencyB);
|
int spinnerPosition = ((ArrayAdapter) sCurrency.getAdapter()).getPosition(exchangeRate.getQuoteCurrency());
|
||||||
if (spinnerPosition < 0) { // requested currency not in list
|
if (spinnerPosition < 0) { // requested currency not in list
|
||||||
Log.e(TAG, "Requested currency not in list " + currencyB);
|
Log.e(TAG, "Requested currency not in list " + exchangeRate.getQuoteCurrency());
|
||||||
sCurrency.setSelection(0, true);
|
sCurrency.setSelection(0, true);
|
||||||
} else {
|
} else {
|
||||||
sCurrency.setSelection(spinnerPosition, true);
|
sCurrency.setSelection(spinnerPosition, true);
|
||||||
}
|
}
|
||||||
balanceCurrency = currencyB;
|
balanceCurrency = exchangeRate.getQuoteCurrency();
|
||||||
balanceRate = rate;
|
balanceRate = exchangeRate.getRate();
|
||||||
}
|
}
|
||||||
updateBalance();
|
updateBalance();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,159 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2017 m2049r
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.layout;
|
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
public class AsyncExchangeRate extends AsyncTask<String, Void, Boolean> {
|
|
||||||
static final String TAG = "AsyncExchangeRate";
|
|
||||||
static final long TIME_REFRESH_INTERVAL = 60000; // refresh exchange rate max every minute
|
|
||||||
|
|
||||||
public interface Listener {
|
|
||||||
void exchangeFailed();
|
|
||||||
|
|
||||||
// callback from AsyncExchangeRate when we have a rate
|
|
||||||
void exchange(String currencyA, String currencyB, double rate);
|
|
||||||
}
|
|
||||||
|
|
||||||
static long RateTime = 0;
|
|
||||||
static double Rate = 0;
|
|
||||||
static String Fiat = null;
|
|
||||||
|
|
||||||
private final WeakReference<Listener> exchangeViewRef;
|
|
||||||
|
|
||||||
public AsyncExchangeRate(Listener exchangeView) {
|
|
||||||
super();
|
|
||||||
exchangeViewRef = new WeakReference<>(exchangeView);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
super.onPreExecute();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean inverse = false;
|
|
||||||
String currencyA = null;
|
|
||||||
String currencyB = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(String... params) {
|
|
||||||
if (params.length != 2) return false;
|
|
||||||
Log.d(TAG, "Getting " + params[0]);
|
|
||||||
currencyA = params[0];
|
|
||||||
currencyB = params[1];
|
|
||||||
|
|
||||||
String fiat = null;
|
|
||||||
if (currencyA.equals("XMR")) {
|
|
||||||
fiat = currencyB;
|
|
||||||
inverse = false;
|
|
||||||
}
|
|
||||||
if (currencyB.equals("XMR")) {
|
|
||||||
fiat = currencyA;
|
|
||||||
inverse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currencyA.equals(currencyB)) {
|
|
||||||
Fiat = null;
|
|
||||||
Rate = 1;
|
|
||||||
RateTime = System.currentTimeMillis();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fiat == null) {
|
|
||||||
Fiat = null;
|
|
||||||
Rate = 0;
|
|
||||||
RateTime = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fiat.equals(Fiat)) { // new currency - reset all
|
|
||||||
Fiat = fiat;
|
|
||||||
Rate = 0;
|
|
||||||
RateTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (System.currentTimeMillis() > RateTime + TIME_REFRESH_INTERVAL) {
|
|
||||||
Log.d(TAG, "Fetching " + Fiat);
|
|
||||||
String closePrice = getExchangeRate(Fiat);
|
|
||||||
if (closePrice != null) {
|
|
||||||
try {
|
|
||||||
Rate = Double.parseDouble(closePrice);
|
|
||||||
RateTime = System.currentTimeMillis();
|
|
||||||
return true;
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Rate = 0;
|
|
||||||
Log.e(TAG, ex.getLocalizedMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Rate = 0;
|
|
||||||
Log.e(TAG, "exchange url failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true; // no change but still valid
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean result) {
|
|
||||||
super.onPostExecute(result);
|
|
||||||
Listener exchangeView = exchangeViewRef.get();
|
|
||||||
if (result) {
|
|
||||||
Log.d(TAG, "yay! = " + Rate);
|
|
||||||
if (exchangeView != null) {
|
|
||||||
exchangeView.exchange(currencyA, currencyB, inverse ? (1 / Rate) : Rate);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "nay!");
|
|
||||||
if (exchangeView != null) {
|
|
||||||
exchangeView.exchangeFailed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// "https://api.kraken.com/0/public/Ticker?pair=XMREUR"
|
|
||||||
String getExchangeRate(String fiat) {
|
|
||||||
String jsonResponse =
|
|
||||||
Helper.getUrl("https://api.kraken.com/0/public/Ticker?pair=XMR" + fiat);
|
|
||||||
if (jsonResponse == null) return null;
|
|
||||||
try {
|
|
||||||
JSONObject response = new JSONObject(jsonResponse);
|
|
||||||
JSONArray errors = response.getJSONArray("error");
|
|
||||||
Log.e(TAG, "errors=" + errors.toString());
|
|
||||||
if (errors.length() == 0) {
|
|
||||||
JSONObject result = response.getJSONObject("result");
|
|
||||||
JSONObject pair = result.getJSONObject("XXMRZ" + fiat);
|
|
||||||
JSONArray close = pair.getJSONArray("c");
|
|
||||||
String closePrice = close.getString(0);
|
|
||||||
Log.d(TAG, "closePrice=" + closePrice);
|
|
||||||
return closePrice;
|
|
||||||
}
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
Log.e(TAG, ex.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,6 +19,8 @@
|
||||||
package com.m2049r.xmrwallet.layout;
|
package com.m2049r.xmrwallet.layout;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.support.design.widget.TextInputLayout;
|
import android.support.design.widget.TextInputLayout;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
@ -37,11 +39,17 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import com.m2049r.xmrwallet.R;
|
import com.m2049r.xmrwallet.R;
|
||||||
import com.m2049r.xmrwallet.model.Wallet;
|
import com.m2049r.xmrwallet.model.Wallet;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.kraken.ExchangeApiImpl;
|
||||||
import com.m2049r.xmrwallet.util.Helper;
|
import com.m2049r.xmrwallet.util.Helper;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public class ExchangeView extends LinearLayout implements AsyncExchangeRate.Listener {
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
public class ExchangeView extends LinearLayout {
|
||||||
static final String TAG = "ExchangeView";
|
static final String TAG = "ExchangeView";
|
||||||
|
|
||||||
public boolean focus() {
|
public boolean focus() {
|
||||||
|
@ -277,11 +285,36 @@ public class ExchangeView extends LinearLayout implements AsyncExchangeRate.List
|
||||||
startExchange();
|
startExchange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final ExchangeApi exchangeApi = new ExchangeApiImpl(Helper.getOkHttpClient());
|
||||||
|
|
||||||
void startExchange() {
|
void startExchange() {
|
||||||
showProgress();
|
showProgress();
|
||||||
String currencyA = (String) sCurrencyA.getSelectedItem();
|
String currencyA = (String) sCurrencyA.getSelectedItem();
|
||||||
String currencyB = (String) sCurrencyB.getSelectedItem();
|
String currencyB = (String) sCurrencyB.getSelectedItem();
|
||||||
new AsyncExchangeRate(this).execute(currencyA, currencyB);
|
exchangeApi.queryExchangeRate(currencyA, currencyB,
|
||||||
|
new ExchangeCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||||
|
if (isAttachedToWindow())
|
||||||
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
exchange(exchangeRate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(final Exception e) {
|
||||||
|
Log.e(TAG, e.getLocalizedMessage());
|
||||||
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
exchangeFailed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void exchange(double rate) {
|
public void exchange(double rate) {
|
||||||
|
@ -349,7 +382,6 @@ public class ExchangeView extends LinearLayout implements AsyncExchangeRate.List
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback from AsyncExchangeRate when it failed getting a rate
|
|
||||||
public void exchangeFailed() {
|
public void exchangeFailed() {
|
||||||
hideProgress();
|
hideProgress();
|
||||||
exchange(0);
|
exchange(0);
|
||||||
|
@ -358,19 +390,19 @@ public class ExchangeView extends LinearLayout implements AsyncExchangeRate.List
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback from AsyncExchangeRate when we have a rate
|
public void exchange(ExchangeRate exchangeRate) {
|
||||||
public void exchange(String currencyA, String currencyB, double rate) {
|
|
||||||
hideProgress();
|
hideProgress();
|
||||||
// first, make sure this is what we want
|
// first, make sure this is what we want
|
||||||
String enteredCurrencyA = (String) sCurrencyA.getSelectedItem();
|
String enteredCurrencyA = (String) sCurrencyA.getSelectedItem();
|
||||||
String enteredCurrencyB = (String) sCurrencyB.getSelectedItem();
|
String enteredCurrencyB = (String) sCurrencyB.getSelectedItem();
|
||||||
if (!currencyA.equals(enteredCurrencyA) || !currencyB.equals(enteredCurrencyB)) {
|
if (!exchangeRate.getBaseCurrency().equals(enteredCurrencyA)
|
||||||
|
|| !exchangeRate.getQuoteCurrency().equals(enteredCurrencyB)) {
|
||||||
// something's wrong
|
// something's wrong
|
||||||
Log.e(TAG, "Currencies don't match!");
|
Log.e(TAG, "Currencies don't match!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (prepareExchange()) {
|
if (prepareExchange()) {
|
||||||
exchange(rate);
|
exchange(exchangeRate.getRate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2006 The Android Open Source Project
|
||||||
|
* Copyright (c) 2017 m2049r
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.m2049r.xmrwallet.service.exchange.api;
|
||||||
|
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
|
||||||
|
public interface ExchangeApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the exchnage rate
|
||||||
|
*
|
||||||
|
* @param baseCurrency base currency
|
||||||
|
* @param quoteCurrency quote currency
|
||||||
|
* @param callback the callback with the exchange rate
|
||||||
|
*/
|
||||||
|
void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency,
|
||||||
|
@NonNull final ExchangeCallback callback);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2006 The Android Open Source Project
|
||||||
|
* Copyright (c) 2017 m2049r
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.m2049r.xmrwallet.service.exchange.api;
|
||||||
|
|
||||||
|
public interface ExchangeCallback {
|
||||||
|
|
||||||
|
void onSuccess(ExchangeRate exchangeRate);
|
||||||
|
|
||||||
|
void onError(Exception ex);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017 m2049r
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.m2049r.xmrwallet.service.exchange.api;
|
||||||
|
|
||||||
|
public class ExchangeException extends Exception {
|
||||||
|
private int code;
|
||||||
|
private final String errorMsg;
|
||||||
|
|
||||||
|
public String getErrorMsg() {
|
||||||
|
return errorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExchangeException(final int code) {
|
||||||
|
super();
|
||||||
|
this.code = code;
|
||||||
|
this.errorMsg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExchangeException(final String errorMsg) {
|
||||||
|
super();
|
||||||
|
this.code = 0;
|
||||||
|
this.errorMsg = errorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExchangeException(final int code, final String errorMsg) {
|
||||||
|
super();
|
||||||
|
this.code = code;
|
||||||
|
this.errorMsg = errorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017 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.exchange.api;
|
||||||
|
|
||||||
|
public interface ExchangeRate {
|
||||||
|
|
||||||
|
String getServiceName();
|
||||||
|
|
||||||
|
String getBaseCurrency();
|
||||||
|
|
||||||
|
String getQuoteCurrency();
|
||||||
|
|
||||||
|
double getRate();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017 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.exchange.kraken;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
interface ExchangeApiCall {
|
||||||
|
|
||||||
|
void call(@NonNull final String fiat, @NonNull final NetworkCallback callback);
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017 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.exchange.kraken;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeException;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class ExchangeApiImpl implements ExchangeApi, ExchangeApiCall {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final OkHttpClient okHttpClient;
|
||||||
|
|
||||||
|
private final HttpUrl baseUrl;
|
||||||
|
|
||||||
|
//so we can inject the mockserver url
|
||||||
|
@VisibleForTesting
|
||||||
|
ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient, final HttpUrl baseUrl) {
|
||||||
|
|
||||||
|
this.okHttpClient = okHttpClient;
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient) {
|
||||||
|
this(okHttpClient, HttpUrl.parse("https://api.kraken.com/0/public/Ticker"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency,
|
||||||
|
@NonNull final ExchangeCallback callback) {
|
||||||
|
ExchangeRateImpl.call(this, baseCurrency, quoteCurrency, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void call(@NonNull final String fiat, @NonNull final NetworkCallback callback) {
|
||||||
|
|
||||||
|
final HttpUrl url = baseUrl.newBuilder()
|
||||||
|
.addQueryParameter("pair", "XMR" + fiat)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final Request httpRequest = createHttpRequest(url);
|
||||||
|
|
||||||
|
okHttpClient.newCall(httpRequest).enqueue(new okhttp3.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(final Call call, final IOException ex) {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(final Call call, final Response response) throws IOException {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
try {
|
||||||
|
final JSONObject json = new JSONObject(response.body().string());
|
||||||
|
final JSONArray jsonError = json.getJSONArray("error");
|
||||||
|
if (jsonError.length() > 0) {
|
||||||
|
final String errorMsg = jsonError.getString(0);
|
||||||
|
callback.onError(new ExchangeException(response.code(), errorMsg));
|
||||||
|
} else {
|
||||||
|
final JSONObject jsonResult = json.getJSONObject("result");
|
||||||
|
callback.onSuccess(jsonResult);
|
||||||
|
}
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback.onError(new ExchangeException(response.code(), response.message()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Request createHttpRequest(final HttpUrl url) {
|
||||||
|
return new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.get()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017 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.exchange.kraken;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeException;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
class ExchangeRateImpl implements ExchangeRate {
|
||||||
|
|
||||||
|
private final String baseCurrency;
|
||||||
|
private final String quoteCurrency;
|
||||||
|
private final double rate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getServiceName() {
|
||||||
|
return "kraken.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseCurrency() {
|
||||||
|
return baseCurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getQuoteCurrency() {
|
||||||
|
return quoteCurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getRate() {
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExchangeRateImpl(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, double rate) {
|
||||||
|
super();
|
||||||
|
this.baseCurrency = baseCurrency;
|
||||||
|
this.quoteCurrency = quoteCurrency;
|
||||||
|
this.rate = rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExchangeRateImpl(final JSONObject jsonObject) throws JSONException, ExchangeException {
|
||||||
|
try {
|
||||||
|
final String key = jsonObject.keys().next(); // we expect only one
|
||||||
|
Pattern pattern = Pattern.compile("^X(.*?)Z(.*?)$");
|
||||||
|
Matcher matcher = pattern.matcher(key);
|
||||||
|
if (matcher.find()) {
|
||||||
|
this.baseCurrency = matcher.group(1);
|
||||||
|
this.quoteCurrency = matcher.group(2);
|
||||||
|
} else {
|
||||||
|
throw new ExchangeException("no pair returned!");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject pair = jsonObject.getJSONObject(key);
|
||||||
|
JSONArray close = pair.getJSONArray("c");
|
||||||
|
String closePrice = close.getString(0);
|
||||||
|
if (closePrice != null) {
|
||||||
|
try {
|
||||||
|
this.rate = Double.parseDouble(closePrice);
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
throw new ExchangeException(ex.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ExchangeException("no close price returned!");
|
||||||
|
}
|
||||||
|
} catch (NoSuchElementException ex) {
|
||||||
|
throw new ExchangeException(ex.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void call(@NonNull final ExchangeApiCall api,
|
||||||
|
@NonNull final String baseCurrency, @NonNull final String quoteCurrency,
|
||||||
|
@NonNull final ExchangeCallback callback) {
|
||||||
|
|
||||||
|
if (baseCurrency.equals(quoteCurrency)) {
|
||||||
|
callback.onSuccess(new ExchangeRateImpl(baseCurrency, quoteCurrency, 1.0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean inverse = false;
|
||||||
|
String fiat = null;
|
||||||
|
|
||||||
|
if (baseCurrency.equals("XMR")) {
|
||||||
|
fiat = quoteCurrency;
|
||||||
|
inverse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quoteCurrency.equals("XMR")) {
|
||||||
|
fiat = baseCurrency;
|
||||||
|
inverse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fiat == null) {
|
||||||
|
callback.onError(new IllegalArgumentException("no fiat specified"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.call(fiat, new NetworkCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(JSONObject jsonObject) {
|
||||||
|
try {
|
||||||
|
final ExchangeRate exchangeRate = new ExchangeRateImpl(jsonObject);
|
||||||
|
callback.onSuccess(exchangeRate);
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
||||||
|
} catch (ExchangeException ex) {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception ex) {
|
||||||
|
callback.onError(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017 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.exchange.kraken;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
interface NetworkCallback {
|
||||||
|
|
||||||
|
void onSuccess(JSONObject jsonObject);
|
||||||
|
|
||||||
|
void onError(Exception ex);
|
||||||
|
|
||||||
|
}
|
|
@ -1,151 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2017 m2049r
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.m2049r.xmrwallet.util;
|
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public class AsyncExchangeRate extends AsyncTask<String, Void, Boolean> {
|
|
||||||
private static final String TAG = "AsyncGetExchangeRate";
|
|
||||||
|
|
||||||
private static final long TIME_REFRESH_INTERVAL = 60000; // refresh exchange rate max every minute
|
|
||||||
|
|
||||||
private static long RateTime = 0;
|
|
||||||
private static double Rate = 0;
|
|
||||||
private static String Fiat = null;
|
|
||||||
|
|
||||||
public interface Listener {
|
|
||||||
void exchange(String currencyA, String currencyB, double rate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Listener listener;
|
|
||||||
|
|
||||||
public AsyncExchangeRate(Listener listener) {
|
|
||||||
super();
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
super.onPreExecute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean inverse = false;
|
|
||||||
private String currencyA = null;
|
|
||||||
private String currencyB = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(String... params) {
|
|
||||||
if (params.length != 2) return false;
|
|
||||||
Log.d(TAG, "Getting " + params[0]);
|
|
||||||
currencyA = params[0];
|
|
||||||
currencyB = params[1];
|
|
||||||
|
|
||||||
String fiat = null;
|
|
||||||
if (currencyA.equals("XMR")) {
|
|
||||||
fiat = currencyB;
|
|
||||||
inverse = false;
|
|
||||||
}
|
|
||||||
if (currencyB.equals("XMR")) {
|
|
||||||
fiat = currencyA;
|
|
||||||
inverse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currencyA.equals(currencyB)) {
|
|
||||||
Fiat = null;
|
|
||||||
Rate = 1;
|
|
||||||
RateTime = System.currentTimeMillis();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fiat == null) {
|
|
||||||
Fiat = null;
|
|
||||||
Rate = 0;
|
|
||||||
RateTime = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fiat.equals(Fiat)) { // new currency - reset all
|
|
||||||
Fiat = fiat;
|
|
||||||
Rate = 0;
|
|
||||||
RateTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (System.currentTimeMillis() > RateTime + TIME_REFRESH_INTERVAL) {
|
|
||||||
Log.d(TAG, "Fetching " + Fiat);
|
|
||||||
String closePrice = getExchangeRate(Fiat);
|
|
||||||
if (closePrice != null) {
|
|
||||||
try {
|
|
||||||
Rate = Double.parseDouble(closePrice);
|
|
||||||
RateTime = System.currentTimeMillis();
|
|
||||||
return true;
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Rate = 0;
|
|
||||||
Log.e(TAG, ex.getLocalizedMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Rate = 0;
|
|
||||||
Log.e(TAG, "exchange url failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true; // no change but still valid
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean result) {
|
|
||||||
super.onPostExecute(result);
|
|
||||||
if (result) {
|
|
||||||
Log.d(TAG, "yay! = " + Rate);
|
|
||||||
if (listener != null) {
|
|
||||||
listener.exchange(currencyA, currencyB, inverse ? (1 / Rate) : Rate);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "nay!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getExchangeRate(String fiat) {
|
|
||||||
String jsonResponse =
|
|
||||||
Helper.getUrl("https://api.kraken.com/0/public/Ticker?pair=XMR" + fiat);
|
|
||||||
if (jsonResponse == null) return null;
|
|
||||||
try {
|
|
||||||
JSONObject response = new JSONObject(jsonResponse);
|
|
||||||
JSONArray errors = response.getJSONArray("error");
|
|
||||||
Log.e(TAG, "errors=" + errors.toString());
|
|
||||||
if (errors.length() == 0) {
|
|
||||||
JSONObject result = response.getJSONObject("result");
|
|
||||||
JSONObject pair = result.getJSONObject("XXMRZ" + fiat);
|
|
||||||
JSONArray close = pair.getJSONArray("c");
|
|
||||||
String closePrice = close.getString(0);
|
|
||||||
Log.d(TAG, "closePrice=" + closePrice);
|
|
||||||
return closePrice;
|
|
||||||
}
|
|
||||||
} catch (JSONException ex) {
|
|
||||||
Log.e(TAG, ex.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// "https://api.kraken.com/0/public/Ticker?pair=XMREUR"
|
|
||||||
}
|
|
|
@ -48,6 +48,8 @@ import java.util.Locale;
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
public class Helper {
|
public class Helper {
|
||||||
static private final String TAG = "Helper";
|
static private final String TAG = "Helper";
|
||||||
static private final String WALLET_DIR = "monerujo";
|
static private final String WALLET_DIR = "monerujo";
|
||||||
|
@ -240,4 +242,17 @@ public class Helper {
|
||||||
ClipData clip = ClipData.newPlainText(label, text);
|
ClipData clip = ClipData.newPlainText(label, text);
|
||||||
clipboardManager.setPrimaryClip(clip);
|
clipboardManager.setPrimaryClip(clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static private OkHttpClient OkHttpClientSingleton;
|
||||||
|
|
||||||
|
static public final OkHttpClient getOkHttpClient() {
|
||||||
|
if (OkHttpClientSingleton == null) {
|
||||||
|
synchronized (Helper.class) {
|
||||||
|
if (OkHttpClientSingleton == null) {
|
||||||
|
OkHttpClientSingleton = new OkHttpClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return OkHttpClientSingleton;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:entries="@array/currency"
|
android:entries="@array/currency"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:paddingBottom="0dp"
|
android:paddingBottom="2dp"
|
||||||
android:paddingEnd="4dp"
|
android:paddingEnd="4dp"
|
||||||
android:paddingStart="4dp"
|
android:paddingStart="4dp"
|
||||||
android:paddingTop="0dp"
|
android:paddingTop="0dp"
|
||||||
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017 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.exchange.kraken;
|
||||||
|
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeException;
|
||||||
|
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||||
|
|
||||||
|
import net.jodah.concurrentunit.Waiter;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.mockwebserver.MockResponse;
|
||||||
|
import okhttp3.mockwebserver.MockWebServer;
|
||||||
|
import okhttp3.mockwebserver.RecordedRequest;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
|
||||||
|
public class ExchangeRateTest {
|
||||||
|
|
||||||
|
private MockWebServer mockWebServer;
|
||||||
|
|
||||||
|
private ExchangeApi exchangeApi;
|
||||||
|
|
||||||
|
private final OkHttpClient okHttpClient = new OkHttpClient();
|
||||||
|
private Waiter waiter;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExchangeCallback mockExchangeCallback;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
mockWebServer = new MockWebServer();
|
||||||
|
mockWebServer.start();
|
||||||
|
|
||||||
|
waiter = new Waiter();
|
||||||
|
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
|
||||||
|
exchangeApi = new ExchangeApiImpl(okHttpClient, mockWebServer.url("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() throws Exception {
|
||||||
|
mockWebServer.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void queryExchangeRate_shouldBeGetMethod()
|
||||||
|
throws InterruptedException, TimeoutException {
|
||||||
|
|
||||||
|
exchangeApi.queryExchangeRate("XMR", "USD", mockExchangeCallback);
|
||||||
|
|
||||||
|
RecordedRequest request = mockWebServer.takeRequest();
|
||||||
|
assertEquals("GET", request.getMethod());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void queryExchangeRate_shouldHavePairInUrl()
|
||||||
|
throws InterruptedException, TimeoutException {
|
||||||
|
|
||||||
|
exchangeApi.queryExchangeRate("XMR", "USD", mockExchangeCallback);
|
||||||
|
|
||||||
|
RecordedRequest request = mockWebServer.takeRequest();
|
||||||
|
assertEquals("/?pair=XMRUSD", request.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void queryExchangeRate_wasSuccessfulShouldRespondWithRate()
|
||||||
|
throws InterruptedException, JSONException, TimeoutException {
|
||||||
|
final String base = "XMR";
|
||||||
|
final String quote = "USD";
|
||||||
|
final double rate = 100;
|
||||||
|
MockResponse jsonMockResponse = new MockResponse().setBody(
|
||||||
|
createMockExchangeRateResponse(base, quote, rate));
|
||||||
|
mockWebServer.enqueue(jsonMockResponse);
|
||||||
|
|
||||||
|
exchangeApi.queryExchangeRate(base, quote, new ExchangeCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||||
|
waiter.assertEquals(exchangeRate.getBaseCurrency(), base);
|
||||||
|
waiter.assertEquals(exchangeRate.getQuoteCurrency(), quote);
|
||||||
|
waiter.assertEquals(exchangeRate.getRate(), rate);
|
||||||
|
waiter.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(final Exception e) {
|
||||||
|
waiter.fail(e);
|
||||||
|
waiter.resume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
waiter.await();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void queryExchangeRate_wasNotSuccessfulShouldCallOnError()
|
||||||
|
throws InterruptedException, JSONException, TimeoutException {
|
||||||
|
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
|
||||||
|
exchangeApi.queryExchangeRate("XMR", "USD", new ExchangeCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||||
|
waiter.fail();
|
||||||
|
waiter.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(final Exception e) {
|
||||||
|
waiter.assertTrue(e instanceof ExchangeException);
|
||||||
|
waiter.assertTrue(((ExchangeException) e).getCode() == 500);
|
||||||
|
waiter.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
waiter.await();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void queryExchangeRate_unknownAssetShouldCallOnError()
|
||||||
|
throws InterruptedException, JSONException, TimeoutException {
|
||||||
|
mockWebServer.enqueue(new MockResponse().
|
||||||
|
setResponseCode(200).
|
||||||
|
setBody("{\"error\":[\"EQuery:Unknown asset pair\"]}"));
|
||||||
|
exchangeApi.queryExchangeRate("XMR", "ABC", new ExchangeCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||||
|
waiter.fail();
|
||||||
|
waiter.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(final Exception e) {
|
||||||
|
waiter.assertTrue(e instanceof ExchangeException);
|
||||||
|
ExchangeException ex = (ExchangeException) e;
|
||||||
|
waiter.assertTrue(ex.getCode() == 200);
|
||||||
|
waiter.assertEquals(ex.getErrorMsg(), "EQuery:Unknown asset pair");
|
||||||
|
waiter.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
waiter.await();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createMockExchangeRateResponse(final String base, final String quote, final double rate) {
|
||||||
|
return "{\n" +
|
||||||
|
" \"error\":[],\n" +
|
||||||
|
" \"result\":{\n" +
|
||||||
|
" \"X" + base + "Z" + quote + "\":{\n" +
|
||||||
|
" \"a\":[\"" + rate + "\",\"322\",\"322.000\"],\n" +
|
||||||
|
" \"b\":[\"" + rate + "\",\"76\",\"76.000\"],\n" +
|
||||||
|
" \"c\":[\"" + rate + "\",\"2.90000000\"],\n" +
|
||||||
|
" \"v\":[\"4559.03962053\",\"5231.33235586\"],\n" +
|
||||||
|
" \"p\":[\"" + rate + "\",\"" + rate + "\"],\n" +
|
||||||
|
" \"t\":[801,1014],\n" +
|
||||||
|
" \"l\":[\"" + (rate * 0.8) + "\",\"" + rate + "\"],\n" +
|
||||||
|
" \"h\":[\"" + (rate * 1.2) + "\",\"" + rate + "\"],\n" +
|
||||||
|
" \"o\":\"" + rate + "\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.0.0'
|
classpath 'com.android.tools.build:gradle:3.0.0'
|
||||||
|
@ -24,3 +25,9 @@ allprojects {
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
okHttpVersion = '3.9.0'
|
||||||
|
junitVersion = '4.12'
|
||||||
|
mockitoVersion = '1.10.19'
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue