mirror of https://github.com/m2049r/xmrwallet
386 lines
14 KiB
Java
386 lines
14 KiB
Java
/*
|
|
* 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.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.BitcoinAddressValidator;
|
|
import com.m2049r.xmrwallet.util.Helper;
|
|
import com.m2049r.xmrwallet.util.OpenAliasHelper;
|
|
|
|
import java.util.Map;
|
|
|
|
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 SendAddressWizardFragment setSendListener(Listener listener) {
|
|
this.sendListener = listener;
|
|
return this;
|
|
}
|
|
|
|
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 View cvScan;
|
|
private View tvPaymentIdIntegrated;
|
|
private TextView tvXmrTo;
|
|
private View llXmrTo;
|
|
private ImageButton bPasteAddress;
|
|
|
|
private boolean resolvingOA = false;
|
|
|
|
OnScanListener onScanListener;
|
|
|
|
public interface OnScanListener {
|
|
void onScan();
|
|
}
|
|
|
|
@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);
|
|
|
|
tvPaymentIdIntegrated = view.findViewById(R.id.tvPaymentIdIntegrated);
|
|
llXmrTo = view.findViewById(R.id.llXmrTo);
|
|
tvXmrTo = view.findViewById(R.id.tvXmrTo);
|
|
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto)));
|
|
|
|
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(new View.OnFocusChangeListener() {
|
|
@Override
|
|
public void onFocusChange(View v, boolean 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);
|
|
if (isIntegratedAddress()) {
|
|
Timber.d("isIntegratedAddress");
|
|
etAddress.setError(getString(R.string.info_paymentid_integrated));
|
|
tvPaymentIdIntegrated.setVisibility(View.VISIBLE);
|
|
llXmrTo.setVisibility(View.INVISIBLE);
|
|
sendListener.setMode(SendFragment.Mode.XMR);
|
|
} else if (isBitcoinAddress()) {
|
|
Timber.d("isBitcoinAddress");
|
|
setBtcMode();
|
|
} else {
|
|
Timber.d("isStandardAddress or other");
|
|
tvPaymentIdIntegrated.setVisibility(View.INVISIBLE);
|
|
llXmrTo.setVisibility(View.INVISIBLE);
|
|
sendListener.setMode(SendFragment.Mode.XMR);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
}
|
|
|
|
@Override
|
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
}
|
|
});
|
|
|
|
bPasteAddress = view.findViewById(R.id.bPasteAddress);
|
|
bPasteAddress.setOnClickListener(new View.OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
final String clip = Helper.getClipBoardText(getActivity());
|
|
if (clip == null) return;
|
|
// clean it up
|
|
final String address = clip.replaceAll("[^0-9A-Z-a-z]", "");
|
|
if (Wallet.isAddressValid(address) || BitcoinAddressValidator.validate(address)) {
|
|
final EditText et = etAddress.getEditText();
|
|
et.setText(address);
|
|
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(new TextView.OnEditorActionListener() {
|
|
public boolean onEditorAction(TextView v, int actionId, KeyEvent 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;
|
|
}
|
|
});
|
|
|
|
cvScan = view.findViewById(R.id.bScan);
|
|
cvScan.setOnClickListener(new View.OnClickListener() {
|
|
@Override
|
|
public void onClick(View 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 setBtcMode() {
|
|
Timber.d("setBtcMode");
|
|
tvPaymentIdIntegrated.setVisibility(View.INVISIBLE);
|
|
llXmrTo.setVisibility(View.VISIBLE);
|
|
sendListener.setMode(SendFragment.Mode.BTC);
|
|
}
|
|
|
|
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<BarcodeData.Asset, BarcodeData> dataMap) {
|
|
resolvingOA = false;
|
|
BarcodeData barcodeData = dataMap.get(BarcodeData.Asset.XMR);
|
|
if (barcodeData == null) barcodeData = dataMap.get(BarcodeData.Asset.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() {
|
|
String address = etAddress.getEditText().getText().toString();
|
|
return Wallet.isAddressValid(address)
|
|
|| BitcoinAddressValidator.validate(address);
|
|
}
|
|
|
|
private boolean checkAddress() {
|
|
boolean ok = checkAddressNoError();
|
|
if (!ok) {
|
|
etAddress.setError(getString(R.string.send_address_invalid));
|
|
} else {
|
|
etAddress.setError(null);
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
private boolean isIntegratedAddress() {
|
|
String address = etAddress.getEditText().getText().toString();
|
|
return (address.length() == INTEGRATED_ADDRESS_LENGTH)
|
|
&& Wallet.isAddressValid(address);
|
|
}
|
|
|
|
private boolean isBitcoinAddress() {
|
|
final String address = etAddress.getEditText().getText().toString();
|
|
return BitcoinAddressValidator.validate(address);
|
|
}
|
|
|
|
private void shakeAddress() {
|
|
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());
|
|
txData.setDestinationAddress(null);
|
|
} else {
|
|
txData.setDestinationAddress(etAddress.getEditText().getText().toString());
|
|
}
|
|
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 (barcodeData.address != null) {
|
|
etAddress.getEditText().setText(barcodeData.address);
|
|
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.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
|
|
}
|
|
}
|