You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Wonerujo/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java

515 lines
20 KiB

/*
* 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.content.Context;
import android.nfc.NfcManager;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.Patterns;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.google.android.material.textfield.TextInputLayout;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.OpenAliasHelper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import timber.log.Timber;
public class SendAddressWizardFragment extends SendWizardFragment {
static final int INTEGRATED_ADDRESS_LENGTH = 106;
public static SendAddressWizardFragment newInstance(Listener listener) {
SendAddressWizardFragment instance = new SendAddressWizardFragment();
instance.setSendListener(listener);
return instance;
}
Listener sendListener;
public void setSendListener(Listener listener) {
this.sendListener = listener;
}
public interface Listener {
void setBarcodeData(BarcodeData data);
BarcodeData getBarcodeData();
BarcodeData popBarcodeData();
void setMode(SendFragment.Mode mode);
TxData getTxData();
}
private EditText etDummy;
private TextInputLayout etAddress;
private TextInputLayout etNotes;
private TextView tvXmrTo;
private Map<Crypto, ImageButton> ibCrypto;
final private Set<Crypto> possibleCryptos = new HashSet<>();
private Crypto selectedCrypto = null;
private boolean resolvingOA = false;
OnScanListener onScanListener;
public interface OnScanListener {
void onScan();
}
private Crypto getCryptoForButton(ImageButton button) {
for (Map.Entry<Crypto, ImageButton> entry : ibCrypto.entrySet()) {
if (entry.getValue() == button) return entry.getKey();
}
return null;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(R.layout.fragment_send_address, container, false);
if (Helper.ALLOW_SHIFT) {
tvXmrTo = view.findViewById(R.id.tvXmrTo);
ibCrypto = new HashMap<>();
for (Crypto crypto : Crypto.values()) {
final ImageButton button = view.findViewById(crypto.getButtonId());
ibCrypto.put(crypto, button);
button.setOnClickListener(v -> {
if (possibleCryptos.contains(crypto)) {
selectedCrypto = crypto;
updateCryptoButtons(false);
} else {
// show help what to do:
if (button.getId() != R.id.ibXMR) {
final String name = getResources().getStringArray(R.array.cryptos)[crypto.ordinal()];
final String symbol = getCryptoForButton(button).getSymbol();
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, name, symbol)));
tvXmrTo.setVisibility(View.VISIBLE);
} else {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
tvXmrTo.setVisibility(View.VISIBLE);
}
}
});
}
updateCryptoButtons(true);
} else {
view.findViewById(R.id.llExchange).setVisibility(View.GONE);
}
etAddress = view.findViewById(R.id.etAddress);
etAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etAddress.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
// ignore ENTER
return ((event != null) && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER));
}
});
etAddress.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
String enteredAddress = etAddress.getEditText().getText().toString().trim();
String dnsOA = dnsFromOpenAlias(enteredAddress);
Timber.d("OpenAlias is %s", dnsOA);
if (dnsOA != null) {
processOpenAlias(dnsOA);
}
}
});
etAddress.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
Timber.d("AFTER: %s", editable.toString());
etAddress.setError(null);
possibleCryptos.clear();
selectedCrypto = null;
final String address = etAddress.getEditText().getText().toString();
if (isIntegratedAddress(address)) {
Timber.d("isIntegratedAddress");
possibleCryptos.add(Crypto.XMR);
selectedCrypto = Crypto.XMR;
etAddress.setError(getString(R.string.info_paymentid_integrated));
sendListener.setMode(SendFragment.Mode.XMR);
} else if (isStandardAddress(address)) {
Timber.d("isStandardAddress");
possibleCryptos.add(Crypto.XMR);
selectedCrypto = Crypto.XMR;
sendListener.setMode(SendFragment.Mode.XMR);
}
if (!Helper.ALLOW_SHIFT) return;
if ((selectedCrypto == null) && isEthAddress(address)) {
Timber.d("isEthAddress");
possibleCryptos.add(Crypto.ETH);
selectedCrypto = Crypto.ETH;
tvXmrTo.setVisibility(View.VISIBLE);
sendListener.setMode(SendFragment.Mode.BTC);
}
if (possibleCryptos.isEmpty()) {
Timber.d("isBitcoinAddress");
for (BitcoinAddressType type : BitcoinAddressType.values()) {
if (BitcoinAddressValidator.validate(address, type)) {
possibleCryptos.add(Crypto.valueOf(type.name()));
}
}
if (!possibleCryptos.isEmpty()) // found something in need of shifting!
sendListener.setMode(SendFragment.Mode.BTC);
if (possibleCryptos.size() == 1) {
selectedCrypto = (Crypto) possibleCryptos.toArray()[0];
}
}
if (possibleCryptos.isEmpty()) {
Timber.d("other");
tvXmrTo.setVisibility(View.INVISIBLE);
sendListener.setMode(SendFragment.Mode.XMR);
}
updateCryptoButtons(address.isEmpty());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
final ImageButton bPasteAddress = view.findViewById(R.id.bPasteAddress);
bPasteAddress.setOnClickListener(v -> {
final String clip = Helper.getClipBoardText(getActivity());
if (clip == null) return;
// clean it up
final String address = clip.replaceAll("( +)|(\\r?\\n?)", "");
BarcodeData bc = BarcodeData.fromString(address);
if (bc != null) {
processScannedData(bc);
final EditText et = etAddress.getEditText();
et.setSelection(et.getText().length());
etAddress.requestFocus();
} else {
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
}
});
etNotes = view.findViewById(R.id.etNotes);
etNotes.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT);
etNotes.getEditText().
setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
etDummy.requestFocus();
return true;
}
return false;
});
final View cvScan = view.findViewById(R.id.bScan);
cvScan.setOnClickListener(v -> onScanListener.onScan());
etDummy = view.findViewById(R.id.etDummy);
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etDummy.requestFocus();
View tvNfc = view.findViewById(R.id.tvNfc);
NfcManager manager = (NfcManager) getContext().getSystemService(Context.NFC_SERVICE);
if ((manager != null) && (manager.getDefaultAdapter() != null))
tvNfc.setVisibility(View.VISIBLE);
return view;
}
private void selectedCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
button.setImageResource(crypto.getIconEnabledId());
button.setImageAlpha(255);
button.setEnabled(true);
}
private void possibleCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(255);
button.setEnabled(true);
}
private void impossibleCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(128);
button.setEnabled(true);
}
private void updateCryptoButtons(boolean noAddress) {
if (!Helper.ALLOW_SHIFT) return;
for (Crypto crypto : Crypto.values()) {
if (crypto == selectedCrypto) {
selectedCrypto(crypto);
} else if (possibleCryptos.contains(crypto)) {
possibleCrypto(crypto);
} else {
impossibleCrypto(crypto);
}
}
if ((selectedCrypto != null) && (selectedCrypto != Crypto.XMR)) {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getSymbol())));
tvXmrTo.setVisibility(View.VISIBLE);
} else if ((selectedCrypto == null) && (possibleCryptos.size() > 1)) {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_ambiguous)));
tvXmrTo.setVisibility(View.VISIBLE);
} else {
tvXmrTo.setVisibility(View.INVISIBLE);
}
if (noAddress) {
selectedCrypto(Crypto.XMR);
}
}
private void processOpenAlias(String dnsOA) {
if (resolvingOA) return; // already resolving - just wait
sendListener.popBarcodeData();
if (dnsOA != null) {
resolvingOA = true;
etAddress.setError(getString(R.string.send_address_resolve_openalias));
OpenAliasHelper.resolve(dnsOA, new OpenAliasHelper.OnResolvedListener() {
@Override
public void onResolved(Map<Crypto, BarcodeData> dataMap) {
resolvingOA = false;
BarcodeData barcodeData = dataMap.get(Crypto.XMR);
if (barcodeData == null) barcodeData = dataMap.get(Crypto.BTC);
if (barcodeData != null) {
Timber.d("Security=%s, %s", barcodeData.security.toString(), barcodeData.address);
processScannedData(barcodeData);
} else {
etAddress.setError(getString(R.string.send_address_not_openalias));
Timber.d("NO XMR OPENALIAS TXT FOUND");
}
}
@Override
public void onFailure() {
resolvingOA = false;
etAddress.setError(getString(R.string.send_address_not_openalias));
Timber.e("OA FAILED");
}
});
} // else ignore
}
private boolean checkAddressNoError() {
return selectedCrypto != null;
}
private boolean checkAddress() {
boolean ok = checkAddressNoError();
if (possibleCryptos.isEmpty()) {
etAddress.setError(getString(R.string.send_address_invalid));
} else {
etAddress.setError(null);
}
return ok;
}
private boolean isStandardAddress(String address) {
return Wallet.isAddressValid(address);
}
private boolean isIntegratedAddress(String address) {
return (address.length() == INTEGRATED_ADDRESS_LENGTH)
&& Wallet.isAddressValid(address);
}
private boolean isBitcoinishAddress(String address) {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC)
||
BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC)
||
BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
}
private boolean isEthAddress(String address) {
return EthAddressValidator.validate(address);
}
private void shakeAddress() {
if (possibleCryptos.size() > 1) { // address ambiguous
for (Crypto crypto : Crypto.values()) {
if (possibleCryptos.contains(crypto)) {
ibCrypto.get(crypto).startAnimation(Helper.getShakeAnimation(getContext()));
}
}
} else {
etAddress.startAnimation(Helper.getShakeAnimation(getContext()));
}
}
@Override
public boolean onValidateFields() {
if (!checkAddressNoError()) {
shakeAddress();
String enteredAddress = etAddress.getEditText().getText().toString().trim();
String dnsOA = dnsFromOpenAlias(enteredAddress);
Timber.d("OpenAlias is %s", dnsOA);
if (dnsOA != null) {
processOpenAlias(dnsOA);
}
return false;
}
if (sendListener != null) {
TxData txData = sendListener.getTxData();
if (txData instanceof TxDataBtc) {
((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString());
((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol());
txData.setDestinationAddress(null);
ServiceHelper.ASSET = selectedCrypto.getSymbol().toLowerCase();
} else {
txData.setDestinationAddress(etAddress.getEditText().getText().toString());
ServiceHelper.ASSET = null;
}
txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString()));
txData.setPriority(PendingTransaction.Priority.Priority_Default);
txData.setMixin(SendFragment.MIXIN);
}
return true;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof OnScanListener) {
onScanListener = (OnScanListener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement ScanListener");
}
}
// QR Scan Stuff
@Override
public void onResume() {
super.onResume();
Timber.d("onResume");
processScannedData();
}
public void processScannedData(BarcodeData barcodeData) {
sendListener.setBarcodeData(barcodeData);
if (isResumed())
processScannedData();
}
public void processScannedData() {
BarcodeData barcodeData = sendListener.getBarcodeData();
if (barcodeData != null) {
Timber.d("GOT DATA");
if (!Helper.ALLOW_SHIFT && (barcodeData.asset != Crypto.XMR)) {
Timber.d("BUT ONLY XMR SUPPORTED");
barcodeData = null;
sendListener.setBarcodeData(barcodeData);
}
if (barcodeData.address != null) {
etAddress.getEditText().setText(barcodeData.address);
possibleCryptos.clear();
selectedCrypto = null;
if (barcodeData.isAmbiguous()) {
possibleCryptos.addAll(barcodeData.ambiguousAssets);
} else {
possibleCryptos.add(barcodeData.asset);
selectedCrypto = barcodeData.asset;
}
if (Helper.ALLOW_SHIFT)
updateCryptoButtons(false);
if (checkAddress()) {
if (barcodeData.security == BarcodeData.Security.OA_NO_DNSSEC)
etAddress.setError(getString(R.string.send_address_no_dnssec));
else if (barcodeData.security == BarcodeData.Security.OA_DNSSEC)
etAddress.setError(getString(R.string.send_address_openalias));
}
} else {
etAddress.getEditText().getText().clear();
etAddress.setError(null);
}
String scannedNotes = barcodeData.addressName;
if (scannedNotes == null) {
scannedNotes = barcodeData.description;
} else if (barcodeData.description != null) {
scannedNotes = scannedNotes + ": " + barcodeData.description;
}
if (scannedNotes != null) {
etNotes.getEditText().setText(scannedNotes);
} else {
etNotes.getEditText().getText().clear();
etNotes.setError(null);
}
} else
Timber.d("barcodeData=null");
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
etDummy.requestFocus();
}
String dnsFromOpenAlias(String openalias) {
Timber.d("checking openalias candidate %s", openalias);
if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias;
if (Patterns.EMAIL_ADDRESS.matcher(openalias).matches()) {
openalias = openalias.replaceFirst("@", ".");
if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias;
}
return null; // not an openalias
}
}