diff --git a/app/build.gradle b/app/build.gradle index b03d3d4..be9bf7e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "com.m2049r.xmrwallet" minSdkVersion 21 targetSdkVersion 25 - versionCode 51 - versionName "1.2.11" + versionCode 62 + versionName "1.3.2 'Satoshis Dream'" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { diff --git a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java index 09430e3..52d51c7 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java @@ -46,6 +46,7 @@ import com.google.zxing.WriterException; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.m2049r.xmrwallet.data.BarcodeData; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.util.Helper; @@ -304,14 +305,14 @@ public class ReceiveFragment extends Fragment { return; } StringBuffer sb = new StringBuffer(); - sb.append(ScannerFragment.QR_SCHEME).append(address); + sb.append(BarcodeData.XMR_SCHEME).append(address); boolean first = true; if (!paymentId.isEmpty()) { if (first) { sb.append("?"); first = false; } - sb.append(ScannerFragment.QR_PAYMENTID).append('=').append(paymentId); + sb.append(BarcodeData.XMR_PAYMENTID).append('=').append(paymentId); } if (!xmrAmount.isEmpty()) { if (first) { @@ -319,7 +320,7 @@ public class ReceiveFragment extends Fragment { } else { sb.append("&"); } - sb.append(ScannerFragment.QR_AMOUNT).append('=').append(xmrAmount); + sb.append(BarcodeData.XMR_AMOUNT).append('=').append(xmrAmount); } String text = sb.toString(); int size = Math.min(qrCode.getHeight(), qrCode.getWidth()); diff --git a/app/src/main/java/com/m2049r/xmrwallet/ScannerFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ScannerFragment.java index e9997cf..31de64e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/ScannerFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/ScannerFragment.java @@ -36,7 +36,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result private OnScannedListener onScannedListener; public interface OnScannedListener { - boolean onScanned(String uri); + boolean onScanned(String qrCode); } private ZXingScannerView mScannerView; @@ -56,10 +56,6 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result mScannerView.startCamera(); } - static final String QR_SCHEME = "monero:"; - static final String QR_PAYMENTID = "tx_payment_id"; - static final String QR_AMOUNT = "tx_amount"; - @Override public void handleResult(Result rawResult) { if ((rawResult.getBarcodeFormat() == BarcodeFormat.QR_CODE)) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java b/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java index eb4549b..dc9ac1a 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/SecureActivity.java @@ -20,6 +20,10 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; +import com.m2049r.xmrwallet.util.Helper; + +import java.io.File; + import static android.view.WindowManager.LayoutParams; public abstract class SecureActivity extends AppCompatActivity { @@ -27,8 +31,9 @@ public abstract class SecureActivity extends AppCompatActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + File f = new File(Helper.getStorageRoot(this), "screenshot"); // set FLAG_SECURE to prevent screenshots in Release Mode - if (!BuildConfig.DEBUG) { + if ((!BuildConfig.DEBUG) && (!f.exists())) { getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java index b530587..9b0dc19 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java @@ -30,10 +30,13 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; import com.m2049r.xmrwallet.model.TransactionInfo; import com.m2049r.xmrwallet.model.Transfer; import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.UserNotes; import com.m2049r.xmrwallet.widget.Toolbar; import java.text.SimpleDateFormat; @@ -68,12 +71,23 @@ public class TxFragment extends Fragment { private TextView etTxNotes; private Button bTxNotes; + // XMRTO stuff + private View cvXmrTo; + private TextView tvTxXmrToKey; + private TextView tvDestinationBtc; + private TextView tvTxAmountBtc; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_tx_info, container, false); + cvXmrTo = view.findViewById(R.id.cvXmrTo); + tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey); + tvDestinationBtc = (TextView) view.findViewById(R.id.tvDestinationBtc); + tvTxAmountBtc = (TextView) view.findViewById(R.id.tvTxAmountBtc); + tvTxTimestamp = (TextView) view.findViewById(R.id.tvTxTimestamp); tvTxId = (TextView) view.findViewById(R.id.tvTxId); tvTxKey = (TextView) view.findViewById(R.id.tvTxKey); @@ -94,7 +108,16 @@ public class TxFragment extends Fragment { info.notes = null; // force reload on next view bTxNotes.setEnabled(false); etTxNotes.setEnabled(false); - activityCallback.onSetNote(info.hash, etTxNotes.getText().toString()); + userNotes.setNote(etTxNotes.getText().toString()); + activityCallback.onSetNote(info.hash, userNotes.txNotes); + } + }); + + tvTxXmrToKey.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show(); } }); @@ -175,12 +198,14 @@ public class TxFragment extends Fragment { } TransactionInfo info = null; + UserNotes userNotes = null; void loadNotes(TransactionInfo info) { - if (info.notes == null) { + if ((userNotes == null) || (info.notes == null)) { info.notes = activityCallback.getTxNotes(info.hash); } - etTxNotes.setText(info.notes); + userNotes = new UserNotes(info.notes); + etTxNotes.setText(userNotes.note); } private void setTxColour(int clr) { @@ -266,8 +291,21 @@ public class TxFragment extends Fragment { tvTxTransfers.setText(sb.toString()); tvDestination.setText(dstSb.toString()); this.info = info; + showBtcInfo(); } + void showBtcInfo() { + if (userNotes.xmrtoKey != null) { + cvXmrTo.setVisibility(View.VISIBLE); + tvTxXmrToKey.setText(userNotes.xmrtoKey); + tvDestinationBtc.setText(userNotes.xmrtoDestination); + tvTxAmountBtc.setText(userNotes.xmrtoAmount + " BTC"); + } else { + cvXmrTo.setVisibility(View.GONE); + } + } + + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 5fa4e44..f4682bf 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -23,7 +23,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; @@ -38,17 +37,18 @@ import com.m2049r.xmrwallet.data.BarcodeData; import com.m2049r.xmrwallet.data.TxData; import com.m2049r.xmrwallet.dialog.DonationFragment; import com.m2049r.xmrwallet.dialog.HelpFragment; +import com.m2049r.xmrwallet.fragment.send.SendAddressWizardFragment; +import com.m2049r.xmrwallet.fragment.send.SendFragment; import com.m2049r.xmrwallet.model.PendingTransaction; 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.Helper; +import com.m2049r.xmrwallet.util.UserNotes; import com.m2049r.xmrwallet.widget.Toolbar; import java.io.File; -import java.util.HashMap; -import java.util.Map; import timber.log.Timber; @@ -465,7 +465,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis } @Override - public void onTransactionCreated(final PendingTransaction pendingTransaction) { + public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) { try { final SendFragment sendFragment = (SendFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container); @@ -477,7 +477,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis getWallet().disposePendingTransaction(); sendFragment.onCreateTransactionFailed(errorText); } else { - sendFragment.onTransactionCreated(pendingTransaction); + sendFragment.onTransactionCreated(txTag, pendingTransaction); } } }); @@ -588,11 +588,11 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis /////////////////////////// @Override - public void onSend(String notes) { + public void onSend(UserNotes notes) { if (mIsBound) { // no point in talking to unbound service Intent intent = new Intent(getApplicationContext(), WalletService.class); intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_SEND); - intent.putExtra(WalletService.REQUEST_CMD_SEND_NOTES, notes); + intent.putExtra(WalletService.REQUEST_CMD_SEND_NOTES, notes.txNotes); startService(intent); Timber.d("SEND TX request sent"); } else { @@ -617,11 +617,12 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis } @Override - public void onPrepareSend(TxData txData) { + public void onPrepareSend(final String tag, final TxData txData) { if (mIsBound) { // no point in talking to unbound service Intent intent = new Intent(getApplicationContext(), WalletService.class); intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_TX); intent.putExtra(WalletService.REQUEST_CMD_TX_DATA, txData); + intent.putExtra(WalletService.REQUEST_CMD_TX_TAG, tag); startService(intent); Timber.d("CREATE TX request sent"); } else { @@ -694,6 +695,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis @Override public void onDisposeRequest() { + //TODO consider doing this through the WalletService to avoid concurrency issues getWallet().disposePendingTransaction(); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java b/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java index 22ceca7..3ff9e04 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java +++ b/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java @@ -19,6 +19,8 @@ package com.m2049r.xmrwallet.data; import android.net.Uri; import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.BitcoinAddressValidator; import java.util.HashMap; import java.util.Map; @@ -34,7 +36,7 @@ public class BarcodeData { static final String BTC_AMOUNT = "amount"; public enum Asset { - XMR + XMR, BTC } public Asset asset = null; @@ -67,6 +69,14 @@ public class BarcodeData { if (bcData == null) { bcData = parseMoneroNaked(qrCode); } + // check for btc uri + if (bcData == null) { + bcData = parseBitcoinUri(qrCode); + } + // check for naked btc addres + if (bcData == null) { + bcData = parseBitcoinNaked(qrCode); + } return bcData; } @@ -134,4 +144,57 @@ public class BarcodeData { return new BarcodeData(Asset.XMR, address); } + + // bitcoin:mpQ84J43EURZHkCnXbyQ4PpNDLLBqdsMW2?amount=0.01 + static public BarcodeData parseBitcoinUri(String uri) { + Timber.d("parseBitcoinUri=%s", uri); + + if (uri == null) return null; + + if (!uri.startsWith(BTC_SCHEME)) return null; + + String noScheme = uri.substring(BTC_SCHEME.length()); + Uri bitcoin = Uri.parse(noScheme); + Map parms = new HashMap<>(); + String query = bitcoin.getQuery(); + if (query != null) { + String[] args = query.split("&"); + for (String arg : args) { + String[] namevalue = arg.split("="); + if (namevalue.length == 0) { + continue; + } + parms.put(Uri.decode(namevalue[0]).toLowerCase(), + namevalue.length > 1 ? Uri.decode(namevalue[1]) : ""); + } + } + String address = bitcoin.getPath(); + String amount = parms.get(BTC_AMOUNT); + if (amount != null) { + try { + Double.parseDouble(amount); + } catch (NumberFormatException ex) { + Timber.d(ex.getLocalizedMessage()); + return null; // we have an amount but its not a number! + } + } + if (!BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet())) { + Timber.d("address invalid"); + return null; + } + return new BarcodeData(BarcodeData.Asset.BTC, address, amount); + } + + static public BarcodeData parseBitcoinNaked(String address) { + Timber.d("parseBitcoinNaked=%s", address); + + if (address == null) return null; + + if (!BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet())) { + Timber.d("address invalid"); + return null; + } + + return new BarcodeData(BarcodeData.Asset.BTC, address); + } } \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java b/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java index d15c54d..55505da 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java +++ b/app/src/main/java/com/m2049r/xmrwallet/data/TxData.java @@ -20,36 +20,38 @@ import android.os.Parcel; import android.os.Parcelable; import com.m2049r.xmrwallet.model.PendingTransaction; +import com.m2049r.xmrwallet.util.UserNotes; + +import timber.log.Timber; // https://stackoverflow.com/questions/2139134/how-to-send-an-object-from-one-android-activity-to-another-using-intents public class TxData implements Parcelable { + public TxData() { + } + public TxData(TxData txData) { - this.dst_addr = txData.dst_addr; + this.dstAddr = txData.dstAddr; this.paymentId = txData.paymentId; this.amount = txData.amount; this.mixin = txData.mixin; this.priority = txData.priority; } - public TxData(String dst_addr, + public TxData(String dstAddr, String paymentId, long amount, int mixin, PendingTransaction.Priority priority) { - this.dst_addr = dst_addr; + this.dstAddr = dstAddr; this.paymentId = paymentId; this.amount = amount; this.mixin = mixin; this.priority = priority; } - public long getFee() { - return 0L; - } - public String getDestinationAddress() { - return dst_addr; + return dstAddr; } public String getPaymentId() { @@ -68,15 +70,45 @@ public class TxData implements Parcelable { return priority; } - final private String dst_addr; - final private String paymentId; - final private long amount; - final private int mixin; - final private PendingTransaction.Priority priority; + public void setDestinationAddress(String dstAddr) { + this.dstAddr = dstAddr; + } + + public void setPaymentId(String paymentId) { + this.paymentId = paymentId; + } + + public void setAmount(long amount) { + this.amount = amount; + } + + public void setMixin(int mixin) { + this.mixin = mixin; + } + + public void setPriority(PendingTransaction.Priority priority) { + this.priority = priority; + } + + public UserNotes getUserNotes() { + return userNotes; + } + + public void setUserNotes(UserNotes userNotes) { + this.userNotes = userNotes; + } + + private String dstAddr; + private String paymentId; + private long amount; + private int mixin; + private PendingTransaction.Priority priority; + + private UserNotes userNotes; @Override public void writeToParcel(Parcel out, int flags) { - out.writeString(dst_addr); + out.writeString(dstAddr); out.writeString(paymentId); out.writeLong(amount); out.writeInt(mixin); @@ -94,8 +126,8 @@ public class TxData implements Parcelable { } }; - private TxData(Parcel in) { - dst_addr = in.readString(); + protected TxData(Parcel in) { + dstAddr = in.readString(); paymentId = in.readString(); amount = in.readLong(); mixin = in.readInt(); @@ -111,8 +143,8 @@ public class TxData implements Parcelable { @Override public String toString() { StringBuffer sb = new StringBuffer(); - sb.append("dst_addr:"); - sb.append(dst_addr); + sb.append("dstAddr:"); + sb.append(dstAddr); sb.append(",paymentId:"); sb.append(paymentId); sb.append(",amount:"); diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java b/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java new file mode 100644 index 0000000..6b61e10 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/TxDataBtc.java @@ -0,0 +1,98 @@ +/* + * 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.data; + +import android.os.Parcel; + +import com.m2049r.xmrwallet.model.PendingTransaction; + +public class TxDataBtc extends TxData { + + private String xmrtoUuid; + private String btcAddress; + private double btcAmount; + + public TxDataBtc() { + super(); + } + + public TxDataBtc(TxDataBtc txDataBtc) { + super(txDataBtc); + } + + public String getXmrtoUuid() { + return xmrtoUuid; + } + + public void setXmrtoUuid(String xmrtoUuid) { + this.xmrtoUuid = xmrtoUuid; + } + + public String getBtcAddress() { + return btcAddress; + } + + public void setBtcAddress(String btcAddress) { + this.btcAddress = btcAddress; + } + + public double getBtcAmount() { + return btcAmount; + } + + public void setBtcAmount(double btcAmount) { + this.btcAmount = btcAmount; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(xmrtoUuid); + out.writeString(btcAddress); + out.writeDouble(btcAmount); + } + + // this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods + public static final Creator CREATOR = new Creator() { + public TxDataBtc createFromParcel(Parcel in) { + return new TxDataBtc(in); + } + + public TxDataBtc[] newArray(int size) { + return new TxDataBtc[size]; + } + }; + + protected TxDataBtc(Parcel in) { + super(in); + xmrtoUuid = in.readString(); + btcAddress = in.readString(); + btcAmount = in.readDouble(); + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(",xmrtoUuid:"); + sb.append(xmrtoUuid); + sb.append(",btcAddress:"); + sb.append(btcAddress); + sb.append(",btcAmount:"); + sb.append(btcAmount); + return sb.toString(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendAddressWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java similarity index 77% rename from app/src/main/java/com/m2049r/xmrwallet/SendAddressWizardFragment.java rename to app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java index 3b5a9d3..77ad8cd 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SendAddressWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java @@ -14,13 +14,14 @@ * limitations under the License. */ -package com.m2049r.xmrwallet; +package com.m2049r.xmrwallet.fragment.send; import android.content.Context; import android.os.Bundle; import android.support.design.widget.TextInputLayout; import android.support.v7.widget.CardView; import android.text.Editable; +import android.text.Html; import android.text.InputType; import android.text.TextWatcher; import android.view.KeyEvent; @@ -32,9 +33,13 @@ import android.widget.Button; import android.widget.EditText; import android.widget.TextView; +import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.data.TxDataBtc; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.BitcoinAddressValidator; import com.m2049r.xmrwallet.util.Helper; import timber.log.Timber; @@ -56,12 +61,12 @@ public class SendAddressWizardFragment extends SendWizardFragment { return this; } - interface Listener { - void setAddress(final String address); - - void setPaymentId(final String paymentId); - + public interface Listener { void setBarcodeData(BarcodeData data); + + void setMode(SendFragment.Mode mode); + + TxData getTxData(); } private EditText etDummy; @@ -71,6 +76,8 @@ public class SendAddressWizardFragment extends SendWizardFragment { private CardView cvScan; private View tvPaymentIdIntegrated; private View llPaymentId; + private TextView tvXmrTo; + private View llXmrTo; OnScanListener onScanListener; @@ -89,6 +96,9 @@ public class SendAddressWizardFragment extends SendWizardFragment { tvPaymentIdIntegrated = view.findViewById(R.id.tvPaymentIdIntegrated); llPaymentId = view.findViewById(R.id.llPaymentId); + llXmrTo = view.findViewById(R.id.llXmrTo); + tvXmrTo = (TextView) view.findViewById(R.id.tvXmrTo); + tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto))); etAddress = (TextInputLayout) view.findViewById(R.id.etAddress); etAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); @@ -113,12 +123,25 @@ public class SendAddressWizardFragment extends SendWizardFragment { public void afterTextChanged(Editable editable) { etAddress.setError(null); if (isIntegratedAddress()) { + Timber.d("isIntegratedAddress"); etPaymentId.getEditText().getText().clear(); - llPaymentId.setVisibility(View.GONE); + llPaymentId.setVisibility(View.INVISIBLE); tvPaymentIdIntegrated.setVisibility(View.VISIBLE); - } else { // we don't + llXmrTo.setVisibility(View.INVISIBLE); + sendListener.setMode(SendFragment.Mode.XMR); + } else if (isBitcoinAddress()) { + Timber.d("isBitcoinAddress"); + etPaymentId.getEditText().getText().clear(); + llPaymentId.setVisibility(View.INVISIBLE); + tvPaymentIdIntegrated.setVisibility(View.INVISIBLE); + llXmrTo.setVisibility(View.VISIBLE); + sendListener.setMode(SendFragment.Mode.BTC); + } else { + Timber.d("isStandardAddress"); llPaymentId.setVisibility(View.VISIBLE); - tvPaymentIdIntegrated.setVisibility(View.GONE); + tvPaymentIdIntegrated.setVisibility(View.INVISIBLE); + llXmrTo.setVisibility(View.INVISIBLE); + sendListener.setMode(SendFragment.Mode.XMR); } } @@ -188,13 +211,14 @@ public class SendAddressWizardFragment extends SendWizardFragment { private boolean checkAddressNoError() { String address = etAddress.getEditText().getText().toString(); - return Wallet.isAddressValid(address); + return Wallet.isAddressValid(address) + || BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet()); } private boolean checkAddress() { boolean ok = checkAddressNoError(); if (!ok) { - etAddress.setError(getString(R.string.send_qr_address_invalid)); + etAddress.setError(getString(R.string.send_address_invalid)); } else { etAddress.setError(null); } @@ -203,7 +227,16 @@ public class SendAddressWizardFragment extends SendWizardFragment { private boolean isIntegratedAddress() { String address = etAddress.getEditText().getText().toString(); - return (address.length() == INTEGRATED_ADDRESS_LENGTH) && Wallet.isAddressValid(address); + return (address.length() == INTEGRATED_ADDRESS_LENGTH) + && Wallet.isAddressValid(address, WalletManager.getInstance().isTestNet()); + } + + private boolean isBitcoinAddress() { + String address = etAddress.getEditText().getText().toString(); + if ((address.length() >= 27) && (address.length() <= 34)) + return BitcoinAddressValidator.validate(address, WalletManager.getInstance().isTestNet()); + else + return false; } private boolean checkPaymentId() { @@ -235,8 +268,15 @@ public class SendAddressWizardFragment extends SendWizardFragment { } if (!ok) return false; if (sendListener != null) { - sendListener.setAddress(etAddress.getEditText().getText().toString()); - sendListener.setPaymentId(etPaymentId.getEditText().getText().toString()); + TxData txData = sendListener.getTxData(); + if (isBitcoinAddress()) { + ((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString()); + txData.setDestinationAddress(null); + txData.setPaymentId(""); + } else { + txData.setDestinationAddress(etAddress.getEditText().getText().toString()); + txData.setPaymentId(etPaymentId.getEditText().getText().toString()); + } } return true; } diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendAmountWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java similarity index 93% rename from app/src/main/java/com/m2049r/xmrwallet/SendAmountWizardFragment.java rename to app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java index 033dfea..2ea8207 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SendAmountWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAmountWizardFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.m2049r.xmrwallet; +package com.m2049r.xmrwallet.fragment.send; import android.os.Bundle; import android.view.LayoutInflater; @@ -22,10 +22,12 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.data.BarcodeData; -import com.m2049r.xmrwallet.widget.ExchangeTextView; +import com.m2049r.xmrwallet.data.TxData; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.widget.ExchangeTextView; import com.m2049r.xmrwallet.widget.NumberPadView; import timber.log.Timber; @@ -48,7 +50,7 @@ public class SendAmountWizardFragment extends SendWizardFragment { interface Listener { SendFragment.Listener getActivityCallback(); - void setAmount(final long amount); + TxData getTxData(); BarcodeData popBarcodeData(); } @@ -99,9 +101,9 @@ public class SendAmountWizardFragment extends SendWizardFragment { if (sendListener != null) { String xmr = evAmount.getAmount(); if (xmr != null) { - sendListener.setAmount(Wallet.getAmountFromString(xmr)); + sendListener.getTxData().setAmount(Wallet.getAmountFromString(xmr)); } else { - sendListener.setAmount(0L); + sendListener.getTxData().setAmount(0L); } } return true; diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java new file mode 100644 index 0000000..88fe821 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcAmountWizardFragment.java @@ -0,0 +1,261 @@ +/* + * 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.fragment.send; + +import android.os.Bundle; +import android.text.Html; +import android.text.Spanned; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.TxDataBtc; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.OkHttpClientSingleton; +import com.m2049r.xmrwallet.widget.ExchangeBtcTextView; +import com.m2049r.xmrwallet.widget.NumberPadView; +import com.m2049r.xmrwallet.widget.SendProgressView; +import com.m2049r.xmrwallet.xmrto.XmrToError; +import com.m2049r.xmrwallet.xmrto.XmrToException; +import com.m2049r.xmrwallet.xmrto.api.QueryOrderParameters; +import com.m2049r.xmrwallet.xmrto.api.XmrToApi; +import com.m2049r.xmrwallet.xmrto.api.XmrToCallback; +import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl; + +import java.text.NumberFormat; +import java.util.Locale; + +import okhttp3.HttpUrl; +import timber.log.Timber; + +public class SendBtcAmountWizardFragment extends SendWizardFragment { + + public static SendBtcAmountWizardFragment newInstance(SendAmountWizardFragment.Listener listener) { + SendBtcAmountWizardFragment instance = new SendBtcAmountWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + SendAmountWizardFragment.Listener sendListener; + + public SendBtcAmountWizardFragment setSendListener(SendAmountWizardFragment.Listener listener) { + this.sendListener = listener; + return this; + } + + private TextView tvFunds; + private ExchangeBtcTextView evAmount; + private NumberPadView numberPad; + + private TextView tvXmrToParms; + private SendProgressView evParams; + private View llXmrToParms; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState))); + + sendListener = (SendAmountWizardFragment.Listener) getParentFragment(); + + View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false); + + tvFunds = (TextView) view.findViewById(R.id.tvFunds); + + evParams = (SendProgressView) view.findViewById(R.id.evXmrToParms); + llXmrToParms = view.findViewById(R.id.llXmrToParms); + + tvXmrToParms = (TextView) view.findViewById(R.id.tvXmrToParms); + + evAmount = (ExchangeBtcTextView) view.findViewById(R.id.evAmount); + numberPad = (NumberPadView) view.findViewById(R.id.numberPad); + numberPad.setListener(evAmount); + + Helper.hideKeyboard(getActivity()); + + return view; + } + + + @Override + public boolean onValidateFields() { + if (!evAmount.validate(maxBtc, minBtc)) { + return false; + } + if (sendListener != null) { + TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData(); + String btcString = evAmount.getAmount(); + if (btcString != null) { + try { + double btc = Double.parseDouble(btcString); + Timber.d("setAmount %f", btc); + txDataBtc.setBtcAmount(btc); + } catch (NumberFormatException ex) { + Timber.d(ex.getLocalizedMessage()); + txDataBtc.setBtcAmount(0); + } + } else { + txDataBtc.setBtcAmount(0); + } + } + return true; + } + + double maxBtc = 0; + double minBtc = 0; + + @Override + public void onPauseFragment() { + llXmrToParms.setVisibility(View.INVISIBLE); + } + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + Helper.hideKeyboard(getActivity()); + final long funds = getTotalFunds(); + tvFunds.setText(getString(R.string.send_available, + Wallet.getDisplayAmount(funds))); + if ((evAmount.getAmount() == null) || evAmount.getAmount().isEmpty()) { + final BarcodeData data = sendListener.popBarcodeData(); + if ((data != null) && (data.amount != null)) { + evAmount.setAmount(data.amount); + } + } + callXmrTo(); + } + + long getTotalFunds() { + return sendListener.getActivityCallback().getTotalFunds(); + } + + private QueryOrderParameters orderParameters = null; + + private void processOrderParms(final QueryOrderParameters orderParameters) { + this.orderParameters = orderParameters; + getView().post(new Runnable() { + @Override + public void run() { + evAmount.setRate(1.0d / orderParameters.getPrice()); + NumberFormat df = NumberFormat.getInstance(Locale.US); + df.setMaximumFractionDigits(6); + String min = df.format(orderParameters.getLowerLimit()); + String max = df.format(orderParameters.getUpperLimit()); + String rate = df.format(orderParameters.getPrice()); + Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms, min, max, rate)); + if (orderParameters.isZeroConfEnabled()) { + String zeroConf = df.format(orderParameters.getZeroConfMaxAmount()); + Spanned zeroConfText = Html.fromHtml(getString(R.string.info_send_xmrto_zeroconf, zeroConf)); + xmrParmText = (Spanned) TextUtils.concat(xmrParmText, " ", zeroConfText); + } + tvXmrToParms.setText(xmrParmText); + maxBtc = orderParameters.getUpperLimit(); + minBtc = orderParameters.getLowerLimit(); + Timber.d("minBtc=%f / maxBtc=%f", minBtc, maxBtc); + + final long funds = getTotalFunds(); + double availableXmr = 1.0 * funds / 1000000000000L; + maxBtc = Math.min(maxBtc, availableXmr * orderParameters.getPrice()); + + String availBtcString = df.format(availableXmr * orderParameters.getPrice()); + String availXmrString = df.format(availableXmr); + tvFunds.setText(getString(R.string.send_available_btc, + availXmrString, + availBtcString)); + llXmrToParms.setVisibility(View.VISIBLE); + evParams.hideProgress(); + } + }); + } + + private void processOrderParmsError(final Exception ex) { + evAmount.setRate(0); + orderParameters = null; + maxBtc = 0; + minBtc = 0; + Timber.e(ex); + getView().post(new Runnable() { + @Override + public void run() { + if (ex instanceof XmrToException) { + XmrToException xmrEx = (XmrToException) ex; + XmrToError xmrErr = xmrEx.getError(); + if (xmrErr != null) { + if (xmrErr.isRetryable()) { + evParams.showMessage(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_retry)); + evParams.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + evParams.setOnClickListener(null); + callXmrTo(); + } + }); + } else { + evParams.showMessage(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_noretry)); + } + } else { + evParams.showMessage(getString(R.string.label_generic_xmrto_error), + getString(R.string.text_generic_xmrto_error, xmrEx.getCode()), + getString(R.string.text_noretry)); + } + } else { + evParams.showMessage(getString(R.string.label_generic_xmrto_error), + ex.getLocalizedMessage(), + getString(R.string.text_noretry)); + } + } + }); + } + + private void callXmrTo() { + evParams.showProgress(getString(R.string.label_send_progress_queryparms)); + getXmrToApi().queryOrderParameters(new XmrToCallback() { + @Override + public void onSuccess(final QueryOrderParameters orderParameters) { + processOrderParms(orderParameters); + } + + @Override + public void onError(final Exception e) { + processOrderParmsError(e); + } + }); + } + + private XmrToApi xmrToApi = null; + + private final XmrToApi getXmrToApi() { + if (xmrToApi == null) { + synchronized (this) { + if (xmrToApi == null) { + xmrToApi = new XmrToApiImpl(OkHttpClientSingleton.getOkHttpClient(), + Helper.getXmrToBaseUrl()); + } + } + } + return xmrToApi; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java new file mode 100644 index 0000000..3f1bd5e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcConfirmWizardFragment.java @@ -0,0 +1,680 @@ +/* + * 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.fragment.send; + +import android.app.Activity; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.support.design.widget.TextInputLayout; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +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.util.Helper; +import com.m2049r.xmrwallet.util.OkHttpClientSingleton; +import com.m2049r.xmrwallet.widget.SendProgressView; +import com.m2049r.xmrwallet.xmrto.XmrToError; +import com.m2049r.xmrwallet.xmrto.XmrToException; +import com.m2049r.xmrwallet.xmrto.api.CreateOrder; +import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus; +import com.m2049r.xmrwallet.xmrto.api.XmrToApi; +import com.m2049r.xmrwallet.xmrto.api.XmrToCallback; +import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl; + +import java.text.NumberFormat; +import java.util.Locale; + +import okhttp3.HttpUrl; +import timber.log.Timber; + +public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm { + private final int QUERY_INTERVAL = 500;//ms + + public static SendBtcConfirmWizardFragment newInstance(SendConfirmWizardFragment.Listener listener) { + SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + SendConfirmWizardFragment.Listener sendListener; + + public SendBtcConfirmWizardFragment setSendListener(SendConfirmWizardFragment.Listener listener) { + this.sendListener = listener; + return this; + } + + private View llStageA; + private SendProgressView evStageA; + private View llStageB; + private SendProgressView evStageB; + private View llStageC; + private SendProgressView evStageC; + private TextView tvTxBtcAmount; + private TextView tvTxBtcRate; + private TextView tvTxBtcAddress; + private TextView tvTxXmrToKey; + private TextView tvTxFee; + private TextView tvTxTotal; + private View llConfirmSend; + private Button bSend; + private View pbProgressSend; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState))); + + View view = inflater.inflate( + R.layout.fragment_send_btc_confirm, container, false); + + tvTxBtcAddress = (TextView) view.findViewById(R.id.tvTxBtcAddress); + tvTxBtcAmount = ((TextView) view.findViewById(R.id.tvTxBtcAmount)); + tvTxBtcRate = (TextView) view.findViewById(R.id.tvTxBtcRate); + tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey); + + tvTxFee = (TextView) view.findViewById(R.id.tvTxFee); + tvTxTotal = (TextView) view.findViewById(R.id.tvTxTotal); + + + llStageA = view.findViewById(R.id.llStageA); + evStageA = (SendProgressView) view.findViewById(R.id.evStageA); + llStageB = view.findViewById(R.id.llStageB); + evStageB = (SendProgressView) view.findViewById(R.id.evStageB); + llStageC = view.findViewById(R.id.llStageC); + evStageC = (SendProgressView) view.findViewById(R.id.evStageC); + + tvTxXmrToKey.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show(); + } + }); + + llConfirmSend = view.findViewById(R.id.llConfirmSend); + pbProgressSend = view.findViewById(R.id.pbProgressSend); + + bSend = (Button) view.findViewById(R.id.bSend); + bSend.setEnabled(false); + + bSend.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Timber.d("bSend.setOnClickListener"); + bSend.setEnabled(false); + preSend(); + } + }); + + 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; + + private void showProgress(int stage, String progressText) { + Timber.d("showProgress(%d)", stage); + inProgress = stage; + switch (stage) { + case STAGE_A: + evStageA.showProgress(progressText); + break; + case STAGE_B: + evStageB.showProgress(progressText); + break; + case STAGE_C: + evStageC.showProgress(progressText); + break; + default: + throw new IllegalStateException("unknown stage " + stage); + } + } + + public void hideProgress() { + Timber.d("hideProgress(%d)", inProgress); + switch (inProgress) { + case STAGE_A: + evStageA.hideProgress(); + llStageA.setVisibility(View.VISIBLE); + break; + case STAGE_B: + evStageB.hideProgress(); + llStageB.setVisibility(View.VISIBLE); + break; + case STAGE_C: + evStageC.hideProgress(); + llStageC.setVisibility(View.VISIBLE); + break; + default: + throw new IllegalStateException("unknown stage " + inProgress); + } + inProgress = STAGE_X; + } + + public void showStageError(String code, String message, String solution) { + switch (inProgress) { + case STAGE_A: + evStageA.showMessage(code, message, solution); + break; + case STAGE_B: + evStageB.showMessage(code, message, solution); + break; + case STAGE_C: + evStageC.showMessage(code, message, solution); + break; + default: + throw new IllegalStateException("unknown stage"); + } + inProgress = STAGE_X; + } + + PendingTransaction pendingTransaction = null; + + void send() { + Timber.d("SEND @%d", sendCountdown); + if (sendCountdown <= 0) { + Timber.i("User waited too long in password dialog."); + Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show(); + return; + } + sendListener.getTxData().getUserNotes().setXmrtoStatus(xmrtoStatus); + ((TxDataBtc) sendListener.getTxData()).setXmrtoUuid(xmrtoStatus.getUuid()); + // TODO make method in TxDataBtc to set both of the above in one go + sendListener.commitTransaction(); + pbProgressSend.setVisibility(View.VISIBLE); + } + + @Override + public void sendFailed() { + Timber.e("SEND FAILED"); + pbProgressSend.setVisibility(View.INVISIBLE); + } + + @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) + && (xmrtoStatus != null) + && (xmrtoStatus.isCreated() + && (xmrtoStatus.getUuid().equals(txTag)))) { + this.pendingTransaction = pendingTransaction; + getView().post(new Runnable() { + @Override + public void run() { + hideProgress(); + tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee())); + tvTxTotal.setText(Wallet.getDisplayAmount( + pendingTransaction.getFee() + pendingTransaction.getAmount())); + updateSendButton(); + } + }); + } else { + this.pendingTransaction = null; + sendListener.disposeTransaction(); + } + } + + @Override + public void createTransactionFailed(String errorText) { + Timber.e("CREATE TX FAILED"); + if (pendingTransaction != null) { + throw new IllegalStateException("pendingTransaction is not null"); + } + showStageError(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; + stopSendTimer(); + sendListener.disposeTransaction(); + pendingTransaction = null; + inProgress = STAGE_X; + updateSendButton(); + super.onPauseFragment(); + } + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + if (sendListener.getMode() != SendFragment.Mode.BTC) { + throw new IllegalStateException("Mode is not BTC!"); + } + Helper.hideKeyboard(getActivity()); + llStageA.setVisibility(View.INVISIBLE); + evStageA.hideProgress(); + llStageB.setVisibility(View.INVISIBLE); + evStageB.hideProgress(); + llStageC.setVisibility(View.INVISIBLE); + evStageC.hideProgress(); + isResumed = true; + if ((pendingTransaction == null) && (inProgress == STAGE_X)) { + createOrder(); + } // otherwise just sit there blank + // TODO: don't sit there blank - can this happen? should we just die? + } + + private int sendCountdown = 0; + private static final int XMRTO_COUNTDOWN = 10 * 60; // 10 minutes + private static final int XMRTO_COUNTDOWN_STEP = 1; // 1 second + + Runnable updateRunnable = null; + + void startSendTimer() { + Timber.d("startSendTimer()"); + sendCountdown = XMRTO_COUNTDOWN; + updateRunnable = new Runnable() { + @Override + public void run() { + if (!isAdded()) + return; + Timber.d("updateTimer()"); + if (sendCountdown <= 0) { + bSend.setEnabled(false); + sendCountdown = 0; + Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show(); + } + int minutes = sendCountdown / 60; + int seconds = sendCountdown % 60; + String t = String.format("%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); + } + } + }; + getView().post(updateRunnable); + } + + void stopSendTimer() { + getView().removeCallbacks(updateRunnable); + } + + void updateSendButton() { + Timber.d("updateSendButton()"); + if (pendingTransaction != null) { + llConfirmSend.setVisibility(View.VISIBLE); + bSend.setEnabled(sendCountdown > 0); + } else { + llConfirmSend.setVisibility(View.GONE); + bSend.setEnabled(false); + } + } + + public void preSend() { + final Activity activity = getActivity(); + View promptsView = getLayoutInflater().inflate(R.layout.prompt_password, null); + android.app.AlertDialog.Builder alertDialogBuilder = new android.app.AlertDialog.Builder(activity); + alertDialogBuilder.setView(promptsView); + + final TextInputLayout etPassword = (TextInputLayout) promptsView.findViewById(R.id.etPassword); + etPassword.setHint(getString(R.string.prompt_send_password)); + + etPassword.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + if (etPassword.getError() != null) { + etPassword.setError(null); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, + int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, + int before, int count) { + } + }); + + alertDialogBuilder + .setCancelable(false) + .setPositiveButton("OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + String pass = etPassword.getEditText().getText().toString(); + if (getActivityCallback().verifyWalletPassword(pass)) { + dialog.dismiss(); + Helper.hideKeyboardAlways(activity); + send(); + } else { + etPassword.setError(getString(R.string.bad_password)); + } + } + }) + .setNegativeButton("Cancel", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Helper.hideKeyboardAlways(activity); + dialog.cancel(); + bSend.setEnabled(sendCountdown > 0); // allow to try again + } + }); + + final android.app.AlertDialog passwordDialog = alertDialogBuilder.create(); + passwordDialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + Button button = ((android.app.AlertDialog) dialog).getButton(android.app.AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + String pass = etPassword.getEditText().getText().toString(); + if (getActivityCallback().verifyWalletPassword(pass)) { + Helper.hideKeyboardAlways(activity); + passwordDialog.dismiss(); + send(); + } else { + etPassword.setError(getString(R.string.bad_password)); + } + } + }); + } + }); + + Helper.showKeyboard(passwordDialog); + + // accept keyboard "ok" + etPassword.getEditText().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_DONE)) { + String pass = etPassword.getEditText().getText().toString(); + if (getActivityCallback().verifyWalletPassword(pass)) { + Helper.hideKeyboardAlways(activity); + passwordDialog.dismiss(); + send(); + } else { + etPassword.setError(getString(R.string.bad_password)); + } + return true; + } + return false; + } + }); + passwordDialog.show(); + } + + // creates a pending transaction and calls us back with transactionCreated() + // or createTransactionFailed() + void prepareSend() { + if (!isResumed) return; + if ((xmrtoStatus == null)) { + throw new IllegalStateException("xmrtoStatus is null"); + } + if ((!xmrtoStatus.isCreated())) { + throw new IllegalStateException("order is not created"); + } + showProgress(3, getString(R.string.label_send_progress_create_tx)); + TxData txData = sendListener.getTxData(); + txData.setDestinationAddress(xmrtoStatus.getXmrReceivingAddress()); + txData.setPaymentId(xmrtoStatus.getXmrRequiredPaymentIdShort()); + txData.setAmount(Wallet.getAmountFromDouble(xmrtoStatus.getXmrAmountTotal())); + getActivityCallback().onPrepareSend(xmrtoStatus.getUuid(), txData); + } + + SendFragment.Listener getActivityCallback() { + return sendListener.getActivityCallback(); + } + + private CreateOrder xmrtoOrder = null; + + private void processCreateOrder(final CreateOrder createOrder) { + Timber.d("processCreateOrder %s", createOrder.getUuid()); + xmrtoOrder = createOrder; + if (QueryOrderStatus.State.TO_BE_CREATED.toString().equals(createOrder.getState())) { + getView().post(new Runnable() { + @Override + public void run() { + tvTxXmrToKey.setText(createOrder.getUuid()); + tvTxBtcAddress.setText(createOrder.getBtcDestAddress()); + hideProgress(); + } + }); + queryOrder(createOrder.getUuid()); + } else { + throw new IllegalStateException("Create Order is not TO_BE_CREATED"); + } + } + + private void processCreateOrderError(final Exception ex) { + Timber.e("processCreateOrderError %s", ex.getLocalizedMessage()); + getView().post(new Runnable() { + @Override + public void run() { + if (ex instanceof XmrToException) { + XmrToException xmrEx = (XmrToException) ex; + XmrToError xmrErr = xmrEx.getError(); + if (xmrErr != null) { + if (xmrErr.isRetryable()) { + showStageError(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_retry)); + evStageA.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + evStageA.setOnClickListener(null); + createOrder(); + } + }); + } else { + showStageError(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_noretry)); + } + } else { + showStageError(getString(R.string.label_generic_xmrto_error), + getString(R.string.text_generic_xmrto_error, xmrEx.getCode()), + getString(R.string.text_noretry)); + } + } else { + evStageA.showMessage(getString(R.string.label_generic_xmrto_error), + ex.getLocalizedMessage(), + getString(R.string.text_noretry)); + } + } + }); + } + + private void createOrder() { + if (!isResumed) return; + Timber.d("createOrder"); + xmrtoOrder = null; + xmrtoStatus = null; + showProgress(1, getString(R.string.label_send_progress_xmrto_create)); + TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData(); + double btcAmount = txDataBtc.getBtcAmount(); + getXmrToApi().createOrder(btcAmount, txDataBtc.getBtcAddress(), new XmrToCallback() { + @Override + public void onSuccess(CreateOrder createOrder) { + if (!isResumed) return; + if (xmrtoOrder != null) { + Timber.w("another ongoing create order request"); + return; + } + processCreateOrder(createOrder); + } + + @Override + public void onError(Exception ex) { + if (!isResumed) return; + if (xmrtoOrder != null) { + Timber.w("another ongoing create order request"); + return; + } + processCreateOrderError(ex); + } + }); + } + + private QueryOrderStatus xmrtoStatus = null; + + private void processQueryOrder(final QueryOrderStatus status) { + Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getUuid()); + xmrtoStatus = status; + if (status.isCreated()) { + getView().post(new Runnable() { + @Override + public void run() { + NumberFormat df = NumberFormat.getInstance(Locale.US); + df.setMaximumFractionDigits(12); + String btcAmount = df.format(status.getBtcAmount()); + String xmrAmountTotal = df.format(status.getXmrAmountTotal()); + tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount, btcAmount, xmrAmountTotal)); + String xmrPriceBtc = df.format(status.getXmrPriceBtc()); + tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, xmrPriceBtc)); + + double calcRate = status.getBtcAmount() / status.getXmrPriceBtc(); + Timber.i("Rates: %f / %f", calcRate, status.getXmrPriceBtc()); + + tvTxBtcAddress.setText(status.getBtcDestAddress()); // TODO test if this is different? + + Timber.i("Expires @ %s, in %s seconds", status.getExpiresAt().toString(), status.getSecondsTillTimeout()); + + Timber.i("Status = %s", status.getState().toString()); + tvTxXmrToKey.setText(status.getUuid()); + + Timber.d("AmountRemaining=%f, XmrAmountTotal=%f", status.getXmrAmountRemaining(), status.getXmrAmountTotal()); + hideProgress(); + startSendTimer(); + prepareSend(); + } + }); + } else { + Timber.d("try again!"); + handler.postDelayed(new Runnable() { + @Override + public void run() { + queryOrder(status.getUuid()); + } + }, QUERY_INTERVAL); + } + } + + Handler handler = new Handler(); + + private void processQueryOrderError(final Exception ex) { + Timber.e("processQueryOrderError %s", ex.getLocalizedMessage()); + getView().post(new Runnable() { + @Override + public void run() { + if (ex instanceof XmrToException) { + XmrToException xmrEx = (XmrToException) ex; + XmrToError xmrErr = xmrEx.getError(); + if (xmrErr != null) { + if (xmrErr.isRetryable()) { + showStageError(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_retry)); + evStageB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + evStageB.setOnClickListener(null); + queryOrder(xmrtoOrder.getUuid()); + } + }); + } else { + showStageError(xmrErr.getErrorId().toString(), xmrErr.getErrorMsg(), + getString(R.string.text_noretry)); + } + } else { + showStageError(getString(R.string.label_generic_xmrto_error), + getString(R.string.text_generic_xmrto_error, xmrEx.getCode()), + getString(R.string.text_noretry)); + } + } else { + evStageB.showMessage(getString(R.string.label_generic_xmrto_error), + ex.getLocalizedMessage(), + getString(R.string.text_noretry)); + } + } + }); + } + + private void queryOrder(final String uuid) { + Timber.d("queryOrder(%s)", uuid); + if (!isResumed) return; + getView().post(new Runnable() { + @Override + public void run() { + xmrtoStatus = null; + showProgress(2, getString(R.string.label_send_progress_xmrto_query)); + getXmrToApi().queryOrderStatus(uuid, new XmrToCallback() { + @Override + public void onSuccess(QueryOrderStatus status) { + if (!isResumed) return; + if (xmrtoOrder == null) return; + if (!status.getUuid().equals(xmrtoOrder.getUuid())) { + Timber.d("Query UUID does not match"); + // ignore (we got a response to a stale request) + return; + } + if (xmrtoStatus != null) + throw new IllegalStateException("xmrtoStatus must be null here!"); + processQueryOrder(status); + } + + @Override + public void onError(Exception ex) { + if (!isResumed) return; + processQueryOrderError(ex); + } + }); + } + }); + } + + private XmrToApi xmrToApi = null; + + private final XmrToApi getXmrToApi() { + if (xmrToApi == null) { + synchronized (this) { + if (xmrToApi == null) { + xmrToApi = new XmrToApiImpl(OkHttpClientSingleton.getOkHttpClient(), + Helper.getXmrToBaseUrl()); + } + } + } + return xmrToApi; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java new file mode 100644 index 0000000..ca56e2a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendBtcSuccessWizardFragment.java @@ -0,0 +1,267 @@ +/* + * 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.fragment.send; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.PendingTx; +import com.m2049r.xmrwallet.data.TxDataBtc; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.OkHttpClientSingleton; +import com.m2049r.xmrwallet.xmrto.XmrToException; +import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus; +import com.m2049r.xmrwallet.xmrto.api.XmrToApi; +import com.m2049r.xmrwallet.xmrto.api.XmrToCallback; +import com.m2049r.xmrwallet.xmrto.network.XmrToApiImpl; + +import java.text.NumberFormat; +import java.util.Locale; + +import okhttp3.HttpUrl; +import timber.log.Timber; + +public class SendBtcSuccessWizardFragment extends SendWizardFragment { + + public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) { + SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment(); + instance.setSendListener(listener); + return instance; + } + + SendSuccessWizardFragment.Listener sendListener; + + public SendBtcSuccessWizardFragment setSendListener(SendSuccessWizardFragment.Listener listener) { + this.sendListener = listener; + return this; + } + + ImageButton bCopyTxId; + private TextView tvTxId; + private TextView tvTxAddress; + private TextView tvTxPaymentId; + private TextView tvTxAmount; + private TextView tvTxFee; + private TextView tvXmrToAmount; + private TextView tvXmrToStatus; + private ImageView ivXmrToStatus; + private ImageView ivXmrToStatusBig; + private ProgressBar pbXmrto; + private TextView tvTxXmrToKey; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState))); + + View view = inflater.inflate( + R.layout.fragment_send_btc_success, container, false); + + bCopyTxId = (ImageButton) view.findViewById(R.id.bCopyTxId); + bCopyTxId.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show(); + } + }); + + tvXmrToAmount = (TextView) view.findViewById(R.id.tvXmrToAmount); + tvXmrToStatus = (TextView) view.findViewById(R.id.tvXmrToStatus); + ivXmrToStatus = (ImageView) view.findViewById(R.id.ivXmrToStatus); + ivXmrToStatusBig = (ImageView) view.findViewById(R.id.ivXmrToStatusBig); + + tvTxId = (TextView) view.findViewById(R.id.tvTxId); + tvTxAddress = (TextView) view.findViewById(R.id.tvTxAddress); + tvTxPaymentId = (TextView) view.findViewById(R.id.tvTxPaymentId); + tvTxAmount = ((TextView) view.findViewById(R.id.tvTxAmount)); + tvTxFee = (TextView) view.findViewById(R.id.tvTxFee); + + pbXmrto = (ProgressBar) view.findViewById(R.id.pbXmrto); + pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY); + + tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey); + tvTxXmrToKey.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show(); + ; + } + }); + + return view; + } + + @Override + public boolean onValidateFields() { + return true; + } + + private boolean isResumed = false; + + @Override + public void onPauseFragment() { + isResumed = false; + super.onPauseFragment(); + } + + TxDataBtc btcData = null; + + @Override + public void onResumeFragment() { + super.onResumeFragment(); + Timber.d("onResumeFragment()"); + Helper.hideKeyboard(getActivity()); + isResumed = true; + + btcData = (TxDataBtc) sendListener.getTxData(); + tvTxAddress.setText(btcData.getDestinationAddress()); + String paymentId = btcData.getPaymentId(); + if ((paymentId != null) && (!paymentId.isEmpty())) { + tvTxPaymentId.setText(btcData.getPaymentId()); + } else { + tvTxPaymentId.setText("-"); + } + + final PendingTx committedTx = sendListener.getCommittedTx(); + if (committedTx != null) { + tvTxId.setText(committedTx.txId); + bCopyTxId.setEnabled(true); + bCopyTxId.setImageResource(R.drawable.ic_content_copy_black_24dp); + 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)); + //TODO btcData.getBtcAddress(); + tvTxXmrToKey.setText(btcData.getXmrtoUuid()); + queryOrder(); + } else { + throw new IllegalStateException("btcData is null"); + } + } + sendListener.enableDone(); + } + + private final int QUERY_INTERVAL = 1000; // ms + + private void processQueryOrder(final QueryOrderStatus status) { + Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getUuid()); + if (!btcData.getXmrtoUuid().equals(status.getUuid())) + throw new IllegalStateException("UUIDs do not match!"); + if (isResumed && (getView() != null)) + getView().post(new Runnable() { + @Override + public void run() { + showXmrToStatus(status); + if (!status.isTerminal()) { + getView().postDelayed(new Runnable() { + @Override + public void run() { + queryOrder(); + } + }, QUERY_INTERVAL); + } + } + }); + } + + private void queryOrder() { + Timber.d("queryOrder(%s)", btcData.getXmrtoUuid()); + if (!isResumed) return; + getXmrToApi().queryOrderStatus(btcData.getXmrtoUuid(), new XmrToCallback() { + @Override + public void onSuccess(QueryOrderStatus status) { + if (!isAdded()) return; + processQueryOrder(status); + } + + @Override + public void onError(final Exception ex) { + if (!isResumed) return; + Timber.e(ex); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (ex instanceof XmrToException) { + Toast.makeText(getActivity(), ((XmrToException) ex).getError().getErrorMsg(), Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + } + } + }); + } + }); + } + + private int statusResource = 0; + + void showXmrToStatus(final QueryOrderStatus status) { + if (status.isError()) { + tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, status.toString())); + statusResource = R.drawable.ic_error_red_24dp; + pbXmrto.getIndeterminateDrawable().setColorFilter(0xff8b0000, android.graphics.PorterDuff.Mode.MULTIPLY); + } else if (status.isSent()) { + tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent)); + statusResource = R.drawable.ic_success_green_24dp; + pbXmrto.getIndeterminateDrawable().setColorFilter(0xFF417505, android.graphics.PorterDuff.Mode.MULTIPLY); + } else if (status.isPending()) { + if (status.isPaid()) { + tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid)); + } else { + tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid)); + } + statusResource = R.drawable.ic_pending_orange_24dp; + pbXmrto.getIndeterminateDrawable().setColorFilter(0xFFFF6105, android.graphics.PorterDuff.Mode.MULTIPLY); + } else { + throw new IllegalStateException("status is broken: " + status.toString()); + } + ivXmrToStatus.setImageResource(statusResource); + if (status.isTerminal()) { + pbXmrto.setVisibility(View.INVISIBLE); + ivXmrToStatus.setVisibility(View.GONE); + ivXmrToStatusBig.setImageResource(statusResource); + ivXmrToStatusBig.setVisibility(View.VISIBLE); + } + } + + private XmrToApi xmrToApi = null; + + private final XmrToApi getXmrToApi() { + if (xmrToApi == null) { + synchronized (this) { + if (xmrToApi == null) { + xmrToApi = new XmrToApiImpl(OkHttpClientSingleton.getOkHttpClient(), + Helper.getXmrToBaseUrl()); + } + } + } + return xmrToApi; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirm.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirm.java new file mode 100644 index 0000000..965ca4c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirm.java @@ -0,0 +1,27 @@ +/* + * 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.fragment.send; + +import com.m2049r.xmrwallet.model.PendingTransaction; + +interface SendConfirm { + void sendFailed(); + + void createTransactionFailed(String errorText); + + void transactionCreated(String txTag, PendingTransaction pendingTransaction); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendConfirmWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java similarity index 89% rename from app/src/main/java/com/m2049r/xmrwallet/SendConfirmWizardFragment.java rename to app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java index 2d779b0..73e3d91 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SendConfirmWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendConfirmWizardFragment.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package com.m2049r.xmrwallet; +package com.m2049r.xmrwallet.fragment.send; import android.app.Activity; import android.content.DialogInterface; import android.os.Bundle; import android.support.design.widget.TextInputLayout; +import android.support.v7.app.AlertDialog; import android.text.Editable; import android.text.TextWatcher; import android.view.KeyEvent; @@ -30,14 +31,16 @@ import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.TextView; +import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.data.TxData; import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.UserNotes; import timber.log.Timber; -public class SendConfirmWizardFragment extends SendWizardFragment { +public class SendConfirmWizardFragment extends SendWizardFragment implements SendConfirm { public static SendConfirmWizardFragment newInstance(Listener listener) { SendConfirmWizardFragment instance = new SendConfirmWizardFragment(); @@ -57,11 +60,11 @@ public class SendConfirmWizardFragment extends SendWizardFragment { TxData getTxData(); - String getNotes(); - void commitTransaction(); void disposeTransaction(); + + SendFragment.Mode getMode(); } private TextView tvTxAddress; @@ -122,8 +125,11 @@ public class SendConfirmWizardFragment extends SendWizardFragment { PendingTransaction pendingTransaction = null; + @Override // callback from wallet when PendingTransaction created - void transactionCreated(PendingTransaction pendingTransaction) { + public void transactionCreated(String txTag, PendingTransaction pendingTransaction) { + // ignore txTag - the app flow ensures this is the correct tx + // TODO: use the txTag hideProgress(); if (isResumed) { this.pendingTransaction = pendingTransaction; @@ -138,10 +144,22 @@ public class SendConfirmWizardFragment extends SendWizardFragment { pbProgressSend.setVisibility(View.VISIBLE); } - void sendFailed() { + @Override + public void sendFailed() { pbProgressSend.setVisibility(View.INVISIBLE); } + @Override + public void createTransactionFailed(String errorText) { + hideProgress(); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setCancelable(true). + setTitle(getString(R.string.send_create_tx_error_title)). + setMessage(errorText). + create(). + show(); + } + @Override public boolean onValidateFields() { return true; @@ -173,9 +191,9 @@ public class SendConfirmWizardFragment extends SendWizardFragment { } else { tvTxPaymentId.setText("-"); } - String notes = sendListener.getNotes(); - if ((notes != null) && (!notes.isEmpty())) { - tvTxNotes.setText(sendListener.getNotes()); + UserNotes notes = sendListener.getTxData().getUserNotes(); + if ((notes != null) && (!notes.note.isEmpty())) { + tvTxNotes.setText(notes.note); } else { tvTxNotes.setText("-"); } @@ -296,8 +314,10 @@ public class SendConfirmWizardFragment extends SendWizardFragment { passwordDialog.show(); } + // creates a pending transaction and calls us back with transactionCreated() + // or createTransactionFailed() void prepareSend(TxData txData) { - getActivityCallback().onPrepareSend(txData); + getActivityCallback().onPrepareSend(null, txData); } SendFragment.Listener getActivityCallback() { diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java similarity index 75% rename from app/src/main/java/com/m2049r/xmrwallet/SendFragment.java rename to app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java index d8dd537..664ee60 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.m2049r.xmrwallet; +package com.m2049r.xmrwallet.fragment.send; import android.content.Context; import android.graphics.drawable.Drawable; @@ -22,9 +22,8 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.view.ViewPager; -import android.support.v7.app.AlertDialog; import android.text.InputType; import android.util.SparseArray; import android.view.LayoutInflater; @@ -36,12 +35,16 @@ import android.widget.Button; import android.widget.EditText; import android.widget.Toast; +import com.m2049r.xmrwallet.OnBackPressedListener; +import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.data.BarcodeData; import com.m2049r.xmrwallet.data.PendingTx; import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.data.TxDataBtc; import com.m2049r.xmrwallet.layout.SpendViewPager; import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.UserNotes; import com.m2049r.xmrwallet.widget.DotBar; import com.m2049r.xmrwallet.widget.Toolbar; @@ -62,11 +65,11 @@ public class SendFragment extends Fragment public interface Listener { long getTotalFunds(); - void onPrepareSend(TxData data); + void onPrepareSend(String tag, TxData data); boolean verifyWalletPassword(String password); - void onSend(String notes); + void onSend(UserNotes notes); void onDisposeRequest(); @@ -231,7 +234,42 @@ public class SendFragment extends Fragment } } - public class SpendPagerAdapter extends FragmentPagerAdapter { + enum Mode { + XMR, BTC + } + + Mode mode = Mode.XMR; + + @Override + public void setMode(Mode aMode) { + if (mode != aMode) { + mode = aMode; + switch (aMode) { + case XMR: + txData = new TxData(); + break; + case BTC: + txData = new TxDataBtc(); + break; + default: + throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!"); + } + getView().post(new Runnable() { + @Override + public void run() { + pagerAdapter.notifyDataSetChanged(); + } + }); + Timber.d("New Mode = " + mode.toString()); + } + } + + @Override + public Mode getMode() { + return mode; + } + + public class SpendPagerAdapter extends FragmentStatePagerAdapter { private static final int POS_ADDRESS = 0; private static final int POS_AMOUNT = 1; private static final int POS_SETTINGS = 2; @@ -257,6 +295,7 @@ public class SendFragment extends Fragment @Override public Object instantiateItem(ViewGroup container, int position) { + Timber.d("instantiateItem %d", position); SendWizardFragment fragment = (SendWizardFragment) super.instantiateItem(container, position); myFragments.put(position, new WeakReference<>(fragment)); return fragment; @@ -264,6 +303,7 @@ public class SendFragment extends Fragment @Override public void destroyItem(ViewGroup container, int position, Object object) { + Timber.d("destroyItem %d", position); myFragments.remove(position); super.destroyItem(container, position, object); } @@ -279,19 +319,39 @@ public class SendFragment extends Fragment @Override public SendWizardFragment getItem(int position) { Timber.d("getItem(%d) CREATE", position); - switch (position) { - case POS_ADDRESS: - return SendAddressWizardFragment.newInstance(SendFragment.this); - case POS_AMOUNT: - return SendAmountWizardFragment.newInstance(SendFragment.this); - case POS_SETTINGS: - return SendSettingsWizardFragment.newInstance(SendFragment.this); - case POS_CONFIRM: - return SendConfirmWizardFragment.newInstance(SendFragment.this); - case POS_SUCCESS: - return SendSuccessWizardFragment.newInstance(SendFragment.this); - default: - throw new IllegalArgumentException("no such send position(" + position + ")"); + Timber.d("Mode=" + mode.toString()); + if (mode == Mode.XMR) { + switch (position) { + case POS_ADDRESS: + return SendAddressWizardFragment.newInstance(SendFragment.this); + case POS_AMOUNT: + return SendAmountWizardFragment.newInstance(SendFragment.this); + case POS_SETTINGS: + return SendSettingsWizardFragment.newInstance(SendFragment.this); + case POS_CONFIRM: + return SendConfirmWizardFragment.newInstance(SendFragment.this); + case POS_SUCCESS: + return SendSuccessWizardFragment.newInstance(SendFragment.this); + default: + throw new IllegalArgumentException("no such send position(" + position + ")"); + } + } else if (mode == Mode.BTC) { + switch (position) { + case POS_ADDRESS: + return SendAddressWizardFragment.newInstance(SendFragment.this); + case POS_AMOUNT: + return SendBtcAmountWizardFragment.newInstance(SendFragment.this); + case POS_SETTINGS: + return SendSettingsWizardFragment.newInstance(SendFragment.this); + case POS_CONFIRM: + return SendBtcConfirmWizardFragment.newInstance(SendFragment.this); + case POS_SUCCESS: + return SendBtcSuccessWizardFragment.newInstance(SendFragment.this); + default: + throw new IllegalArgumentException("no such send position(" + position + ")"); + } + } else { + throw new IllegalStateException("Unknown mode!"); } } @@ -314,24 +374,25 @@ public class SendFragment extends Fragment return null; } } + + @Override + public int getItemPosition(Object object) { + Timber.d("getItemPosition %s", String.valueOf(object)); + if ((object instanceof SendAddressWizardFragment) || (object instanceof SendSettingsWizardFragment)) { + return POSITION_UNCHANGED; + } else { + return POSITION_NONE; + } + } } @Override public TxData getTxData() { - return new TxData(sendAddress, sendPaymentId, sendAmount, sendMixin, sendPriority); + return txData; } - @Override - public String getNotes() { - return sendNotes; - } + private TxData txData = new TxData(); - private String sendAddress; - private String sendPaymentId; - private long sendAmount; - private PendingTransaction.Priority sendPriority; - private int sendMixin; - private String sendNotes; private BarcodeData barcodeData; // Listeners @@ -347,36 +408,6 @@ public class SendFragment extends Fragment return data; } - @Override - public void setAddress(final String address) { - sendAddress = address; - } - - @Override - public void setPaymentId(final String paymentId) { - sendPaymentId = paymentId; - } - - @Override - public void setAmount(final long amount) { - sendAmount = amount; - } - - @Override - public void setPriority(final PendingTransaction.Priority priority) { - sendPriority = priority; - } - - @Override - public void setMixin(final int mixin) { - sendMixin = mixin; - } - - @Override - public void setNotes(final String notes) { - sendNotes = notes; - } - boolean isComitted() { return committedTx != null; } @@ -391,9 +422,9 @@ public class SendFragment extends Fragment @Override public void commitTransaction() { - Timber.d("REALLY SEND A %s", getNotes()); + Timber.d("REALLY SEND"); disableNavigation(); // committed - disable all navigation - activityCallback.onSend(getNotes()); + activityCallback.onSend(txData.getUserNotes()); committedTx = pendingTx; } @@ -418,12 +449,11 @@ public class SendFragment extends Fragment // callbacks from send service - public void onTransactionCreated(PendingTransaction pendingTransaction) { - //public void onTransactionCreated(TestTransaction pendingTransaction) { - final SendConfirmWizardFragment confirmFragment = getConfirmFragment(); - if (confirmFragment != null) { + public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) { + final SendConfirm confirm = getSendConfirm(); + if (confirm != null) { pendingTx = new PendingTx(pendingTransaction); - confirmFragment.transactionCreated(pendingTransaction); + confirm.transactionCreated(txTag, pendingTransaction); } else { // not in confirm fragment => dispose & move on disposeTransaction(); @@ -443,23 +473,16 @@ public class SendFragment extends Fragment } public void onCreateTransactionFailed(String errorText) { - final SendConfirmWizardFragment confirmFragment = getConfirmFragment(); - if (confirmFragment != null) { - confirmFragment.hideProgress(); - - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setCancelable(true). - setTitle(getString(R.string.send_create_tx_error_title)). - setMessage(errorText). - create(). - show(); + final SendConfirm confirm = getSendConfirm(); + if (confirm != null) { + confirm.createTransactionFailed(errorText); } } - SendConfirmWizardFragment getConfirmFragment() { + SendConfirm getSendConfirm() { final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_CONFIRM); - if (fragment instanceof SendConfirmWizardFragment) { - return (SendConfirmWizardFragment) fragment; + if (fragment instanceof SendConfirm) { + return (SendConfirm) fragment; } else { return null; } @@ -469,8 +492,8 @@ public class SendFragment extends Fragment Timber.d("txid=%s", txId); pagerAdapter.addSuccess(); Timber.d("numPages=%d", spendViewPager.getAdapter().getCount()); - spendViewPager.setCurrentItem(SpendPagerAdapter.POS_SUCCESS); activityCallback.setToolbarButton(Toolbar.BUTTON_NONE); + spendViewPager.setCurrentItem(SpendPagerAdapter.POS_SUCCESS); } public void onSendTransactionFailed(final String error) { @@ -478,7 +501,7 @@ public class SendFragment extends Fragment committedTx = null; Toast.makeText(getContext(), getString(R.string.status_transaction_failed, error), Toast.LENGTH_SHORT).show(); enableNavigation(); - final SendConfirmWizardFragment fragment = getConfirmFragment(); + final SendConfirm fragment = getSendConfirm(); if (fragment != null) { fragment.sendFailed(); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendSettingsWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSettingsWizardFragment.java similarity index 86% rename from app/src/main/java/com/m2049r/xmrwallet/SendSettingsWizardFragment.java rename to app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSettingsWizardFragment.java index 8746c22..42da401 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SendSettingsWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSettingsWizardFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.m2049r.xmrwallet; +package com.m2049r.xmrwallet.fragment.send; import android.os.Bundle; import android.text.InputType; @@ -27,8 +27,11 @@ import android.widget.EditText; import android.widget.Spinner; import android.widget.TextView; +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.data.TxData; import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.UserNotes; import timber.log.Timber; @@ -48,12 +51,7 @@ public class SendSettingsWizardFragment extends SendWizardFragment { } interface Listener { - - void setPriority(final PendingTransaction.Priority priority); - - void setMixin(final int mixin); - - void setNotes(final String notes); + TxData getTxData(); } final static int Mixins[] = {4, 7, 12, 25}; // must match the layout XML @@ -103,13 +101,10 @@ public class SendSettingsWizardFragment extends SendWizardFragment { @Override public boolean onValidateFields() { if (sendListener != null) { - int mixin = Mixins[sMixin.getSelectedItemPosition()]; - int priorityIndex = sPriority.getSelectedItemPosition(); - PendingTransaction.Priority priority = Priorities[priorityIndex]; - sendListener.setPriority(priority); - sendListener.setMixin(mixin); - String notes = etNotes.getText().toString(); - sendListener.setNotes(notes); + TxData txData = sendListener.getTxData(); + txData.setPriority(Priorities[sPriority.getSelectedItemPosition()]); + txData.setMixin(Mixins[sMixin.getSelectedItemPosition()]); + txData.setUserNotes(new UserNotes(etNotes.getText().toString())); } return true; } diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendSuccessWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java similarity index 70% rename from app/src/main/java/com/m2049r/xmrwallet/SendSuccessWizardFragment.java rename to app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java index 9a21dcf..65cf20d 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SendSuccessWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendSuccessWizardFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.m2049r.xmrwallet; +package com.m2049r.xmrwallet.fragment.send; import android.os.Bundle; import android.view.LayoutInflater; @@ -24,10 +24,10 @@ import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; -import com.m2049r.xmrwallet.model.Wallet; -import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.data.PendingTx; import com.m2049r.xmrwallet.data.TxData; +import com.m2049r.xmrwallet.util.Helper; import timber.log.Timber; @@ -47,23 +47,21 @@ public class SendSuccessWizardFragment extends SendWizardFragment { } interface Listener { - String getNotes(); - TxData getTxData(); PendingTx getCommittedTx(); void enableDone(); + + SendFragment.Mode getMode(); } - ImageButton bCopyAddress; + ImageButton bCopyTxId; private TextView tvTxId; private TextView tvTxAddress; private TextView tvTxPaymentId; - private TextView tvTxNotes; private TextView tvTxAmount; private TextView tvTxFee; - private TextView tvTxTotal; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -74,21 +72,20 @@ public class SendSuccessWizardFragment extends SendWizardFragment { View view = inflater.inflate( R.layout.fragment_send_success, container, false); - bCopyAddress = (ImageButton) view.findViewById(R.id.bCopyAddress); - bCopyAddress.setOnClickListener(new View.OnClickListener() { + bCopyTxId = (ImageButton) view.findViewById(R.id.bCopyTxId); + bCopyTxId.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - copyAddress(); + Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString()); + Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show(); } }); tvTxId = (TextView) view.findViewById(R.id.tvTxId); tvTxAddress = (TextView) view.findViewById(R.id.tvTxAddress); tvTxPaymentId = (TextView) view.findViewById(R.id.tvTxPaymentId); - tvTxNotes = (TextView) view.findViewById(R.id.tvTxNotes); tvTxAmount = ((TextView) view.findViewById(R.id.tvTxAmount)); tvTxFee = (TextView) view.findViewById(R.id.tvTxFee); - tvTxTotal = (TextView) view.findViewById(R.id.tvTxTotal); return view; } @@ -117,30 +114,15 @@ public class SendSuccessWizardFragment extends SendWizardFragment { } else { tvTxPaymentId.setText("-"); } - String notes = sendListener.getNotes(); - if ((notes != null) && (!notes.isEmpty())) { - tvTxNotes.setText(sendListener.getNotes()); - } else { - tvTxNotes.setText("-"); - } final PendingTx committedTx = sendListener.getCommittedTx(); if (committedTx != null) { tvTxId.setText(committedTx.txId); - bCopyAddress.setEnabled(true); - bCopyAddress.setImageResource(R.drawable.ic_content_copy_black_24dp); - tvTxAmount.setText(Wallet.getDisplayAmount(committedTx.amount)); - tvTxFee.setText(Wallet.getDisplayAmount(committedTx.fee)); - //tvTxDust.setText(Wallet.getDisplayAmount(pendingTransaction.getDust())); - tvTxTotal.setText(Wallet.getDisplayAmount( - committedTx.fee + committedTx.amount)); + bCopyTxId.setEnabled(true); + bCopyTxId.setImageResource(R.drawable.ic_content_copy_black_24dp); + tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount))); + tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee))); } sendListener.enableDone(); } - - void copyAddress() { - Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString()); - Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show(); - } - } diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendWizardFragment.java similarity index 95% rename from app/src/main/java/com/m2049r/xmrwallet/SendWizardFragment.java rename to app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendWizardFragment.java index ae99bd7..0ef911d 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SendWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendWizardFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.m2049r.xmrwallet; +package com.m2049r.xmrwallet.fragment.send; import android.support.v4.app.Fragment; diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/SpendViewPager.java b/app/src/main/java/com/m2049r/xmrwallet/layout/SpendViewPager.java index 4fee6b3..1ddde3e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/layout/SpendViewPager.java +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/SpendViewPager.java @@ -21,7 +21,7 @@ import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.view.MotionEvent; -import com.m2049r.xmrwallet.SendFragment; +import com.m2049r.xmrwallet.fragment.send.SendFragment; public class SpendViewPager extends ViewPager { diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java index 8c4157c..b44d84d 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java @@ -22,11 +22,13 @@ import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.model.TransactionInfo; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.UserNotes; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -101,6 +103,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter 0)) { String fee = Helper.getDisplayAmount(infoItem.fee, 5); - this.tvFee.setText(context.getString(R.string.tx_list_fee, fee)); + tvFee.setText(context.getString(R.string.tx_list_fee, fee)); + tvFee.setVisibility(View.VISIBLE); } else { - this.tvFee.setText(""); + tvFee.setText(""); + tvFee.setVisibility(View.GONE); } if (infoItem.isFailed) { this.tvAmount.setText(context.getString(R.string.tx_list_amount_failed, displayAmount)); this.tvFee.setText(context.getString(R.string.tx_list_failed_text)); + tvFee.setVisibility(View.VISIBLE); setTxColour(failedColour); } else if (infoItem.isPending) { setTxColour(pendingColour); @@ -156,11 +162,20 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter 35) + return false; + byte[] decoded = decodeBase58To25Bytes(addrress); + if (decoded == null) + return false; + + if (!testnet) { + if ((decoded[0] != 0x00) && (decoded[0] != 0x05)) return false; + } else { + if ((decoded[0] != 0x6f) && (decoded[0] != 0xc4)) 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(); + 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); + } + } +} \ 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 ec05c68..118b7b0 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -38,6 +38,7 @@ 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; @@ -49,6 +50,7 @@ import java.util.Locale; import javax.net.ssl.HttpsURLConnection; +import okhttp3.HttpUrl; import timber.log.Timber; public class Helper { @@ -160,7 +162,7 @@ public class Helper { } static public String getDisplayAmount(long amount) { - return getDisplayAmount(amount, 20); + return getDisplayAmount(amount, 12); } static public String getDisplayAmount(long amount, int maxDecimals) { @@ -271,4 +273,11 @@ public class Helper { return ShakeAnimation; } + static public HttpUrl getXmrToBaseUrl() { + if ((WalletManager.getInstance() == null) || WalletManager.getInstance().isTestNet()) { + return HttpUrl.parse("https://test.xmr.to/api/v2/xmr2btc/"); + } else { + return HttpUrl.parse("https://xmr.to/api/v2/xmr2btc/"); + } + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/UserNotes.java b/app/src/main/java/com/m2049r/xmrwallet/util/UserNotes.java new file mode 100644 index 0000000..e6e463c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/UserNotes.java @@ -0,0 +1,90 @@ +/* + * 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 com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import timber.log.Timber; + +public class UserNotes { + public String txNotes = ""; + public String note = ""; + public String xmrtoKey = null; + public String xmrtoAmount = null; // could be a double - but we are not doing any calculations + public String xmrtoDestination = null; + + public UserNotes(final String txNotes) { + if (txNotes == null) { + return; + } + this.txNotes = txNotes; + Pattern p = Pattern.compile("^\\{(xmrto-\\w{6}),([0-9.]*)BTC,(\\w*)\\} ?(.*)"); + Matcher m = p.matcher(txNotes); + if (m.find()) { + xmrtoKey = m.group(1); + xmrtoAmount = m.group(2); + xmrtoDestination = m.group(3); + note = m.group(4); + } else { + note = txNotes; + } + } + + public void setNote(String newNote) { + if (newNote != null) { + note = newNote; + } else { + note = ""; + } + txNotes = buildTxNote(); + } + + public void setXmrtoStatus(QueryOrderStatus xmrtoStatus) { + if (xmrtoStatus != null) { + xmrtoKey = xmrtoStatus.getUuid(); + xmrtoAmount = String.valueOf(xmrtoStatus.getBtcAmount()); + xmrtoDestination = xmrtoStatus.getBtcDestAddress(); + } else { + xmrtoKey = null; + xmrtoAmount = null; + xmrtoDestination = null; + } + txNotes = buildTxNote(); + } + + private String buildTxNote() { + StringBuffer sb = new StringBuffer(); + if (xmrtoKey != null) { + if ((xmrtoAmount == null) || (xmrtoDestination == null)) + throw new IllegalArgumentException("Broken notes"); + sb.append("{"); + sb.append(xmrtoKey); + sb.append(","); + sb.append(xmrtoAmount); + sb.append("BTC,"); + sb.append(xmrtoDestination); + sb.append("}"); + if ((note != null) && (!note.isEmpty())) + sb.append(" "); + } + sb.append(note); + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeBtcTextView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeBtcTextView.java new file mode 100644 index 0000000..5a86ed1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeBtcTextView.java @@ -0,0 +1,198 @@ +/* + * 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. + */ + +// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889 + +package com.m2049r.xmrwallet.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.util.Helper; + +import timber.log.Timber; + +public class ExchangeBtcTextView extends LinearLayout + implements NumberPadView.NumberPadListener { + + String btcAmount = null; + String xmrAmount = null; + + private boolean validate(String amount, double max, double min) { + boolean ok = true; + if (amount != null) { + try { + double x = Double.parseDouble(amount); + if ((x < min) || (x > max)) { + ok = false; + } + } catch (NumberFormatException ex) { + Timber.e(ex.getLocalizedMessage()); + ok = false; + } + } else { + ok = false; + } + return ok; + } + + public boolean validate(double maxBtc, double minBtc) { + Timber.d("validate(maxBtc=%f,minBtc=%f)", maxBtc, minBtc); + boolean ok = true; + if (!validate(btcAmount, maxBtc, minBtc)) { + Timber.d("btcAmount invalid %s", btcAmount); + shakeAmountField(); + return false; + } + return true; + } + + void shakeAmountField() { + tvAmountA.startAnimation(Helper.getShakeAnimation(getContext())); + } + + void shakeExchangeField() { + tvAmountB.startAnimation(Helper.getShakeAnimation(getContext())); + } + + public void setRate(double xmrBtcRate) { + this.xmrBtcRate = xmrBtcRate; + post(new Runnable() { + @Override + public void run() { + exchange(); + } + }); + } + + public void setAmount(String btcAmount) { + this.btcAmount = btcAmount; + tvAmountA.setText(btcAmount); + xmrAmount = null; + exchange(); + } + + public String getAmount() { + return btcAmount; + } + + TextView tvAmountA; + TextView tvAmountB; + Spinner sCurrencyA; + Spinner sCurrencyB; + + public ExchangeBtcTextView(Context context) { + super(context); + initializeViews(context); + } + + public ExchangeBtcTextView(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public ExchangeBtcTextView(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + /** + * Inflates the views in the layout. + * + * @param context the current context for the view. + */ + private void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_exchange_btc_text, this); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + tvAmountA = (TextView) findViewById(R.id.tvAmountA); + tvAmountB = (TextView) findViewById(R.id.tvAmountB); + sCurrencyA = (Spinner) findViewById(R.id.sCurrencyA); + sCurrencyB = (Spinner) findViewById(R.id.sCurrencyB); + + ArrayAdapter btcAdapter = new ArrayAdapter(getContext(), + android.R.layout.simple_spinner_item, + new String[]{"BTC"}); + sCurrencyA.setAdapter(btcAdapter); + sCurrencyA.setEnabled(false); + ArrayAdapter xmrAdapter = new ArrayAdapter(getContext(), + android.R.layout.simple_spinner_item, + new String[]{"XMR"}); + sCurrencyB.setAdapter(xmrAdapter); + sCurrencyB.setEnabled(false); + } + + double xmrBtcRate = 0; + + public void exchange() { + btcAmount = tvAmountA.getText().toString(); + if (!btcAmount.isEmpty() && (xmrBtcRate > 0)) { + double xmr = xmrBtcRate * Double.parseDouble(btcAmount); + xmrAmount = Helper.getFormattedAmount(xmr, true); + } else { + xmrAmount = ""; + } + tvAmountB.setText(getResources().getString(R.string.send_amount_btc_xmr, xmrAmount)); + Timber.d("%s BTC =%f> %s XMR", btcAmount, xmrBtcRate, xmrAmount); + } + + // deal with attached numpad + @Override + public void onDigitPressed(final int digit) { + tvAmountA.append(String.valueOf(digit)); + exchange(); + } + + @Override + public void onPointPressed() { + //TODO locale? + if (tvAmountA.getText().toString().indexOf('.') == -1) { + if (tvAmountA.getText().toString().isEmpty()) { + tvAmountA.append("0"); + } + tvAmountA.append("."); + } + } + + @Override + public void onBackSpacePressed() { + String entry = tvAmountA.getText().toString(); + int length = entry.length(); + if (length > 0) { + tvAmountA.setText(entry.substring(0, entry.length() - 1)); + exchange(); + } + } + + @Override + public void onClearAll() { + tvAmountA.setText(null); + exchange(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeTextView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeTextView.java index 1a6edbf..25a1225 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeTextView.java +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/ExchangeTextView.java @@ -436,6 +436,9 @@ public class ExchangeTextView extends LinearLayout public void onPointPressed() { //TODO locale? if (tvAmountA.getText().toString().indexOf('.') == -1) { + if (tvAmountA.getText().toString().isEmpty()) { + tvAmountA.append("0"); + } tvAmountA.append("."); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java b/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java new file mode 100644 index 0000000..1ffcdab --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/widget/SendProgressView.java @@ -0,0 +1,87 @@ +/* + * 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.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.m2049r.xmrwallet.R; + +public class SendProgressView extends LinearLayout { + + public SendProgressView(Context context, AttributeSet attrs) { + super(context, attrs); + initializeViews(context); + } + + public SendProgressView(Context context, + AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + initializeViews(context); + } + + private void initializeViews(Context context) { + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.view_send_progress, this); + } + + + View pbProgress; + View llMessage; + TextView tvCode; + TextView tvMessage; + TextView tvSolution; + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + pbProgress = findViewById(R.id.pbProgress); + llMessage = findViewById(R.id.llMessage); + tvCode = (TextView) findViewById(R.id.tvCode); + tvMessage = (TextView) findViewById(R.id.tvMessage); + tvSolution = (TextView) findViewById(R.id.tvSolution); + } + + public void showProgress(String progressText) { + pbProgress.setVisibility(VISIBLE); + tvCode.setVisibility(INVISIBLE); + tvMessage.setText(progressText); + llMessage.setVisibility(VISIBLE); + tvSolution.setVisibility(INVISIBLE); + } + + public void hideProgress() { + pbProgress.setVisibility(INVISIBLE); + llMessage.setVisibility(INVISIBLE); + } + + public void showMessage(String code, String message, String solution) { + tvCode.setText(code); + tvMessage.setText(message); + tvSolution.setText(solution); + tvCode.setVisibility(VISIBLE); + llMessage.setVisibility(VISIBLE); + tvSolution.setVisibility(VISIBLE); + pbProgress.setVisibility(INVISIBLE); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/XmrToError.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/XmrToError.java new file mode 100644 index 0000000..2ac5bc2 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/XmrToError.java @@ -0,0 +1,107 @@ +/* + * 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.xmrto; + +import org.json.JSONException; +import org.json.JSONObject; + +public class XmrToError { + private final Error errorId; + private final String errorMsg; + + public enum Error { + XMRTO_ERROR_UNDEF, + XMRTO_ERROR_001, // (503) internal services not available try again later + XMRTO_ERROR_002, // (400) malformed bitcoin address check address validity + XMRTO_ERROR_003, // (400) invalid bitcoin amount check amount data type + XMRTO_ERROR_004, // (400) bitcoin amount out of bounds check min and max amount + XMRTO_ERROR_005, // (400) unexpected validation error contact support + XMRTO_ERROR_006, // (404) requested order not found check order UUID + XMRTO_ERROR_007, // (503) third party service not available try again later + XMRTO_ERROR_008, // (503) insufficient funds available try again later + XMRTO_ERROR_009 // (400) invalid request check request parameters + } + + public boolean isRetryable() { + return (errorId == Error.XMRTO_ERROR_001) + || (errorId == Error.XMRTO_ERROR_007) + || (errorId == Error.XMRTO_ERROR_008); + } + + public static String getErrorIdString(Error anError) { + return anError.toString().replace('_', '-'); + } + + // mostly for testing + public XmrToError(String errorId, String message) { + Error error; + try { + error = Error.valueOf(errorId.replace('-', '_')); + } catch (IllegalArgumentException ex) { + error = Error.XMRTO_ERROR_UNDEF; + } + this.errorId = error; + this.errorMsg = message; + } + + public XmrToError(final JSONObject jsonObject) throws JSONException { + final String errorId = jsonObject.getString("error"); + Error error; + try { + error = Error.valueOf(errorId.replace('-', '_')); + } catch (IllegalArgumentException ex) { + error = Error.XMRTO_ERROR_UNDEF; + } + this.errorId = error; + this.errorMsg = jsonObject.getString("error_msg"); + } + + public Error getErrorId() { + return errorId; + } + + public String getErrorMsg() { + return errorMsg; + } + + @Override + public String toString() { + return getErrorIdString(getErrorId()) + ": " + getErrorMsg(); + } + + /* Errors from Query Parameters + HTTP code XMR.TO error code Error message/reason Solution + 503 XMRTO-ERROR-001 internal services not available try again later + 503 XMRTO-ERROR-007 third party service not available try again later + 503 XMRTO-ERROR-008 insufficient funds available try again later + */ + + /* Errors from Create Order + HTTP code XMR.TO error code Error message/reason Solution + 503 XMRTO-ERROR-001 internal services not available try again later + 400 XMRTO-ERROR-002 malformed bitcoin address check address validity + 400 XMRTO-ERROR-003 invalid bitcoin amount check amount data type + 503 XMRTO-ERROR-004 bitcoin amount out of bounds check min and max amount + 400 XMRTO-ERROR-005 unexpected validation error contact support + */ + + /* Errors from Query Order + HTTP code XMR.TO error code Error message/reason Solution + 400 XMRTO-ERROR-009 invalid request check request parameters + 404 XMRTO-ERROR-006 requested order not found check order UUID + */ +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/XmrToException.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/XmrToException.java new file mode 100644 index 0000000..05f810e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/XmrToException.java @@ -0,0 +1,42 @@ +/* + * 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.xmrto; + +public class XmrToException extends Exception { + private int code; + private XmrToError error; + + public XmrToException(final int code) { + super(); + this.code = code; + this.error = null; + } + + public XmrToException(final int code, final XmrToError error) { + super(); + this.code = code; + this.error = error; + } + + public int getCode() { + return code; + } + + public XmrToError getError() { + return error; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/CreateOrder.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/CreateOrder.java new file mode 100644 index 0000000..b03abe7 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/CreateOrder.java @@ -0,0 +1,28 @@ +/* + * 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.xmrto.api; + +public interface CreateOrder { + Double getBtcAmount(); + + String getBtcDestAddress(); + + String getState(); + + String getUuid(); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/QueryOrderParameters.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/QueryOrderParameters.java new file mode 100644 index 0000000..e27c458 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/QueryOrderParameters.java @@ -0,0 +1,31 @@ +/* + * 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.xmrto.api; + +public interface QueryOrderParameters { + + double getLowerLimit(); // "lower_limit": , + + double getPrice(); // "price": , + + double getUpperLimit(); // "upper_limit": , + + boolean isZeroConfEnabled(); // "zero_conf_enabled": , + + double getZeroConfMaxAmount(); // "zero_conf_max_amount": + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/QueryOrderStatus.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/QueryOrderStatus.java new file mode 100644 index 0000000..21603a9 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/QueryOrderStatus.java @@ -0,0 +1,87 @@ +/* + * 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.xmrto.api; + +import java.util.Date; + +public interface QueryOrderStatus { + enum State { + UNDEF, + TO_BE_CREATED, // order creation pending + UNPAID, // waiting for Monero payment by user + UNDERPAID, // order partially paid + PAID_UNCONFIRMED, // order paid, waiting for enough confirmations + PAID, // order paid and sufficiently confirmed + BTC_SENT, // bitcoin payment sent + TIMED_OUT, // order timed out before payment was complete + NOT_FOUND // order wasn’t found in system (never existed or was purged) + } + + boolean isCreated(); + + boolean isTerminal(); + + boolean isPending(); + + boolean isPaid(); + + boolean isSent(); + + boolean isError(); + + State getState(); // "state": "", + + double getBtcAmount(); // "btc_amount": , + + String getBtcDestAddress(); // "btc_dest_address": "", + + String getUuid(); // "uuid": "" + + int getBtcNumConfirmations(); // "btc_num_confirmations": , + + int getBtcNumConfirmationsBeforePurge(); // "btc_num_confirmations_before_purge": , + + String getBtcTransactionId(); // "btc_transaction_id": "", + + Date getCreatedAt(); // "created_at": "", + + Date getExpiresAt(); // "expires_at": "", + + int getSecondsTillTimeout(); // "seconds_till_timeout": , + + double getXmrAmountTotal(); // "xmr_amount_total": , + + double getXmrAmountRemaining(); // "xmr_amount_remaining": , + + int getXmrNumConfirmationsRemaining(); // "xmr_num_confirmations_remaining": , + + double getXmrPriceBtc(); // "xmr_price_btc": , + + String getXmrReceivingAddress(); // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string", + + String getXmrReceivingIntegratedAddress(); // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string", + + int getXmrRecommendedMixin(); // "xmr_recommended_mixin": , + + @Deprecated + double getXmrRequiredAmount(); // "xmr_required_amount": , + + String getXmrRequiredPaymentIdLong(); // "xmr_required_payment_id_long": "xmr_payment_id_user_needs_to_include_when_using_old_stlye_address_as_string" + + String getXmrRequiredPaymentIdShort(); // "xmr_required_payment_id_short": "xmr_payment_id_included_in_integrated_address_as_string" + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/XmrToApi.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/XmrToApi.java new file mode 100644 index 0000000..daaf071 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/XmrToApi.java @@ -0,0 +1,46 @@ +/* + * 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.xmrto.api; + +import android.support.annotation.NonNull; + +public interface XmrToApi { + + /** + * Queries the order parameter. + * + * @param callback the callback with the OrderParameter object + */ + void queryOrderParameters(@NonNull final XmrToCallback callback); + + /** + * Creates an order + * + * @param amount the desired amount + * @param address the target bitcoin address + */ + void createOrder(final double amount, @NonNull final String address, @NonNull final XmrToCallback callback); + + /** + * Queries the order status for given current order + * + * @param uuid the order uuid + * @param callback the callback with the OrderStatus object + */ + void queryOrderStatus(@NonNull final String uuid, @NonNull final XmrToCallback callback); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/XmrToCallback.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/XmrToCallback.java new file mode 100644 index 0000000..1758363 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/api/XmrToCallback.java @@ -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.xmrto.api; + +public interface XmrToCallback { + + void onSuccess(T t); + + void onError(Exception ex); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/CreateOrderImpl.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/CreateOrderImpl.java new file mode 100644 index 0000000..f6faf98 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/CreateOrderImpl.java @@ -0,0 +1,89 @@ +/* + * 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.xmrto.network; + +import android.support.annotation.NonNull; + +import com.m2049r.xmrwallet.xmrto.api.XmrToCallback; +import com.m2049r.xmrwallet.xmrto.api.CreateOrder; + +import org.json.JSONException; +import org.json.JSONObject; + +class CreateOrderImpl implements CreateOrder { + + private final String state; + private final double btcAmount; + private final String btcDestAddress; + private final String uuid; + + public Double getBtcAmount() { + return btcAmount; + } + + public String getBtcDestAddress() { + return btcDestAddress; + } + + public String getUuid() { + return uuid; + } + + public String getState() { + return state; + } + + CreateOrderImpl(final JSONObject jsonObject) throws JSONException { + this.state = jsonObject.getString("state"); + this.btcAmount = jsonObject.getDouble("btc_amount"); + this.btcDestAddress = jsonObject.getString("btc_dest_address"); + this.uuid = jsonObject.getString("uuid"); + } + + public static void call(@NonNull final XmrToApiCall api, final double amount, @NonNull final String address, + @NonNull final XmrToCallback callback) { + try { + final JSONObject request = createRequest(amount, address); + api.call("order_create", 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 double amount, final String address) throws JSONException { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("btc_amount", amount); + jsonObject.put("btc_dest_address", address); + return jsonObject; + } + + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/NetworkCallback.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/NetworkCallback.java new file mode 100644 index 0000000..ba7caab --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/NetworkCallback.java @@ -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.xmrto.network; + +import org.json.JSONObject; + +interface NetworkCallback { + + void onSuccess(JSONObject jsonObject); + + void onError(Exception ex); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/QueryOrderParametersImpl.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/QueryOrderParametersImpl.java new file mode 100644 index 0000000..dc413e5 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/QueryOrderParametersImpl.java @@ -0,0 +1,82 @@ +/* + * 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.xmrto.network; + +import android.support.annotation.NonNull; + +import com.m2049r.xmrwallet.xmrto.api.XmrToCallback; +import com.m2049r.xmrwallet.xmrto.api.QueryOrderParameters; + +import org.json.JSONException; +import org.json.JSONObject; + +class QueryOrderParametersImpl implements QueryOrderParameters { + + private double lowerLimit; // "lower_limit": , + private double price; // "price": , + private double upperLimit; // "upper_limit": , + private boolean isZeroConfEnabled; // "zero_conf_enabled": , + private double zeroConfMaxAmount; // "zero_conf_max_amount": + + public double getLowerLimit() { + return lowerLimit; + } + + public double getPrice() { + return price; + } + + public double getUpperLimit() { + return upperLimit; + } + + public boolean isZeroConfEnabled() { + return isZeroConfEnabled; + } + + public double getZeroConfMaxAmount() { + return zeroConfMaxAmount; + } + + QueryOrderParametersImpl(final JSONObject jsonObject) throws JSONException { + lowerLimit = jsonObject.getDouble("lower_limit"); + price = jsonObject.getDouble("price"); + upperLimit = jsonObject.getDouble("upper_limit"); + isZeroConfEnabled = jsonObject.getBoolean("zero_conf_enabled"); + zeroConfMaxAmount = jsonObject.getDouble("zero_conf_max_amount"); + } + + public static void call(@NonNull final XmrToApiCall api, + @NonNull final XmrToCallback callback) { + api.call("order_parameter_query", 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); + } + }); + } + //{"zero_conf_enabled":true,"price":0.015537,"upper_limit":20.0,"lower_limit":0.001,"zero_conf_max_amount":0.1} +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/QueryOrderStatusImpl.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/QueryOrderStatusImpl.java new file mode 100644 index 0000000..91b721d --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/QueryOrderStatusImpl.java @@ -0,0 +1,255 @@ +/* + * 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.xmrto.network; + +import android.support.annotation.NonNull; + +import com.m2049r.xmrwallet.xmrto.api.XmrToCallback; +import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import timber.log.Timber; + +class QueryOrderStatusImpl implements QueryOrderStatus { + public static final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + + public static Date parseDate(String dateString) throws ParseException { + return DATETIME_FORMATTER.parse(dateString.replaceAll("Z$", "+0000")); + } + + private QueryOrderStatus.State state; // "state": "", + private double btcAmount; // "btc_amount": , + private String btcDestAddress; // "btc_dest_address": "", + private String uuid; // "uuid": "" + // the following are only returned if the state is "after" TO_BE_CREATED + private int btcNumConfirmations; // "btc_num_confirmations": , + private int btcNumConfirmationsBeforePurge; // "btc_num_confirmations_before_purge": , + private String btcTransactionId; // "btc_transaction_id": "", + private Date createdAt; // "created_at": "", + private Date expiresAt; // "expires_at": "", + private int secondsTillTimeout; // "seconds_till_timeout": , + private double xmrAmountTotal; // "xmr_amount_total": , + private double xmrAmountRemaining; // "xmr_amount_remaining": , + private int xmrNumConfirmationsRemaining; // "xmr_num_confirmations_remaining": , + private double xmrPriceBtc; // "xmr_price_btc": , + private String xmrReceivingAddress; // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string", + private String xmrReceivingIntegratedAddress; // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string", + private int xmrRecommendedMixin; // "xmr_recommended_mixin": , + private double xmrRequiredAmount; // "xmr_required_amount": , + private String xmrRequiredPaymentIdLong; // "xmr_required_payment_id_long": "xmr_payment_id_user_needs_to_include_when_using_old_stlye_address_as_string" + private String xmrRequiredPaymentIdShort; // "xmr_required_payment_id_short": "xmr_payment_id_included_in_integrated_address_as_string" + + public QueryOrderStatus.State getState() { + return state; + } + + public double getBtcAmount() { + return btcAmount; + } + + public String getBtcDestAddress() { + return btcDestAddress; + } + + public String getUuid() { + return uuid; + } + + public int getBtcNumConfirmations() { + return btcNumConfirmations; + } + + public int getBtcNumConfirmationsBeforePurge() { + return btcNumConfirmationsBeforePurge; + } + + public String getBtcTransactionId() { + return btcTransactionId; + } + + public Date getCreatedAt() { + return createdAt; + } + + public Date getExpiresAt() { + return expiresAt; + } + + public int getSecondsTillTimeout() { + return secondsTillTimeout; + } + + public double getXmrAmountTotal() { + return xmrAmountTotal; + } + + public double getXmrAmountRemaining() { + return xmrAmountRemaining; + } + + public int getXmrNumConfirmationsRemaining() { + return xmrNumConfirmationsRemaining; + } + + public double getXmrPriceBtc() { + return xmrPriceBtc; + } + + public String getXmrReceivingAddress() { + return xmrReceivingAddress; + } + + public String getXmrReceivingIntegratedAddress() { + return xmrReceivingIntegratedAddress; + } + + public int getXmrRecommendedMixin() { + return xmrRecommendedMixin; + } + + public double getXmrRequiredAmount() { + return xmrRequiredAmount; + } + + public String getXmrRequiredPaymentIdLong() { + return xmrRequiredPaymentIdLong; + } + + public String getXmrRequiredPaymentIdShort() { + return xmrRequiredPaymentIdShort; + } + + public boolean isCreated() { + return (state.equals(State.UNPAID) || + state.equals(State.BTC_SENT) || + state.equals(State.PAID) || + state.equals(State.PAID_UNCONFIRMED) || + state.equals(State.UNDERPAID)); + } + + public boolean isTerminal() { + return (state.equals(State.BTC_SENT) || isError()); + } + + + public boolean isError() { + return (state.equals(State.UNDEF) || + state.equals(State.NOT_FOUND) || + state.equals(State.TIMED_OUT)); + } + + public boolean isPending() { + return (state.equals(State.TO_BE_CREATED) || + state.equals(State.UNPAID) || + state.equals(State.UNDERPAID) || + state.equals(State.PAID_UNCONFIRMED) || + state.equals(State.PAID)); + } + + public boolean isPaid() { + return (state.equals(State.PAID_UNCONFIRMED) || + state.equals(State.PAID)); + } + + public boolean isSent() { + return state.equals(State.BTC_SENT); + } + + + QueryOrderStatusImpl(final JSONObject jsonObject) throws JSONException { + Timber.d(jsonObject.toString(4)); + String stateName = jsonObject.getString("state"); // "state": "", + State state; + try { + state = State.valueOf(stateName); + } catch (IllegalArgumentException ex) { + state = State.UNDEF; //TODO: throws IllegalArgumentException? + } + this.state = state; + + btcAmount = jsonObject.getDouble("btc_amount"); // "btc_amount": , + btcDestAddress = jsonObject.getString("btc_dest_address"); // "btc_dest_address": "", + uuid = jsonObject.getString("uuid"); // "uuid": "" + + if (isCreated()) { + btcNumConfirmations = jsonObject.getInt("btc_num_confirmations"); // "btc_num_confirmations": , + btcNumConfirmationsBeforePurge = jsonObject.getInt("btc_num_confirmations_before_purge"); // "btc_num_confirmations_before_purge": , + btcTransactionId = jsonObject.getString("btc_transaction_id"); // "btc_transaction_id": "", + try { + String created = jsonObject.getString("created_at"); // "created_at": "", + createdAt = parseDate(created); + String expires = jsonObject.getString("expires_at"); // "expires_at": "", + expiresAt = parseDate(expires); + } catch (ParseException ex) { + throw new JSONException(ex.getLocalizedMessage()); + } + secondsTillTimeout = jsonObject.getInt("seconds_till_timeout"); // "seconds_till_timeout": , + xmrAmountTotal = jsonObject.getDouble("xmr_amount_total"); // "xmr_amount_total": , + xmrAmountRemaining = jsonObject.getDouble("xmr_amount_remaining"); // "xmr_amount_remaining": , + xmrNumConfirmationsRemaining = jsonObject.getInt("xmr_num_confirmations_remaining"); // "xmr_num_confirmations_remaining": , + xmrPriceBtc = jsonObject.getDouble("xmr_price_btc"); // "xmr_price_btc": , + xmrReceivingAddress = jsonObject.getString("xmr_receiving_address"); // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string", + xmrReceivingIntegratedAddress = jsonObject.getString("xmr_receiving_integrated_address"); // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string", + xmrRecommendedMixin = jsonObject.getInt("xmr_recommended_mixin"); // "xmr_recommended_mixin": , + xmrRequiredAmount = jsonObject.getDouble("xmr_required_amount"); // "xmr_required_amount": , + xmrRequiredPaymentIdLong = jsonObject.getString("xmr_required_payment_id_long"); // "xmr_required_payment_id_long": "xmr_payment_id_user_needs_to_include_when_using_old_stlye_address_as_string" + xmrRequiredPaymentIdShort = jsonObject.getString("xmr_required_payment_id_short"); // "xmr_required_payment_id_short": "xmr_payment_id_included_in_integrated_address_as_string" + } + } + + public static void call(@NonNull final XmrToApiCall api, @NonNull final String uuid, + @NonNull final XmrToCallback callback) { + try { + final JSONObject request = createRequest(uuid); + api.call("order_status_query", request, 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); + } + }); + } catch (JSONException ex) { + callback.onError(ex); + } + } + + /** + * Create JSON request object + * + * @param uuid unique_order_identifier_as_12_character_string + */ + + static JSONObject createRequest(final String uuid) throws JSONException { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("uuid", uuid); + return jsonObject; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiCall.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiCall.java new file mode 100644 index 0000000..b4816a1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiCall.java @@ -0,0 +1,28 @@ +/* + * 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.xmrto.network; + +import android.support.annotation.NonNull; + +import org.json.JSONObject; + +interface XmrToApiCall { + + void call(@NonNull final String path, @NonNull final NetworkCallback callback); + + void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback); +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiImpl.java b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiImpl.java new file mode 100644 index 0000000..8d8946b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/xmrto/network/XmrToApiImpl.java @@ -0,0 +1,143 @@ +/* + * 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.xmrto.network; + +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; + +import com.m2049r.xmrwallet.xmrto.api.XmrToCallback; +import com.m2049r.xmrwallet.xmrto.XmrToError; +import com.m2049r.xmrwallet.xmrto.XmrToException; +import com.m2049r.xmrwallet.xmrto.api.CreateOrder; +import com.m2049r.xmrwallet.xmrto.api.QueryOrderParameters; +import com.m2049r.xmrwallet.xmrto.api.QueryOrderStatus; +import com.m2049r.xmrwallet.xmrto.api.XmrToApi; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import timber.log.Timber; + +public class XmrToApiImpl implements XmrToApi, XmrToApiCall { + + @NonNull + private final OkHttpClient okHttpClient; + + private final HttpUrl baseUrl; + + public XmrToApiImpl(@NonNull final OkHttpClient okHttpClient, final HttpUrl baseUrl) { + this.okHttpClient = okHttpClient; + this.baseUrl = baseUrl; + } + + public XmrToApiImpl(@NonNull final OkHttpClient okHttpClient) { + this(okHttpClient, HttpUrl.parse("https://xmr.to/api/v2/xmr2btc/")); + } + + + @Override + public void createOrder(final double amount, @NonNull final String address, + @NonNull final XmrToCallback callback) { + CreateOrderImpl.call(this, amount, address, callback); + } + + @Override + public void queryOrderStatus(@NonNull final String uuid, + @NonNull final XmrToCallback callback) { + QueryOrderStatusImpl.call(this, uuid, callback); + } + + @Override + public void queryOrderParameters(@NonNull final XmrToCallback callback) { + QueryOrderParametersImpl.call(this, callback); + } + + + @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() + .addPathSegment(path) + .addPathSegment("") // xmr.to needs a trailing slash! + .build(); + + Timber.d(url.toString()); + final Request httpRequest = createHttpRequest(request, url); + Timber.d(httpRequest.toString()); + Timber.d(request == null ? "null request" : request.toString()); + + okHttpClient.newCall(httpRequest).enqueue(new okhttp3.Callback() { + @Override + public void onFailure(final Call call, final IOException ex) { + Timber.d("A"); + callback.onError(ex); + } + + @Override + public void onResponse(final Call call, 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()); + final XmrToError error = new XmrToError(json); + Timber.e("xmr.to says %d/%s", response.code(), error.toString()); + callback.onError(new XmrToException(response.code(), error)); + } catch (JSONException ex) { + callback.onError(new XmrToException(response.code())); + } + } + } + }); + } + + private Request createHttpRequest(final JSONObject request, final HttpUrl url) { + if (request != null) { + final RequestBody body = RequestBody.create( + MediaType.parse("application/json"), request.toString()); + + return new Request.Builder() + .url(url) + .post(body) + .build(); + } else { + return new Request.Builder() + .url(url) + .get() + .build(); + } + } +} diff --git a/app/src/main/res/drawable-v24/ic_check_circle_xmr.xml b/app/src/main/res/drawable-v24/ic_check_circle_xmr.xml new file mode 100644 index 0000000..7458d13 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_check_circle_xmr.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/gunther_24dp.png b/app/src/main/res/drawable/gunther_24dp.png new file mode 100644 index 0000000..33e0c0f Binary files /dev/null and b/app/src/main/res/drawable/gunther_24dp.png differ diff --git a/app/src/main/res/drawable/ic_arrow_right_orange_24dp.xml b/app/src/main/res/drawable/ic_arrow_right_orange_24dp.xml new file mode 100644 index 0000000..89be915 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_orange_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle_xmr.xml b/app/src/main/res/drawable/ic_check_circle_xmr.xml new file mode 100644 index 0000000..c48c1ee --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_xmr.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_content_copy_black_24dp.xml b/app/src/main/res/drawable/ic_content_copy_black_24dp.xml index 53a5858..48836c4 100644 --- a/app/src/main/res/drawable/ic_content_copy_black_24dp.xml +++ b/app/src/main/res/drawable/ic_content_copy_black_24dp.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + android:fillColor="#FF000000" + android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" /> diff --git a/app/src/main/res/drawable/ic_content_copy_white_24dp.xml b/app/src/main/res/drawable/ic_content_copy_white_24dp.xml new file mode 100644 index 0000000..127c9d3 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_error_red_24dp.xml b/app/src/main/res/drawable/ic_error_red_24dp.xml new file mode 100644 index 0000000..5b0e5c8 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_red_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_right_black_24dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_right_black_24dp.xml deleted file mode 100644 index a3d1622..0000000 --- a/app/src/main/res/drawable/ic_keyboard_arrow_right_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_logo_horizontol_xmrujo.xml b/app/src/main/res/drawable/ic_logo_horizontol_xmrujo.xml index cf1dde3..cd2b3ec 100644 --- a/app/src/main/res/drawable/ic_logo_horizontol_xmrujo.xml +++ b/app/src/main/res/drawable/ic_logo_horizontol_xmrujo.xml @@ -24,9 +24,6 @@ - diff --git a/app/src/main/res/drawable/ic_pending_orange_24dp.xml b/app/src/main/res/drawable/ic_pending_orange_24dp.xml new file mode 100644 index 0000000..68bb8e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_pending_orange_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_success_green_24dp.xml b/app/src/main/res/drawable/ic_success_green_24dp.xml new file mode 100644 index 0000000..8b33e98 --- /dev/null +++ b/app/src/main/res/drawable/ic_success_green_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_xmrto_32dp.png b/app/src/main/res/drawable/ic_xmrto_32dp.png new file mode 100644 index 0000000..b9cec90 Binary files /dev/null and b/app/src/main/res/drawable/ic_xmrto_32dp.png differ diff --git a/app/src/main/res/drawable/ic_xmrto_whitestroke_24px.png b/app/src/main/res/drawable/ic_xmrto_whitestroke_24px.png new file mode 100644 index 0000000..a1650c3 Binary files /dev/null and b/app/src/main/res/drawable/ic_xmrto_whitestroke_24px.png differ diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index d46dbd2..7f2e426 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -17,6 +17,13 @@ android:layout_height="48dp" android:src="@mipmap/ic_launcher" /> + + diff --git a/app/src/main/res/layout/fragment_send.xml b/app/src/main/res/layout/fragment_send.xml index 6d2a3c0..610db21 100644 --- a/app/src/main/res/layout/fragment_send.xml +++ b/app/src/main/res/layout/fragment_send.xml @@ -277,7 +277,7 @@ android:layout_marginTop="4dp" android:enabled="false" android:minHeight="36dp" - android:text="@string/send_send_hint" /> + android:text="@string/send_send_label" />