package com.mygeopay.wallet.ui; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; import android.support.v7.view.ActionMode; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.FilterQueryProvider; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; import com.mygeopay.core.coins.CoinType; import com.mygeopay.core.coins.FiatType; import com.mygeopay.core.coins.Value; import com.mygeopay.core.coins.ValueType; import com.mygeopay.core.exchange.shapeshift.ShapeShift; import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftMarketInfo; import com.mygeopay.core.uri.CoinURI; import com.mygeopay.core.uri.CoinURIParseException; import com.mygeopay.core.util.ExchangeRate; import com.mygeopay.core.util.GenericUtils; import com.mygeopay.core.wallet.WalletAccount; import com.mygeopay.core.wallet.exceptions.NoSuchPocketException; import com.mygeopay.wallet.AddressBookProvider; import com.mygeopay.wallet.Configuration; import com.mygeopay.wallet.Constants; import com.mygeopay.wallet.ExchangeRatesProvider; import com.mygeopay.wallet.R; import com.mygeopay.wallet.WalletApplication; import com.mygeopay.wallet.tasks.MarketInfoPollTask; import com.mygeopay.wallet.ui.widget.AddressView; import com.mygeopay.wallet.ui.widget.AmountEditView; import com.mygeopay.wallet.util.ThrottlingWalletChangeListener; import com.mygeopay.wallet.util.UiUtils; import com.mygeopay.wallet.util.WeakHandler; import org.acra.ACRA; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Wallet; import org.bitcoinj.crypto.KeyCrypterException; import org.bitcoinj.utils.Threading; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Timer; import javax.annotation.Nullable; import static android.view.View.OnClickListener; import static com.mygeopay.core.Preconditions.checkNotNull; import static com.mygeopay.core.coins.Value.canCompare; import static com.mygeopay.wallet.ExchangeRatesProvider.getRates; import static com.mygeopay.wallet.util.UiUtils.setGone; import static com.mygeopay.wallet.util.UiUtils.setVisible; /** * Fragment that prepares a transaction * * @author Andreas Schildbach * @author John L. Jegutanis */ public class SendFragment extends Fragment { private static final Logger log = LoggerFactory.getLogger(SendFragment.class); // the fragment initialization parameters private static final int REQUEST_CODE_SCAN = 0; private static final int SIGN_TRANSACTION = 1; private static final int UPDATE_LOCAL_EXCHANGE_RATES = 0; private static final int UPDATE_WALLET_CHANGE = 1; private static final int UPDATE_MARKET = 2; // Loader IDs private static final int ID_RATE_LOADER = 0; private static final int ID_RECEIVING_ADDRESS_LOADER = 1; // Saved state private static final String STATE_ADDRESS = "address"; private static final String STATE_ADDRESS_CAN_CHANGE_TYPE = "address_can_change_type"; private static final String STATE_AMOUNT = "amount"; private static final String STATE_AMOUNT_TYPE = "amount_type"; @Nullable private Value lastBalance; // TODO setup wallet watcher for the latest balance private AutoCompleteTextView sendToAddressView; private AddressView sendToStaticAddressView; private TextView addressError; private AmountEditView sendCoinAmountView; private CurrencyCalculatorLink amountCalculatorLink; private TextView amountError; private TextView amountWarning; private ImageButton scanQrCodeButton; private ImageButton eraseAddressButton; private Button sendConfirmButton; private Timer timer; private MyMarketInfoPollTask pollTask; private ActionMode actionMode; private State state = State.INPUT; private Address address; private boolean addressTypeCanChange; private Value sendAmount; private CoinType sendAmountType; private WalletApplication application; private Listener listener; private WalletAccount pocket; private Configuration config; private ContentResolver resolver; private NavigationDrawerFragment mNavigationDrawerFragment; private LoaderManager loaderManager; private ReceivingAddressViewAdapter sendToAddressViewAdapter; private Map<String, ExchangeRate> localRates = new HashMap<>(); private ShapeShiftMarketInfo marketInfo; Handler handler = new MyHandler(this); private enum State { INPUT, PREPARATION, SENDING, SENT, FAILED } /** * Use this factory method to create a new instance of * this fragment using the an account id. * * @param accountId the id of an account * @return A new instance of fragment WalletSendCoins. */ public static SendFragment newInstance(String accountId) { SendFragment fragment = new SendFragment(); Bundle args = new Bundle(); args.putSerializable(Constants.ARG_ACCOUNT_ID, accountId); fragment.setArguments(args); return fragment; } /** * Use this factory method to create a new instance of * this fragment using a URI. * * @param uri the payment uri * @return A new instance of fragment WalletSendCoins. */ public static SendFragment newInstance(Uri uri) { SendFragment fragment = new SendFragment(); Bundle args = new Bundle(); args.putString(Constants.ARG_URI, uri.toString()); fragment.setArguments(args); return fragment; } public SendFragment() { // Required empty public constructor } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle args = getArguments(); if (args != null) { if (args.containsKey(Constants.ARG_ACCOUNT_ID)) { String accountId = args.getString(Constants.ARG_ACCOUNT_ID); pocket = checkNotNull(application.getAccount(accountId)); } if (args.containsKey(Constants.ARG_URI)) { try { processUri(args.getString(Constants.ARG_URI)); } catch (CoinURIParseException e) { // TODO handle more elegantly Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show(); ACRA.getErrorReporter().handleException(e); } } if (pocket == null) { List<WalletAccount> accounts = application.getAllAccounts(); if (!accounts.isEmpty()) pocket = accounts.get(0); } checkNotNull(pocket, "No account selected"); } else { throw new RuntimeException("Must provide account ID or a payment URI"); } sendAmountType = pocket.getCoinType(); if (savedInstanceState != null) { address = (Address) savedInstanceState.getSerializable(STATE_ADDRESS); addressTypeCanChange = savedInstanceState.getBoolean(STATE_ADDRESS_CAN_CHANGE_TYPE); sendAmount = (Value) savedInstanceState.getSerializable(STATE_AMOUNT); sendAmountType = (CoinType) savedInstanceState.getSerializable(STATE_AMOUNT_TYPE); } updateBalance(); setHasOptionsMenu(true); mNavigationDrawerFragment = (NavigationDrawerFragment) getFragmentManager().findFragmentById(R.id.navigation_drawer); String localSymbol = config.getExchangeCurrencyCode(); for (ExchangeRatesProvider.ExchangeRate rate : getRates(getActivity(), localSymbol)) { localRates.put(rate.currencyCodeId, rate.rate); } loaderManager.initLoader(ID_RATE_LOADER, null, rateLoaderCallbacks); loaderManager.initLoader(ID_RECEIVING_ADDRESS_LOADER, null, receivingAddressLoaderCallbacks); } private void processUri(String uri) throws CoinURIParseException { CoinURI coinUri = new CoinURI(uri); CoinType scannedType = coinUri.getType(); if (!Constants.SUPPORTED_COINS.contains(scannedType)) { String error = getResources().getString(R.string.unsupported_coin, scannedType.getName()); throw new CoinURIParseException(error); } setUri(coinUri); if (pocket == null) { List<WalletAccount> allAccounts = application.getAllAccounts(); List<WalletAccount> sendFromAccounts = application.getAccounts(coinUri.getType()); if (sendFromAccounts.size() == 1) { pocket = sendFromAccounts.get(0); } else if (allAccounts.size() == 1) { pocket = allAccounts.get(0); } else { throw new CoinURIParseException("No default account found"); } } } private void updateBalance() { if (pocket != null) { lastBalance = pocket.getBalance(); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_send, container, false); sendToAddressView = (AutoCompleteTextView) view.findViewById(R.id.send_to_address); sendToAddressViewAdapter = new ReceivingAddressViewAdapter(application); sendToAddressView.setAdapter(sendToAddressViewAdapter); sendToAddressView.setOnFocusChangeListener(receivingAddressListener); sendToAddressView.addTextChangedListener(receivingAddressListener); sendToStaticAddressView = (AddressView) view.findViewById(R.id.send_to_address_static); sendToStaticAddressView.setOnClickListener(addressOnClickListener); sendCoinAmountView = (AmountEditView) view.findViewById(R.id.send_coin_amount); sendCoinAmountView.resetType(sendAmountType); if (sendAmount != null) sendCoinAmountView.setAmount(sendAmount, false); AmountEditView sendLocalAmountView = (AmountEditView) view.findViewById(R.id.send_local_amount); sendLocalAmountView.setFormat(FiatType.FRIENDLY_FORMAT); amountCalculatorLink = new CurrencyCalculatorLink(sendCoinAmountView, sendLocalAmountView); amountCalculatorLink.setExchangeDirection(config.getLastExchangeDirection()); amountCalculatorLink.setExchangeRate(getCurrentRate()); addressError = (TextView) view.findViewById(R.id.address_error_message); addressError.setVisibility(View.GONE); amountError = (TextView) view.findViewById(R.id.amount_error_message); amountError.setVisibility(View.GONE); amountWarning = (TextView) view.findViewById(R.id.amount_warning_message); amountWarning.setVisibility(View.GONE); scanQrCodeButton = (ImageButton) view.findViewById(R.id.scan_qr_code); scanQrCodeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { handleScan(); } }); eraseAddressButton = (ImageButton) view.findViewById(R.id.erase_address); eraseAddressButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { clearAddress(true); updateView(); } }); sendConfirmButton = (Button) view.findViewById(R.id.send_confirm); sendConfirmButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { validateAddress(); validateAmount(); if (everythingValid()) handleSendConfirm(); else requestFocusFirst(); } }); return view; } private void clearAddress(boolean clearTextField) { address = null; if (clearTextField) setSendToAddressText(null); sendAmountType = pocket.getCoinType(); addressTypeCanChange = false; } private void setAddress(Address address, boolean typeCanChange) { this.address = address; this.addressTypeCanChange = typeCanChange; } @Override public void onDestroyView() { config.setLastExchangeDirection(amountCalculatorLink.getExchangeDirection()); super.onDestroyView(); } @Override public void onDestroy() { loaderManager.destroyLoader(ID_RECEIVING_ADDRESS_LOADER); loaderManager.destroyLoader(ID_RATE_LOADER); super.onDestroy(); } @Override public void onResume() { super.onResume(); amountCalculatorLink.setListener(amountsListener); resolver.registerContentObserver(AddressBookProvider.contentUri( getActivity().getPackageName()), true, addressBookObserver); if (pocket != null) { pocket.addEventListener(transactionChangeListener, Threading.SAME_THREAD); } updateBalance(); updateView(); } @Override public void onPause() { if (pocket != null) pocket.removeEventListener(transactionChangeListener); transactionChangeListener.removeCallbacks(); resolver.unregisterContentObserver(addressBookObserver); amountCalculatorLink.setListener(null); stopPolling(); super.onPause(); } @Override public void onSaveInstanceState(Bundle outState) { outState.putSerializable(STATE_ADDRESS, address); outState.putBoolean(STATE_ADDRESS_CAN_CHANGE_TYPE, addressTypeCanChange); outState.putSerializable(STATE_AMOUNT, sendAmount); outState.putSerializable(STATE_AMOUNT_TYPE, sendAmountType); } private void startOrStopMarketRatePolling() { if (address != null && !pocket.isType(address)) { String pair = ShapeShift.getPair(pocket.getCoinType(), (CoinType) address.getParameters()); if (timer == null) { startPolling(pair); } else { pollTask.updatePair(pair); } } else if (timer != null) { stopPolling(); } } /** * Start polling for the market information of the current pair, if it is already stated this * call does nothing */ private void startPolling(String pair) { if (timer == null) { ShapeShift shapeShift = application.getShapeShift(); pollTask = new MyMarketInfoPollTask(handler, shapeShift, pair); timer = new Timer(); timer.schedule(pollTask, 0, Constants.RATE_UPDATE_FREQ_MS); } } /** * Stop the polling for the market info, if it is already stop this call does nothing */ private void stopPolling() { if (timer != null) { timer.cancel(); timer.purge(); timer = null; pollTask.cancel(); pollTask = null; } } private void handleScan() { startActivityForResult(new Intent(getActivity(), ScanActivity.class), REQUEST_CODE_SCAN); } private void handleSendConfirm() { if (!everythingValid()) { // Sanity check log.error("Unexpected validity failure."); validateAmount(); validateAddress(); return; } state = State.PREPARATION; updateView(); if (application.getWallet() != null) { onMakeTransaction(address, sendAmount); } reset(); } public void onMakeTransaction(Address toAddress, Value amount) { Intent intent = new Intent(getActivity(), SignTransactionActivity.class); // Decide if emptying wallet or not if (canCompare(lastBalance, amount) && amount.compareTo(lastBalance) == 0) { intent.putExtra(Constants.ARG_EMPTY_WALLET, true); } else { intent.putExtra(Constants.ARG_SEND_VALUE, amount); } intent.putExtra(Constants.ARG_ACCOUNT_ID, pocket.getId()); intent.putExtra(Constants.ARG_SEND_TO_ADDRESS, toAddress); startActivityForResult(intent, SIGN_TRANSACTION); } private void reset() { clearAddress(true); sendToAddressView.setVisibility(View.VISIBLE); sendToStaticAddressView.setVisibility(View.GONE); amountCalculatorLink.setPrimaryAmount(null); sendAmount = null; state = State.INPUT; addressError.setVisibility(View.GONE); amountError.setVisibility(View.GONE); amountWarning.setVisibility(View.GONE); updateView(); } @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { if (requestCode == REQUEST_CODE_SCAN) { if (resultCode == Activity.RESULT_OK) { String input = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); if (!processInput(input)) { String error = getResources().getString(R.string.scan_error, input); Toast.makeText(getActivity(), error, Toast.LENGTH_LONG).show(); } } } else if (requestCode == SIGN_TRANSACTION && resultCode == Activity.RESULT_OK) { Exception error = (Exception) intent.getSerializableExtra(Constants.ARG_ERROR); if (error == null) { Toast.makeText(getActivity(), R.string.sending_msg, Toast.LENGTH_SHORT).show(); if (listener != null) listener.onTransactionBroadcastSuccess(pocket, null); } else { if (error instanceof InsufficientMoneyException) { Toast.makeText(getActivity(), R.string.amount_error_not_enough_money_plain, Toast.LENGTH_LONG).show(); } else if (error instanceof NoSuchPocketException) { Toast.makeText(getActivity(), R.string.no_such_pocket_error, Toast.LENGTH_LONG).show(); } else if (error instanceof KeyCrypterException) { Toast.makeText(getActivity(), R.string.password_failed, Toast.LENGTH_LONG).show(); } else if (error instanceof IOException) { Toast.makeText(getActivity(), R.string.send_coins_error_network, Toast.LENGTH_LONG).show(); } else if (error instanceof Wallet.DustySendRequested) { Toast.makeText(getActivity(), R.string.send_coins_error_dust, Toast.LENGTH_LONG).show(); } else { log.error("An unknown error occurred while sending coins", error); String errorMessage = getString(R.string.send_coins_error, error.getMessage()); Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_LONG).show(); } if (listener != null) listener.onTransactionBroadcastFailure(pocket, null); } } } private boolean processInput(String input) { input = input.trim(); try { updateStateFrom(new CoinURI(input)); return true; } catch (final CoinURIParseException x) { try { parseAddress(input); updateView(); return true; } catch (AddressFormatException e) { return false; } } } void updateStateFrom(CoinURI coinUri) throws CoinURIParseException { setUri(coinUri); // delay these actions until fragment is resumed handler.post(new Runnable() { @Override public void run() { if (sendAmount != null) { amountCalculatorLink.setPrimaryAmount(sendAmount); } updateView(); validateEverything(); requestFocusFirst(); } }); } private void setUri(CoinURI coinUri) throws CoinURIParseException { setAddress(coinUri.getAddress(), false); if (address == null) { // TODO when going to support the payment protocol, address could be null throw new CoinURIParseException("missing address"); } sendAmountType = (CoinType) address.getParameters(); sendAmount = coinUri.getAmount(); } private void updateView() { sendConfirmButton.setEnabled(everythingValid()); if (address == null) { setVisible(sendToAddressView); setGone(sendToStaticAddressView); setVisible(scanQrCodeButton); setGone(eraseAddressButton); if (actionMode != null) { actionMode.finish(); actionMode = null; } } else { setGone(sendToAddressView); setVisible(sendToStaticAddressView); sendToStaticAddressView.setAddressAndLabel(address); setGone(scanQrCodeButton); setVisible(eraseAddressButton); } if (sendCoinAmountView.resetType(sendAmountType)) { amountCalculatorLink.setExchangeRate(getCurrentRate()); } startOrStopMarketRatePolling(); // enable actions scanQrCodeButton.setEnabled(state == State.INPUT); eraseAddressButton.setEnabled(state == State.INPUT); } private boolean isOutputsValid() { return address != null; } private boolean isAmountValid() { return isAmountValid(sendAmount); } private boolean isAmountValid(Value amount) { return amount != null && isAmountWithinLimits(amount); } /** * Check if amount is within the minimum and maximum deposit limits and if is dust or if is more * money than currently in the wallet */ private boolean isAmountWithinLimits(Value amount) { boolean isWithinLimits = amount != null && amount.isPositive() && !amount.isDust(); // Check if within min & max deposit limits if (isWithinLimits && marketInfo != null && canCompare(marketInfo.limit, amount)) { isWithinLimits = amount.within(marketInfo.minimum, marketInfo.limit); } // Check if we have the amount if (isWithinLimits && canCompare(lastBalance, amount)) { isWithinLimits = amount.compareTo(lastBalance) <= 0; } return isWithinLimits; } /** * Check if amount is smaller than the dust limit or if applicable, the minimum deposit. */ private boolean isAmountTooSmall(Value amount) { return amount.compareTo(getLowestAmount(amount.type)) < 0; } /** * Get the lowest deposit or withdraw for the provided amount type */ private Value getLowestAmount(ValueType type) { Value min = type.minNonDust(); if (marketInfo != null) { if (marketInfo.minimum.isOfType(min)) { min = Value.max(marketInfo.minimum, min); } else if (marketInfo.rate.canConvert(type, marketInfo.minimum.type)) { min = Value.max(marketInfo.rate.convert(marketInfo.minimum), min); } } return min; } private boolean everythingValid() { return state == State.INPUT && isOutputsValid() && isAmountValid(); } private void requestFocusFirst() { if (!isOutputsValid()) { sendToAddressView.requestFocus(); } else if (!isAmountValid()) { amountCalculatorLink.requestFocus(); // FIXME causes problems in older Androids // Keyboard.focusAndShowKeyboard(sendAmountView, getActivity()); } else if (everythingValid()) { sendConfirmButton.requestFocus(); } else { log.warn("unclear focus"); } } private void validateEverything() { validateAddress(); validateAmount(); } private void validateAmount() { validateAmount(false); } private void validateAmount(boolean isTyping) { Value amountParsed = amountCalculatorLink.getPrimaryAmount(); if (isAmountValid(amountParsed)) { sendAmount = amountParsed; amountError.setVisibility(View.GONE); // Show warning that fees apply when entered the full amount inside the pocket if (canCompare(sendAmount, lastBalance) && sendAmount.compareTo(lastBalance) == 0) { amountWarning.setText(R.string.amount_warn_fees_apply); amountWarning.setVisibility(View.VISIBLE); } else { amountWarning.setVisibility(View.GONE); } } else { amountWarning.setVisibility(View.GONE); // ignore printing errors for null and zero amounts if (shouldShowErrors(isTyping, amountParsed)) { sendAmount = null; if (amountParsed == null) { amountError.setText(R.string.amount_error); } else if (amountParsed.isNegative()) { amountError.setText(R.string.amount_error_negative); } else if (!isAmountWithinLimits(amountParsed)) { String message = getString(R.string.error_generic); // If the amount is dust or lower than the deposit limit if (isAmountTooSmall(amountParsed)) { Value minAmount = getLowestAmount(amountParsed.type); message = getString(R.string.amount_error_too_small, minAmount.toFriendlyString()); } else { // If we have the amount if (canCompare(lastBalance, amountParsed) && amountParsed.compareTo(lastBalance) > 0) { message = getString(R.string.amount_error_not_enough_money, lastBalance.toFriendlyString()); } if (marketInfo != null && canCompare(marketInfo.limit, amountParsed) && amountParsed.compareTo(marketInfo.limit) > 0) { message = getString(R.string.trade_error_max_limit, marketInfo.limit.toFriendlyString()); } } amountError.setText(message); } else { // Should not happen, but show a generic error amountError.setText(R.string.amount_error); } amountError.setVisibility(View.VISIBLE); } else { amountError.setVisibility(View.GONE); } } updateView(); } /** * Decide if should show errors in the UI. */ private boolean shouldShowErrors(boolean isTyping, Value amount) { if (amount != null && !amount.isZero() && !isAmountWithinLimits(amount)) { return true; } if (isTyping) return false; if (amountCalculatorLink.isEmpty()) return false; if (amount != null && amount.isZero()) return false; return true; } private void validateAddress() { validateAddress(false); } private void validateAddress(boolean isTyping) { if (address == null) { String input = sendToAddressView.getText().toString().trim(); try { if (!input.isEmpty()) { // Process fast the input string if (processInput(input)) return; // Try to fix address if needed parseAddress(GenericUtils.fixAddress(input)); } else { // empty field should not raise error message clearAddress(false); } addressError.setVisibility(View.GONE); } catch (final AddressFormatException x) { // could not decode address at all if (!isTyping) { clearAddress(false); addressError.setText(R.string.address_error); addressError.setVisibility(View.VISIBLE); } } updateView(); } } private void setSendToAddressText(String addressStr) { // Remove listener before changing input, to avoid infinite recursion sendToAddressView.removeTextChangedListener(receivingAddressListener); sendToAddressView.setOnFocusChangeListener(null); sendToAddressView.setText(addressStr); sendToAddressView.addTextChangedListener(receivingAddressListener); sendToAddressView.setOnFocusChangeListener(receivingAddressListener); } private void parseAddress(String addressStr) throws AddressFormatException { List<CoinType> possibleTypes = GenericUtils.getPossibleTypes(addressStr); if (possibleTypes.contains(pocket.getCoinType())) { setAddress(new Address(pocket.getCoinType(), addressStr), true); sendAmountType = pocket.getCoinType(); } else if (possibleTypes.size() == 1) { setAddress(new Address(possibleTypes.get(0), addressStr), true); sendAmountType = possibleTypes.get(0); } else { // This address string could be more that one coin type so first check if this address // comes from an account to determine the type. List<WalletAccount> possibleAccounts = application.getAccounts(possibleTypes); Address addressOfAccount = null; for (WalletAccount account : possibleAccounts) { Address testAddress = new Address(account.getCoinType(), addressStr); if (account.isAddressMine(testAddress)) { addressOfAccount = testAddress; break; } } if (addressOfAccount != null) { // If address is from an account don't show a dialog. The type should not change as // we know 100% that is correct one setAddress(addressOfAccount, false); sendAmountType = (CoinType) addressOfAccount.getParameters(); } else { // As a last resort let the use choose the correct coin type showPayToDialog(addressStr); } } } private void showPayToDialog(String addressStr) { if (!isVisible() || !isResumed()) return; if (selectCoinTypeDialog.getArguments() == null) { selectCoinTypeDialog.setArguments(new Bundle()); } selectCoinTypeDialog.getArguments().putString(Constants.ARG_ADDRESS_STRING, addressStr); selectCoinTypeDialog.show(getFragmentManager(), null); } SelectCoinTypeDialog selectCoinTypeDialog = new SelectCoinTypeDialog() { // FIXME crash when this dialog being restored from saved state @Override public void onAddressSelected(Address selectedAddress) { setAddress(selectedAddress, true); sendAmountType = (CoinType) selectedAddress.getParameters(); updateView(); } }; private void setAmountForEmptyWallet() { updateBalance(); if (state != State.INPUT || pocket == null || lastBalance == null) return; if (lastBalance.isZero()) { String message = getResources().getString(R.string.amount_error_not_enough_money_plain); Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show(); } else { amountCalculatorLink.setPrimaryAmount(lastBalance); validateAmount(); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (mNavigationDrawerFragment != null && !mNavigationDrawerFragment.isDrawerOpen()) { // Only show items in the action bar relevant to this screen // if the drawer is not showing. Otherwise, let the drawer // decide what to show in the action bar. inflater.inflate(R.menu.send, menu); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_empty_wallet: setAmountForEmptyWallet(); return true; default: // Not one of ours. Perform default menu processing return super.onOptionsItemSelected(item); } } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { this.listener = (Listener) activity; this.application = (WalletApplication) activity.getApplication(); this.config = application.getConfiguration(); this.resolver = activity.getContentResolver(); this.loaderManager = getLoaderManager(); } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement " + Listener.class); } } @Override public void onDetach() { super.onDetach(); listener = null; } public interface Listener { public void onTransactionBroadcastSuccess(WalletAccount pocket, Transaction transaction); public void onTransactionBroadcastFailure(WalletAccount pocket, Transaction transaction); } private abstract class EditViewListener implements View.OnFocusChangeListener, TextWatcher { @Override public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { } @Override public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { } } OnClickListener addressOnClickListener = new OnClickListener() { @Override public void onClick(View v) { if (address != null) { final boolean showChangeType = addressTypeCanChange && GenericUtils.hasMultipleTypes(address); ActionMode.Callback callback = new UiUtils.AddressActionModeCallback( address, application.getApplicationContext(), getFragmentManager()) { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { mode.getMenuInflater().inflate(R.menu.address_options_extra, menu); return super.onCreateActionMode(mode, menu); } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { menu.findItem(R.id.action_change_address_type).setVisible(showChangeType); return super.onPrepareActionMode(mode, menu); } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.action_change_address_type: showPayToDialog(getAddress().toString()); mode.finish(); return true; } return super.onActionItemClicked(mode, menuItem); } }; actionMode = UiUtils.startActionMode(getActivity(), callback); // Hack to dismiss this action mode when back is pressed if (listener != null && listener instanceof WalletActivity) { ((WalletActivity) listener).registerActionMode(actionMode); } } } }; EditViewListener receivingAddressListener = new EditViewListener() { @Override public void onFocusChange(final View v, final boolean hasFocus) { if (!hasFocus) { validateAddress(); } } @Override public void afterTextChanged(final Editable s) { validateAddress(true); } }; private final AmountEditView.Listener amountsListener = new AmountEditView.Listener() { @Override public void changed() { validateAmount(true); } @Override public void focusChanged(final boolean hasFocus) { if (!hasFocus) { validateAmount(); } } }; private final LoaderCallbacks<Cursor> rateLoaderCallbacks = new LoaderManager.LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(final int id, final Bundle args) { String localSymbol = config.getExchangeCurrencyCode(); return new ExchangeRateLoader(getActivity(), config, localSymbol); } @Override public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) { if (data != null && data.getCount() > 0) { HashMap<String, com.mygeopay.core.util.ExchangeRate> rates = new HashMap<>(data.getCount()); data.moveToFirst(); do { ExchangeRatesProvider.ExchangeRate rate = ExchangeRatesProvider.getExchangeRate(data); rates.put(rate.currencyCodeId, rate.rate); } while (data.moveToNext()); handler.sendMessage(handler.obtainMessage(UPDATE_LOCAL_EXCHANGE_RATES, rates)); } } @Override public void onLoaderReset(final Loader<Cursor> loader) { } }; private void onLocalExchangeRatesUpdate(HashMap<String, ExchangeRate> rates) { localRates = rates; if (state == State.INPUT) { amountCalculatorLink.setExchangeRate(getCurrentRate()); } } /** * Updates the exchange rate and limits for the specific market. * Note: if the current pair is different that the marketInfo pair, do nothing */ private void onMarketUpdate(ShapeShiftMarketInfo marketInfo) { if (address != null && marketInfo.isPair(pocket.getCoinType(), (CoinType) address.getParameters())) { this.marketInfo = marketInfo; } } @Nullable private ExchangeRate getCurrentRate() { return localRates.get(sendAmountType.getSymbol()); } private void onWalletUpdate() { updateBalance(); validateAmount(); } private static class MyHandler extends WeakHandler<SendFragment> { public MyHandler(SendFragment referencingObject) { super(referencingObject); } @Override protected void weakHandleMessage(SendFragment ref, Message msg) { switch (msg.what) { case UPDATE_LOCAL_EXCHANGE_RATES: ref.onLocalExchangeRatesUpdate((HashMap<String, ExchangeRate>) msg.obj); break; case UPDATE_WALLET_CHANGE: ref.onWalletUpdate(); break; case UPDATE_MARKET: ref.onMarketUpdate((ShapeShiftMarketInfo) msg.obj); break; } } } private final ThrottlingWalletChangeListener transactionChangeListener = new ThrottlingWalletChangeListener() { @Override public void onThrottledWalletChanged() { handler.sendMessage(handler.obtainMessage(UPDATE_WALLET_CHANGE)); } }; private final LoaderCallbacks<Cursor> receivingAddressLoaderCallbacks = new LoaderManager.LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(final int id, final Bundle args) { final String constraint = args != null ? args.getString("constraint") : null; // TODO support addresses from other accounts Uri uri = AddressBookProvider.contentUri(application.getPackageName(), pocket.getCoinType()); return new CursorLoader(application, uri, null, AddressBookProvider.SELECTION_QUERY, new String[]{constraint != null ? constraint : ""}, null); } @Override public void onLoadFinished(final Loader<Cursor> cursor, final Cursor data) { sendToAddressViewAdapter.swapCursor(data); } @Override public void onLoaderReset(final Loader<Cursor> cursor) { sendToAddressViewAdapter.swapCursor(null); } }; private final class ReceivingAddressViewAdapter extends CursorAdapter implements FilterQueryProvider { public ReceivingAddressViewAdapter(final Context context) { super(context, null, false); setFilterQueryProvider(this); } @Override public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { final LayoutInflater inflater = LayoutInflater.from(context); return inflater.inflate(R.layout.address_book_row, parent, false); } @Override public void bindView(final View view, final Context context, final Cursor cursor) { final String label = cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_LABEL)); final String address = cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_ADDRESS)); final ViewGroup viewGroup = (ViewGroup) view; final TextView labelView = (TextView) viewGroup.findViewById(R.id.address_book_row_label); labelView.setText(label); final TextView addressView = (TextView) viewGroup.findViewById(R.id.address_book_row_address); addressView.setText(GenericUtils.addressSplitToGroupsMultiline(address)); } @Override public CharSequence convertToString(final Cursor cursor) { return cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_ADDRESS)); } @Override public Cursor runQuery(final CharSequence constraint) { final Bundle args = new Bundle(); if (constraint != null) args.putString("constraint", constraint.toString()); loaderManager.restartLoader(ID_RECEIVING_ADDRESS_LOADER, args, receivingAddressLoaderCallbacks); return getCursor(); } } private static class MyMarketInfoPollTask extends MarketInfoPollTask { private final Handler handler; MyMarketInfoPollTask(Handler handler, ShapeShift shapeShift, String pair) { super(shapeShift, pair); this.handler = handler; } @Override public void onHandleMarketInfo(ShapeShiftMarketInfo marketInfo) { handler.sendMessage(handler.obtainMessage(UPDATE_MARKET, marketInfo)); } } private final ContentObserver addressBookObserver = new ContentObserver(handler) { @Override public void onChange(final boolean selfChange) { updateView(); } }; }