xmrwallet/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java

567 lines
18 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.content.SharedPreferences;
import android.os.Bundle;
import android.text.InputType;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.m2049r.xmrwallet.OnBackPressedListener;
import com.m2049r.xmrwallet.OnUriScannedListener;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.WalletActivity;
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.data.UserNotes;
import com.m2049r.xmrwallet.layout.SpendViewPager;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.Notice;
import com.m2049r.xmrwallet.widget.DotBar;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.lang.ref.WeakReference;
import timber.log.Timber;
public class SendFragment extends Fragment
implements SendAddressWizardFragment.Listener,
SendAmountWizardFragment.Listener,
SendConfirmWizardFragment.Listener,
SendSuccessWizardFragment.Listener,
OnBackPressedListener, OnUriScannedListener {
final static public int MIXIN = 10;
private Listener activityCallback;
public interface Listener {
SharedPreferences getPrefs();
long getTotalFunds();
boolean isStreetMode();
void onPrepareSend(String tag, TxData data);
String getWalletName();
void onSend(UserNotes notes);
void onDisposeRequest();
void onFragmentDone();
void setToolbarButton(int type);
void setTitle(String title);
void setSubtitle(String subtitle);
void setOnUriScannedListener(OnUriScannedListener onUriScannedListener);
}
private EditText etDummy;
private View llNavBar;
private DotBar dotBar;
private Button bPrev;
private Button bNext;
private Button bDone;
static private int MAX_FALLBACK = Integer.MAX_VALUE;
public static SendFragment newInstance(String uri) {
SendFragment f = new SendFragment();
Bundle args = new Bundle();
args.putString(WalletActivity.REQUEST_URI, uri);
f.setArguments(args);
return f;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_send, container, false);
llNavBar = view.findViewById(R.id.llNavBar);
bDone = view.findViewById(R.id.bDone);
dotBar = view.findViewById(R.id.dotBar);
bPrev = view.findViewById(R.id.bPrev);
bNext = view.findViewById(R.id.bNext);
ViewGroup llNotice = view.findViewById(R.id.llNotice);
Notice.showAll(llNotice, ".*_send");
spendViewPager = view.findViewById(R.id.pager);
pagerAdapter = new SpendPagerAdapter(getChildFragmentManager());
spendViewPager.setOffscreenPageLimit(pagerAdapter.getCount()); // load & keep all pages in cache
spendViewPager.setAdapter(pagerAdapter);
spendViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
private int fallbackPosition = MAX_FALLBACK;
private int currentPosition = 0;
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int newPosition) {
Timber.d("onPageSelected=%d/%d", newPosition, fallbackPosition);
if (fallbackPosition < newPosition) {
spendViewPager.setCurrentItem(fallbackPosition);
} else {
pagerAdapter.getFragment(currentPosition).onPauseFragment();
pagerAdapter.getFragment(newPosition).onResumeFragment();
updatePosition(newPosition);
currentPosition = newPosition;
fallbackPosition = MAX_FALLBACK;
}
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_DRAGGING) {
if (!spendViewPager.validateFields(spendViewPager.getCurrentItem())) {
fallbackPosition = spendViewPager.getCurrentItem();
} else {
fallbackPosition = spendViewPager.getCurrentItem() + 1;
}
}
}
});
bPrev.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
spendViewPager.previous();
}
});
bNext.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
spendViewPager.next();
}
});
bDone.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Timber.d("bDone.onClick");
activityCallback.onFragmentDone();
}
});
updatePosition(0);
etDummy = view.findViewById(R.id.etDummy);
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etDummy.requestFocus();
Helper.hideKeyboard(getActivity());
Bundle args = getArguments();
if (args != null) {
String uri = args.getString(WalletActivity.REQUEST_URI);
Timber.d("URI: %s", uri);
if (uri != null) {
barcodeData = BarcodeData.fromQrCode(uri);
Timber.d("barcodeData: %s", barcodeData != null ? barcodeData.toString() : "null");
}
}
return view;
}
void updatePosition(int position) {
dotBar.setActiveDot(position);
CharSequence nextLabel = pagerAdapter.getPageTitle(position + 1);
bNext.setText(nextLabel);
if (nextLabel != null) {
bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_navigate_next_white_24dp, 0);
} else {
bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
CharSequence prevLabel = pagerAdapter.getPageTitle(position - 1);
bPrev.setText(prevLabel);
if (prevLabel != null) {
bPrev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_navigate_prev_white_24dp, 0, 0, 0);
} else {
bPrev.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume");
activityCallback.setSubtitle(getString(R.string.send_title));
if (spendViewPager.getCurrentItem() == SpendPagerAdapter.POS_SUCCESS) {
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
} else {
activityCallback.setToolbarButton(Toolbar.BUTTON_CANCEL);
}
}
@Override
public void onAttach(Context context) {
Timber.d("onAttach %s", context);
super.onAttach(context);
if (context instanceof Listener) {
activityCallback = (Listener) context;
activityCallback.setOnUriScannedListener(this);
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
@Override
public void onDetach() {
activityCallback.setOnUriScannedListener(null);
super.onDetach();
}
private SpendViewPager spendViewPager;
private SpendPagerAdapter pagerAdapter;
@Override
public boolean onBackPressed() {
if (isComitted()) return true; // no going back
if (spendViewPager.getCurrentItem() == 0) {
return false;
} else {
spendViewPager.previous();
return true;
}
}
@Override
public boolean onUriScanned(BarcodeData barcodeData) {
if (spendViewPager.getCurrentItem() == SpendPagerAdapter.POS_ADDRESS) {
final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_ADDRESS);
if (fragment instanceof SendAddressWizardFragment) {
((SendAddressWizardFragment) fragment).processScannedData(barcodeData);
return true;
}
}
return false;
}
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 = %s", 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_CONFIRM = 2;
private static final int POS_SUCCESS = 3;
private int numPages = 3;
SparseArray<WeakReference<SendWizardFragment>> myFragments = new SparseArray<>();
public SpendPagerAdapter(FragmentManager fm) {
super(fm);
}
public void addSuccess() {
numPages++;
notifyDataSetChanged();
}
@Override
public int getCount() {
return numPages;
}
@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;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
Timber.d("destroyItem %d", position);
myFragments.remove(position);
super.destroyItem(container, position, object);
}
public SendWizardFragment getFragment(int position) {
WeakReference ref = myFragments.get(position);
if (ref != null)
return myFragments.get(position).get();
else
return null;
}
@Override
public SendWizardFragment getItem(int position) {
Timber.d("getItem(%d) CREATE", position);
Timber.d("Mode=%s", 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_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_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!");
}
}
@Override
public CharSequence getPageTitle(int position) {
Timber.d("getPageTitle(%d)", position);
if (position >= numPages) return null;
switch (position) {
case POS_ADDRESS:
return getString(R.string.send_address_title);
case POS_AMOUNT:
return getString(R.string.send_amount_title);
case POS_CONFIRM:
return getString(R.string.send_confirm_title);
case POS_SUCCESS:
return getString(R.string.send_success_title);
default:
return null;
}
}
@Override
public int getItemPosition(Object object) {
Timber.d("getItemPosition %s", String.valueOf(object));
if (object instanceof SendAddressWizardFragment) {
// keep these pages
return POSITION_UNCHANGED;
} else {
return POSITION_NONE;
}
}
}
@Override
public TxData getTxData() {
return txData;
}
private TxData txData = new TxData();
private BarcodeData barcodeData;
// Listeners
@Override
public void setBarcodeData(BarcodeData data) {
barcodeData = data;
}
@Override
public BarcodeData getBarcodeData() {
return barcodeData;
}
@Override
public BarcodeData popBarcodeData() {
Timber.d("POPPED");
BarcodeData data = barcodeData;
barcodeData = null;
return data;
}
boolean isComitted() {
return committedTx != null;
}
PendingTx committedTx;
@Override
public PendingTx getCommittedTx() {
return committedTx;
}
@Override
public void commitTransaction() {
Timber.d("REALLY SEND");
disableNavigation(); // committed - disable all navigation
activityCallback.onSend(txData.getUserNotes());
committedTx = pendingTx;
}
void disableNavigation() {
spendViewPager.allowSwipe(false);
}
void enableNavigation() {
spendViewPager.allowSwipe(true);
}
@Override
public void enableDone() {
llNavBar.setVisibility(View.INVISIBLE);
bDone.setVisibility(View.VISIBLE);
}
public Listener getActivityCallback() {
return activityCallback;
}
// callbacks from send service
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {
pendingTx = new PendingTx(pendingTransaction);
confirm.transactionCreated(txTag, pendingTransaction);
} else {
// not in confirm fragment => dispose & move on
disposeTransaction();
}
}
@Override
public void disposeTransaction() {
pendingTx = null;
activityCallback.onDisposeRequest();
}
PendingTx pendingTx;
public PendingTx getPendingTx() {
return pendingTx;
}
public void onCreateTransactionFailed(String errorText) {
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {
confirm.createTransactionFailed(errorText);
}
}
SendConfirm getSendConfirm() {
final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_CONFIRM);
if (fragment instanceof SendConfirm) {
return (SendConfirm) fragment;
} else {
return null;
}
}
public void onTransactionSent(final String txId) {
Timber.d("txid=%s", txId);
pagerAdapter.addSuccess();
Timber.d("numPages=%d", spendViewPager.getAdapter().getCount());
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
spendViewPager.setCurrentItem(SpendPagerAdapter.POS_SUCCESS);
}
public void onSendTransactionFailed(final String error) {
Timber.d("error=%s", error);
committedTx = null;
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {
confirm.sendFailed(getString(R.string.status_transaction_failed, error));
}
enableNavigation();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.send_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
}