From 5c1fa097a9c7566750529f00ae8fb2f86227f29d Mon Sep 17 00:00:00 2001 From: m2049r <30435443+m2049r@users.noreply.github.com> Date: Sat, 9 Sep 2017 11:39:39 +0200 Subject: [PATCH] show alternative currency (USD/EUR) on receive uses https://www.kraken.com/help/api#get-ticker-info --- .../com/m2049r/xmrwallet/LoginActivity.java | 11 +- .../com/m2049r/xmrwallet/ReceiveFragment.java | 180 ++++++++++++++++-- .../com/m2049r/xmrwallet/WalletActivity.java | 13 +- .../xmrwallet/util/AsyncExchangeRate.java | 151 +++++++++++++++ .../com/m2049r/xmrwallet/util/Helper.java | 29 ++- app/src/main/res/layout/receive_fragment.xml | 57 +++--- app/src/main/res/values/strings.xml | 14 +- app/src/main/res/values/styles.xml | 12 +- 8 files changed, 414 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/AsyncExchangeRate.java diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index 463c0431..5996953b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -45,6 +45,7 @@ import android.widget.Toast; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.service.WalletService; +import com.m2049r.xmrwallet.util.AsyncExchangeRate; import com.m2049r.xmrwallet.util.Helper; import java.io.File; @@ -54,7 +55,8 @@ import java.io.IOException; import java.nio.channels.FileChannel; public class LoginActivity extends AppCompatActivity - implements LoginFragment.Listener, GenerateFragment.Listener, GenerateReviewFragment.Listener { + implements LoginFragment.Listener, GenerateFragment.Listener, + GenerateReviewFragment.Listener, ReceiveFragment.Listener { static final String TAG = "LoginActivity"; private static final String GENERATE_STACK = "gen"; @@ -700,6 +702,7 @@ public class LoginActivity extends AppCompatActivity interface WalletCreator { boolean createWallet(File aFile, String password); + } @Override @@ -778,6 +781,12 @@ public class LoginActivity extends AppCompatActivity } } + @Override + public void onExchange(AsyncExchangeRate.Listener listener, String currencyA, String currencyB) { + new AsyncExchangeRate(listener).execute(currencyA, currencyB); + } + + Wallet.Status testWallet(String path, String password) { Log.d(TAG, "testing wallet " + path); Wallet aWallet = WalletManager.getInstance().openWallet(path, password); diff --git a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java index efceac14..40459d00 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java @@ -16,10 +16,13 @@ package com.m2049r.xmrwallet; +import android.content.Context; +import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Canvas; import android.os.AsyncTask; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.text.Editable; import android.text.InputType; @@ -30,10 +33,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; +import android.widget.AdapterView; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.ProgressBar; +import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -45,24 +50,73 @@ import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.AsyncExchangeRate; import com.m2049r.xmrwallet.util.Helper; import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; import java.util.HashMap; +import java.util.Locale; import java.util.Map; -public class ReceiveFragment extends Fragment { +public class ReceiveFragment extends Fragment implements AsyncExchangeRate.Listener { static final String TAG = "ReceiveFragment"; ProgressBar pbProgress; TextView tvAddress; EditText etPaymentId; EditText etAmount; + TextView tvAmountB; Button bPaymentId; Button bGenerate; ImageView qrCode; EditText etDummy; + Spinner sCurrencyA; + Spinner sCurrencyB; + + public interface Listener { + void onExchange(AsyncExchangeRate.Listener listener, String currencyA, String currencyB); + } + + @Override + public void exchange(String currencyA, String currencyB, double rate) { + // first, make sure this is what we want + String enteredCurrencyA = (String) sCurrencyA.getSelectedItem(); + String enteredCurrencyB = (String) sCurrencyB.getSelectedItem(); + if (!currencyA.equals(enteredCurrencyA) || !currencyB.equals(enteredCurrencyB)) { + // something's wrong + Log.e(TAG, "Currencies don't match!"); + tvAmountB.setText(""); + return; + } + String enteredAmount = etAmount.getText().toString(); + String xmrAmount = ""; + if (!enteredAmount.isEmpty()) { + // losing precision using double here doesn't matter + double amountA = Double.parseDouble(enteredAmount); + double amountB = amountA * rate; + Log.d(TAG, "exchange A=" + amountA + " B=" + amountB); + if (enteredCurrencyA.equals("XMR")) { + String validatedAmountA = Helper.getDisplayAmount(Wallet.getAmountFromString(enteredAmount)); + xmrAmount = validatedAmountA; // take what was entered in XMR + etAmount.setText(xmrAmount); // display what we stick into the QR code + String displayB = String.format(Locale.US, "%.2f", amountB); + tvAmountB.setText(displayB); + } else if (enteredCurrencyB.equals("XMR")) { + xmrAmount = Wallet.getDisplayAmount(Wallet.getAmountFromDouble(amountB)); + // cut off at 5 decimals + xmrAmount = xmrAmount.substring(0, xmrAmount.length() - (12 - 5)); + tvAmountB.setText(xmrAmount); + } else { // no XMR currency + tvAmountB.setText(""); + return; // and no qr code + } + } else { + tvAmountB.setText(""); + } + generateQr(xmrAmount); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -72,15 +126,21 @@ public class ReceiveFragment extends Fragment { pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress); tvAddress = (TextView) view.findViewById(R.id.tvAddress); etPaymentId = (EditText) view.findViewById(R.id.etPaymentId); - etAmount = (EditText) view.findViewById(R.id.etAmount); + etAmount = (EditText) view.findViewById(R.id.etAmountA); + tvAmountB = (TextView) view.findViewById(R.id.tvAmountB); bPaymentId = (Button) view.findViewById(R.id.bPaymentId); qrCode = (ImageView) view.findViewById(R.id.qrCode); bGenerate = (Button) view.findViewById(R.id.bGenerate); etDummy = (EditText) view.findViewById(R.id.etDummy); + sCurrencyA = (Spinner) view.findViewById(R.id.sCurrencyA); + sCurrencyB = (Spinner) view.findViewById(R.id.sCurrencyB); + etPaymentId.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + loadPrefs(); + etPaymentId.setOnEditorActionListener(new TextView.OnEditorActionListener() { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_NEXT)) { @@ -117,7 +177,7 @@ public class ReceiveFragment extends Fragment { if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { if (paymentIdOk() && amountOk()) { Helper.hideKeyboard(getActivity()); - generateQr(); + startExchange(); } return true; } @@ -127,6 +187,7 @@ public class ReceiveFragment extends Fragment { etAmount.addTextChangedListener(new TextWatcher() { @Override public void afterTextChanged(Editable editable) { + tvAmountB.setText(""); qrCode.setImageBitmap(getMoneroLogo()); if (paymentIdOk() && amountOk()) { bGenerate.setEnabled(true); @@ -150,7 +211,7 @@ public class ReceiveFragment extends Fragment { etPaymentId.setText((Wallet.generatePaymentId())); etPaymentId.setSelection(etPaymentId.getText().length()); if (paymentIdOk() && amountOk()) { - generateQr(); + startExchange(); } } }); @@ -160,11 +221,42 @@ public class ReceiveFragment extends Fragment { public void onClick(View v) { if (paymentIdOk() && amountOk()) { Helper.hideKeyboard(getActivity()); - generateQr(); + startExchange(); } } }); + sCurrencyA.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parentView, View selectedItemView, int position, long id) { + if (position != 0) { + sCurrencyB.setSelection(0, true); + } + startExchange(); + } + + @Override + public void onNothingSelected(AdapterView parentView) { + // nothing (yet?) + } + }); + + sCurrencyB.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parentView, View selectedItemView, int position, long id) { + if (position != 0) { + sCurrencyA.setSelection(0, true); + } + tvAmountB.setText(""); + startExchange(); + } + + @Override + public void onNothingSelected(AdapterView parentView) { + // nothing (yet?) + } + }); + showProgress(); qrCode.setImageBitmap(getMoneroLogo()); @@ -180,12 +272,26 @@ public class ReceiveFragment extends Fragment { return view; } + void startExchange() { + if (paymentIdOk() && amountOk() && tvAddress.getText().length() > 0) { + String enteredCurrencyA = (String) sCurrencyA.getSelectedItem(); + String enteredCurrencyB = (String) sCurrencyB.getSelectedItem(); + String enteredAmount = etAmount.getText().toString(); + tvAmountB.setText(""); + if (!enteredAmount.isEmpty()) { // start conversion + listenerCallback.onExchange(ReceiveFragment.this, enteredCurrencyA, enteredCurrencyB); + } else { + generateQr(""); + } + } + } + @Override public void onResume() { super.onResume(); Log.d(TAG, "onResume()"); if (paymentIdOk() && amountOk() && tvAddress.getText().length() > 0) { - generateQr(); + startExchange(); } } @@ -196,7 +302,7 @@ public class ReceiveFragment extends Fragment { bPaymentId.setEnabled(true); bGenerate.setEnabled(true); hideProgress(); - generateQr(); + startExchange(); } private void show(String walletPath, String password) { @@ -245,12 +351,13 @@ public class ReceiveFragment extends Fragment { return paymentId.isEmpty() || Wallet.isPaymentIdValid(paymentId); } - private void generateQr() { + private void generateQr(String xmrAmount) { + Log.d(TAG, "AMOUNT=" + xmrAmount); String address = tvAddress.getText().toString(); String paymentId = etPaymentId.getText().toString(); - String enteredAmount = etAmount.getText().toString(); +// String enteredAmount = etAmount.getText().toString(); // that's a lot of converting ... - String amount = (enteredAmount.isEmpty() ? enteredAmount : Helper.getDisplayAmount(Wallet.getAmountFromString(enteredAmount))); + //String amount = (xmrAmount.isEmpty() ? xmrAmount : Helper.getDisplayAmount(Wallet.getAmountFromString(xmrAmount))); StringBuffer sb = new StringBuffer(); sb.append(ScannerFragment.QR_SCHEME).append(address); boolean first = true; @@ -261,18 +368,17 @@ public class ReceiveFragment extends Fragment { } sb.append(ScannerFragment.QR_PAYMENTID).append('=').append(paymentId); } - if (!amount.isEmpty()) { + if (!xmrAmount.isEmpty()) { if (first) { sb.append("?"); } else { sb.append("&"); } - sb.append(ScannerFragment.QR_AMOUNT).append('=').append(amount); + sb.append(ScannerFragment.QR_AMOUNT).append('=').append(xmrAmount); } String text = sb.toString(); Bitmap qr = generate(text, 500, 500); if (qr != null) { - etAmount.setText(amount); qrCode.setImageBitmap(qr); etDummy.requestFocus(); bGenerate.setEnabled(false); @@ -346,4 +452,52 @@ public class ReceiveFragment extends Fragment { pbProgress.setVisibility(View.GONE); } + Listener listenerCallback = null; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof Listener) { + this.listenerCallback = (Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + static final String PREF_CURRENCY_A = "PREF_CURRENCY_A"; + static final String PREF_CURRENCY_B = "PREF_CURRENCY_B"; + + void loadPrefs() { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); + int currencyA = sharedPreferences.getInt(PREF_CURRENCY_A, 0); + int currencyB = sharedPreferences.getInt(PREF_CURRENCY_B, 0); + + if (currencyA * currencyB != 0) { // make sure one of them is 0 (=XMR) + currencyA = 0; + } + // in case we change the currency lists in the future + if (currencyA >= sCurrencyA.getCount()) currencyA = 0; + if (currencyB >= sCurrencyB.getCount()) currencyB = 0; + sCurrencyA.setSelection(currencyA); + sCurrencyB.setSelection(currencyB); + } + + void savePrefs() { + int currencyA = sCurrencyA.getSelectedItemPosition(); + int currencyB = sCurrencyB.getSelectedItemPosition(); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt(PREF_CURRENCY_A, currencyA); + editor.putInt(PREF_CURRENCY_B, currencyB); + editor.apply(); + } + + @Override + public void onPause() { + Log.d(TAG, "onPause()"); + savePrefs(); + super.onPause(); + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 2ebed53c..7704d48d 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -17,6 +17,7 @@ package com.m2049r.xmrwallet; import android.app.AlertDialog; +import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -24,6 +25,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; @@ -42,17 +44,22 @@ import com.m2049r.xmrwallet.model.TransactionInfo; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.service.WalletService; +import com.m2049r.xmrwallet.util.AsyncExchangeRate; import com.m2049r.xmrwallet.util.BarcodeData; import com.m2049r.xmrwallet.util.Helper; import com.m2049r.xmrwallet.util.TxData; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; import java.util.HashMap; import java.util.Map; public class WalletActivity extends AppCompatActivity implements WalletFragment.Listener, WalletService.Observer, SendFragment.Listener, TxFragment.Listener, GenerateReviewFragment.ListenerWithWallet, - ScannerFragment.Listener { + ScannerFragment.Listener, ReceiveFragment.Listener { private static final String TAG = "WalletActivity"; public static final String REQUEST_ID = "id"; @@ -741,4 +748,8 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. Log.d(TAG, "ReceiveFragment placed"); } + @Override + public void onExchange(AsyncExchangeRate.Listener listener, String currencyA, String currencyB) { + new AsyncExchangeRate(listener).execute(currencyA, currencyB); + } } \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/AsyncExchangeRate.java b/app/src/main/java/com/m2049r/xmrwallet/util/AsyncExchangeRate.java new file mode 100644 index 00000000..0c724d1a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/AsyncExchangeRate.java @@ -0,0 +1,151 @@ +/* + * 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 { + static final String TAG = "AsyncGetExchangeRate"; + + static final long TIME_REFRESH_INTERVAL = 60000; // refresh exchange rate max every minute + + static protected long RateTime = 0; + static protected double Rate = 0; + static protected String Fiat = null; + + public interface Listener { + void exchange(String currencyA, String currencyB, double rate); + } + + Listener listener; + + public AsyncExchangeRate(Listener listener) { + super(); + this.listener = listener; + } + + + @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); + if (result) { + Log.d(TAG, "yay! = " + Rate); + if (listener != null) { + listener.exchange(currencyA, currencyB, inverse ? (1 / Rate) : Rate); + } + } else { + Log.d(TAG, "nay!"); + } + } + + 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" +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java index 23458cb6..9c876d88 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -35,13 +35,10 @@ import android.view.inputmethod.InputMethodManager; import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.model.Wallet; -import com.m2049r.xmrwallet.model.WalletManager; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; -import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; @@ -193,4 +190,30 @@ public class Helper { return bitmap; } + static public String getUrl(String httpsUrl) { + HttpsURLConnection urlConnection = null; + try { + URL url = new URL(httpsUrl); + urlConnection = (HttpsURLConnection) url.openConnection(); + InputStreamReader in = new InputStreamReader(urlConnection.getInputStream()); + StringBuffer sb = new StringBuffer(); + final int BUFFER_SIZE = 512; + char[] buffer = new char[BUFFER_SIZE]; + int length = in.read(buffer, 0, BUFFER_SIZE); + while (length >= 0) { + sb.append(buffer, 0, length); + length = in.read(buffer, 0, BUFFER_SIZE); + } + return sb.toString(); + } catch (MalformedURLException ex) { + Log.e(TAG, "A " + ex.getLocalizedMessage()); + } catch (IOException ex) { + Log.e(TAG, "B " + ex.getLocalizedMessage()); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + return null; + } } diff --git a/app/src/main/res/layout/receive_fragment.xml b/app/src/main/res/layout/receive_fragment.xml index 5e88ef32..8d110439 100644 --- a/app/src/main/res/layout/receive_fragment.xml +++ b/app/src/main/res/layout/receive_fragment.xml @@ -35,26 +35,17 @@ android:orientation="horizontal" android:weightSum="10"> - - + android:textAlignment="center" />