package com.mygeopay.wallet.ui; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; 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.AdapterView; import android.widget.Button; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import com.mygeopay.core.coins.CoinType; import com.mygeopay.core.coins.Value; import com.mygeopay.core.exchange.shapeshift.ShapeShift; import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftCoins; import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftException; import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftMarketInfo; import com.mygeopay.core.util.ExchangeRate; import com.mygeopay.core.wallet.Wallet; import com.mygeopay.core.wallet.WalletAccount; import com.mygeopay.wallet.Constants; import com.mygeopay.wallet.R; import com.mygeopay.wallet.WalletApplication; import com.mygeopay.wallet.tasks.AddCoinTask; import com.mygeopay.wallet.tasks.MarketInfoPollTask; import com.mygeopay.wallet.ui.adaptors.AvailableAccountsAdaptor; import com.mygeopay.wallet.ui.widget.AmountEditView; import com.mygeopay.wallet.util.Keyboard; import com.mygeopay.wallet.util.ThrottlingWalletChangeListener; import com.mygeopay.wallet.util.WeakHandler; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.bitcoinj.utils.Threading; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.Timer; import javax.annotation.Nullable; import static com.mygeopay.core.coins.Value.canCompare; import static com.mygeopay.core.coins.Value.max; /** * @author John L. Jegutanis */ public class TradeSelectFragment extends Fragment { private static final Logger log = LoggerFactory.getLogger(TradeSelectFragment.class); private static final int UPDATE_MARKET = 0; private static final int UPDATE_MARKET_ERROR = 1; private static final int UPDATE_WALLET = 2; private static final int VALIDATE_AMOUNT = 3; private static final int INITIAL_TASK_ERROR = 4; private static final int UPDATE_AVAILABLE_COINS = 5; // UI & misc private WalletApplication application; private Wallet wallet; private final Handler handler = new MyHandler(this); private final AmountListener amountsListener = new AmountListener(handler); private final AccountListener sourceAccountListener = new AccountListener(handler); @Nullable private Listener listener; @Nullable private MenuItem actionSwapMenu; private Spinner sourceSpinner; private Spinner destinationSpinner; private AvailableAccountsAdaptor sourceAdapter; private AvailableAccountsAdaptor destinationAdapter; private AmountEditView sourceAmountView; private AmountEditView destinationAmountView; private CurrencyCalculatorLink amountCalculatorLink; private TextView amountError; private TextView amountWarning; private Button nextButton; // Tasks private MarketInfoTask marketTask; private InitialCheckTask initialTask; private Timer timer; private MyMarketInfoPollTask pollTask; private AddCoinAndProceedTask addCoinAndProceedTask; // State private WalletAccount sourceAccount; @Nullable private WalletAccount destinationAccount; private CoinType destinationType; @Nullable private Value sendAmount; @Nullable private Value maximumDeposit; @Nullable private Value minimumDeposit; @Nullable private Value lastBalance; @Nullable private ExchangeRate lastRate; /** Required empty public constructor */ public TradeSelectFragment() {} //////////////////////////////////////////////////////////////////////////////////////////////// // Android callback methods @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); // Select some default coins List<WalletAccount> accounts = application.getAllAccounts(); sourceAccount = accounts.get(0); if (accounts.size() > 1) { destinationAccount = accounts.get(1); destinationType = destinationAccount.getCoinType(); } else { // Find a destination coin that is different than the source coin for (CoinType type : Constants.SUPPORTED_COINS) { if (type.equals(sourceAccount.getCoinType())) continue; destinationType = type; break; } } updateBalance(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_trade_select, container, false); sourceSpinner = (Spinner) view.findViewById(R.id.from_coin); sourceSpinner.setAdapter(getSourceSpinnerAdapter()); sourceSpinner.setOnItemSelectedListener(getSourceSpinnerListener()); destinationSpinner = (Spinner) view.findViewById(R.id.to_coin); destinationSpinner.setAdapter(getDestinationSpinnerAdapter()); destinationSpinner.setOnItemSelectedListener(getDestinationSpinnerListener()); sourceAmountView = (AmountEditView) view.findViewById(R.id.trade_coin_amount); destinationAmountView = (AmountEditView) view.findViewById(R.id.receive_coin_amount); amountCalculatorLink = new CurrencyCalculatorLink(sourceAmountView, destinationAmountView); // receiveCoinWarning = (TextView) view.findViewById(R.id.warn_no_account_found); // receiveCoinWarning.setVisibility(View.GONE); // 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 View.OnClickListener() { // @Override // public void onClick(View v) { // handleScan(); // } // }); view.findViewById(R.id.powered_by_shapeshift).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new AlertDialog.Builder(getActivity()) .setTitle(R.string.about_shapeshift_title) .setMessage(R.string.about_shapeshift_message) .setPositiveButton(R.string.button_ok, null) .create().show(); } }); nextButton = (Button) view.findViewById(R.id.button_next); nextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // validateAddress(); validateAmount(); if (everythingValid()) { onHandleNext(); } else if (amountCalculatorLink.isEmpty()) { amountError.setText(R.string.amount_error_empty); amountError.setVisibility(View.VISIBLE); } } }); // Setup the default source & destination views setSource(sourceAccount, false); if (destinationAccount != null) { setDestination(destinationAccount, false); } else { setDestination(destinationType, false); } if (!application.isConnected()) { showInitialTaskErrorDialog(null); } else { maybeStartInitialTask(); } return view; } private AvailableAccountsAdaptor getDestinationSpinnerAdapter() { if (destinationAdapter == null) { destinationAdapter = new AvailableAccountsAdaptor(getActivity()); } return destinationAdapter; } private AvailableAccountsAdaptor getSourceSpinnerAdapter() { if (sourceAdapter == null) { sourceAdapter = new AvailableAccountsAdaptor(getActivity()); } return sourceAdapter; } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { this.listener = (Listener) activity; this.application = (WalletApplication) activity.getApplication(); this.wallet = application.getWallet(); } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement " + TradeSelectFragment.Listener.class); } } @Override public void onDetach() { super.onDetach(); listener = null; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.trade, menu); actionSwapMenu = menu.findItem(R.id.action_swap_coins); } @Override public void onPause() { stopPolling(); removeSourceListener(); amountCalculatorLink.setListener(null); super.onPause(); } @Override public void onResume() { super.onResume(); startPolling(); amountCalculatorLink.setListener(amountsListener); addSourceListener(); updateNextButtonState(); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { // case R.id.action_empty_wallet: // setAmountForEmptyWallet(); // return true; case R.id.action_refresh: refreshStartInitialTask(); return true; case R.id.action_swap_coins: swapAccounts(); return true; case R.id.action_exchange_history: startActivity(new Intent(getActivity(), ExchangeHistoryActivity.class)); return true; default: return super.onOptionsItemSelected(item); } } //////////////////////////////////////////////////////////////////////////////////////////////// // Methods private void onHandleNext() { if (listener != null) { if (destinationAccount == null) { createToAccountAndProceed(); } else { if (everythingValid()) { Keyboard.hideKeyboard(getActivity()); listener.onMakeTrade(sourceAccount, destinationAccount, sendAmount); } else { Toast.makeText(getActivity(), R.string.amount_error, Toast.LENGTH_LONG).show(); } } } } private void createToAccountAndProceed() { if (destinationType == null) { Toast.makeText(getActivity(), R.string.error_generic, Toast.LENGTH_SHORT).show(); return; } createToAccountAndProceedDialog.show(getFragmentManager(), null); } /** * Start account creation task and proceed */ private void maybeStartAddCoinAndProceedTask(@Nullable String password) { if (addCoinAndProceedTask == null) { addCoinAndProceedTask = new AddCoinAndProceedTask(destinationType, wallet, password); addCoinAndProceedTask.execute(); } } private void addSourceListener() { sourceAccount.addEventListener(sourceAccountListener, Threading.SAME_THREAD); onWalletUpdate(); } private void removeSourceListener() { sourceAccount.removeEventListener(sourceAccountListener); sourceAccountListener.removeCallbacks(); } /** * Start polling for the market information of the current pair, if it is already stated this * call does nothing */ private void startPolling() { if (timer == null) { ShapeShift shapeShift = application.getShapeShift(); pollTask = new MyMarketInfoPollTask(handler, shapeShift, getPair()); 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; } } /** * Updates the spinners to include only available and supported coins */ private void updateAvailableCoins(ShapeShiftCoins availableCoins) { List<CoinType> supportedTypes = getSupportedTypes(availableCoins.availableCoinTypes); List<WalletAccount> allAccounts = application.getAllAccounts(); sourceAdapter.update(allAccounts, supportedTypes, false); List<CoinType> sourceTypes = sourceAdapter.getTypes(); // No supported source accounts found if (sourceTypes.isEmpty()) { new AlertDialog.Builder(getActivity()) .setTitle(R.string.trade_error) .setMessage(R.string.trade_error_no_supported_source_accounts) .setPositiveButton(R.string.button_ok, null) .create().show(); return; } if (sourceSpinner.getSelectedItemPosition() == -1) { sourceSpinner.setSelection(0); } CoinType sourceType = ((AvailableAccountsAdaptor.Entry) sourceSpinner.getSelectedItem()).getType(); // If we have only one source type, remove it as a destination if (sourceTypes.size() == 1) { List<CoinType> typesWithoutSourceType = Lists.newArrayList(supportedTypes); typesWithoutSourceType.remove(sourceType); destinationAdapter.update(allAccounts, typesWithoutSourceType, true); } else { destinationAdapter.update(allAccounts, supportedTypes, true); } if (destinationSpinner.getSelectedItemPosition() == -1) { for (AvailableAccountsAdaptor.Entry entry : destinationAdapter.getEntries()) { // Select the first item that is of a different type than the source if (!sourceType.equals(entry.getType())) { int selectionIndex = destinationAdapter.getAccountOrTypePosition( entry.accountOrCoinType); destinationSpinner.setSelection(selectionIndex); break; } } } } /** * Show a no connectivity error */ private void showInitialTaskErrorDialog(String error) { if (getActivity() == null) { return; } DialogBuilder builder; if (error == null) { builder = DialogBuilder.warn(getActivity(), R.string.trade_warn_no_connection_title); builder.setMessage(R.string.trade_warn_no_connection_message); } else { builder = DialogBuilder.warn(getActivity(), R.string.trade_error); builder.setMessage(R.string.trade_error_service_not_available); } builder.setNegativeButton(R.string.button_dismiss, null); builder.setPositiveButton(R.string.button_retry, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { initialTask = null; maybeStartInitialTask(); } }); builder.create().show(); } /** * Returns a list of the supported coins from the list of the available coins */ private List<CoinType> getSupportedTypes(List<CoinType> availableCoins) { ImmutableList.Builder<CoinType> builder = ImmutableList.builder(); for (CoinType supportedType : Constants.SUPPORTED_COINS) { if (availableCoins.contains(supportedType)) { builder.add(supportedType); } } return builder.build(); } private void refreshStartInitialTask() { if (initialTask != null) { initialTask.cancel(true); initialTask = null; } maybeStartInitialTask(); } private void maybeStartInitialTask() { if (initialTask == null) { initialTask = new InitialCheckTask(); initialTask.execute(); } } /** * Starts a new task to query about the market of the currently selected pair. * Notes: * - If a task is already running, this call will cancel it. * - If the fragment is detached, it will not run. */ private void startMarketInfoTask() { if (marketTask != null) { marketTask.cancel(true); marketTask = null; } if (getActivity() != null) { marketTask = new MarketInfoTask(handler, application.getShapeShift(), getPair()); marketTask.execute(); } } /** * Get the current source and destination pair */ private String getPair() { return ShapeShift.getPair(sourceAccount.getCoinType(), destinationType); } /** * 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 not current pair, do nothing if (!marketInfo.isPair(sourceAccount.getCoinType(), destinationType)) return; maximumDeposit = marketInfo.limit; minimumDeposit = marketInfo.minimum; lastRate = marketInfo.rate; amountCalculatorLink.setExchangeRate(lastRate); if (amountCalculatorLink.isEmpty() && lastRate != null) { Value hintValue = sourceAccount.getCoinType().oneCoin(); Value exchangedValue = lastRate.convert(hintValue); Value minerFee100 = marketInfo.rate.minerFee.multiply(100); // If hint value is too small, make it higher to get a no zero exchanged value and // at least 10 times higher than the miner fee for (int tries = 8; tries > 0 && (exchangedValue.isZero() || exchangedValue.compareTo(minerFee100) < 0); tries--) { hintValue = hintValue.multiply(10); exchangedValue = lastRate.convert(hintValue); } amountCalculatorLink.setExchangeRateHints(hintValue); } } /** * Get the item selected listener for the source spinner. It will swap the accounts if the * destination account is the same as the new source account. */ private AdapterView.OnItemSelectedListener getSourceSpinnerListener() { return new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { AvailableAccountsAdaptor.Entry entry = (AvailableAccountsAdaptor.Entry) parent.getSelectedItem(); if (entry.accountOrCoinType instanceof WalletAccount) { WalletAccount newSource = (WalletAccount) entry.accountOrCoinType; // If same account selected, do nothing if (newSource.isEquals(sourceAccount)) return; // If new source and destination are the same, swap accounts if (destinationAccount != null && destinationAccount.isType(newSource)) { // Swap accounts setDestinationSpinner(sourceAccount); setDestination(sourceAccount, false); } setSource(newSource, true); } else { // Should not happen as "source" is always an account throw new IllegalStateException("Unexpected class: " + entry.accountOrCoinType.getClass()); } } @Override public void onNothingSelected(AdapterView<?> parent) {} }; } /** * Get the item selected listener for the destination spinner. It will swap the accounts if the * source account is the same as the new destination account. */ private AdapterView.OnItemSelectedListener getDestinationSpinnerListener() { return new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { AvailableAccountsAdaptor.Entry entry = (AvailableAccountsAdaptor.Entry) parent.getSelectedItem(); if (entry.accountOrCoinType instanceof WalletAccount) { WalletAccount newDestination = (WalletAccount) entry.accountOrCoinType; // If same account selected, do nothing if (newDestination.isEquals(destinationAccount)) return; // If new destination and source are the same, swap accounts if (destinationAccount != null && sourceAccount.isType(newDestination)) { // Swap accounts setSourceSpinner(destinationAccount); setSource(destinationAccount, false); } setDestination(newDestination, true); } else if (entry.accountOrCoinType instanceof CoinType) { setDestination((CoinType) entry.accountOrCoinType, true); } else { // Should not happen throw new IllegalStateException("Unexpected class: " + entry.accountOrCoinType.getClass()); } } @Override public void onNothingSelected(AdapterView<?> parent) {} }; } /** * Selects an account on the sourceSpinner without calling the callback. If no account found in * the adaptor, does not do anything */ private void setSourceSpinner(WalletAccount account) { int newPosition = sourceAdapter.getAccountOrTypePosition(account); if (newPosition >= 0) { AdapterView.OnItemSelectedListener cb = sourceSpinner.getOnItemSelectedListener(); sourceSpinner.setOnItemSelectedListener(null); sourceSpinner.setSelection(newPosition); sourceSpinner.setOnItemSelectedListener(cb); } } /** * Selects an account on the destinationSpinner without calling the callback. If no account * found in the adaptor, does not do anything */ private void setDestinationSpinner(Object accountOrType) { int newPosition = destinationAdapter.getAccountOrTypePosition(accountOrType); if (newPosition >= 0) { AdapterView.OnItemSelectedListener cb = destinationSpinner.getOnItemSelectedListener(); destinationSpinner.setOnItemSelectedListener(null); destinationSpinner.setSelection(newPosition); destinationSpinner.setOnItemSelectedListener(cb); } } /** * Sets the source account and makes a network call to ask about the new pair. * Note: this does not update the source spinner, use {@link #setSourceSpinner(WalletAccount)} */ private void setSource(WalletAccount account, boolean startNetworkTask) { removeSourceListener(); sourceAccount = account; addSourceListener(); sourceAmountView.reset(); sourceAmountView.setType(sourceAccount.getCoinType()); sourceAmountView.setFormat(sourceAccount.getCoinType().getMonetaryFormat()); amountCalculatorLink.setExchangeRate(null); minimumDeposit = null; maximumDeposit = null; updateOptionsMenu(); if (startNetworkTask) { startMarketInfoTask(); if (pollTask != null) pollTask.updatePair(getPair()); application.maybeConnectAccount(sourceAccount); } } /** * Sets the destination account and makes a network call to ask about the new pair. * Note: this does not update the destination spinner, use * {@link #setDestinationSpinner(Object)} */ private void setDestination(WalletAccount account, boolean startNetworkTask) { setDestination(account.getCoinType(), false); destinationAccount = account; updateOptionsMenu(); if (startNetworkTask) { startMarketInfoTask(); if (pollTask != null) pollTask.updatePair(getPair()); } } /** * Sets the destination coin type and makes a network call to ask about the new pair. * Note: this does not update the destination spinner, use * {@link #setDestinationSpinner(Object)} */ private void setDestination(CoinType type, boolean startNetworkTask) { destinationAccount = null; destinationType = type; destinationAmountView.reset(); destinationAmountView.setType(destinationType); destinationAmountView.setFormat(destinationType.getMonetaryFormat()); amountCalculatorLink.setExchangeRate(null); minimumDeposit = null; maximumDeposit = null; updateOptionsMenu(); if (startNetworkTask) { startMarketInfoTask(); if (pollTask != null) pollTask.updatePair(getPair()); } } /** * Swap the source & destination accounts. * Note: this works if the destination is an account, not a CoinType. */ private void swapAccounts() { if (isSwapAccountPossible()) { WalletAccount newSource = destinationAccount; WalletAccount newDestination = sourceAccount; setSourceSpinner(newSource); setDestinationSpinner(newDestination); setSource(newSource, false); setDestination(newDestination, true); } else { // Should not happen as we need to first check if isSwapAccountPossible() before showing // a swap action to the user Toast.makeText(getActivity(), R.string.error_generic, Toast.LENGTH_SHORT).show(); } } /** * Check is if possible to perform the {@link #swapAccounts()} action */ private boolean isSwapAccountPossible() { return destinationAccount != null; } /** * Updates the options menu to take in to account the new selected accounts types, i.e. disable * the swap action */ private void updateOptionsMenu() { if (actionSwapMenu != null) { actionSwapMenu.setEnabled(isSwapAccountPossible()); } } private void updateBalance() { lastBalance = sourceAccount.getBalance(); } private void onWalletUpdate() { updateBalance(); validateAmount(); } /** * 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.isDust(); // Check if within min & max deposit limits if (isWithinLimits && minimumDeposit != null && maximumDeposit != null && minimumDeposit.isOfType(amount) && maximumDeposit.isOfType(amount)) { isWithinLimits = amount.within(minimumDeposit, maximumDeposit); } // 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)) < 0; } /** * Get the lowest deposit or withdraw for the provided amount type */ private Value getLowestAmount(Value amount) { Value min = amount.type.minNonDust(); if (minimumDeposit != null) { if (minimumDeposit.isOfType(min)) { min = Value.max(minimumDeposit, min); } else if (lastRate != null && lastRate.canConvert(amount.type, minimumDeposit.type)) { min = Value.max(lastRate.convert(minimumDeposit), min); } } return min; } /** * Check if the amount is valid */ private boolean isAmountValid(Value amount) { boolean isValid = amount != null && !amount.isDust(); if (isValid && amount.isOfType(sourceAccount.getCoinType())) { isValid = isAmountWithinLimits(amount); } return isValid; } /** * {@inheritDoc #validateAmount(boolean)} */ private void validateAmount() { validateAmount(false); } /** * Validate amount and show errors if needed */ private void validateAmount(boolean isTyping) { Value depositAmount = amountCalculatorLink.getPrimaryAmount(); Value withdrawAmount = amountCalculatorLink.getSecondaryAmount(); Value requestedAmount = amountCalculatorLink.getRequestedAmount(); if (isAmountValid(depositAmount) && isAmountValid(withdrawAmount)) { sendAmount = requestedAmount; amountError.setVisibility(View.GONE); // Show warning that fees apply when entered the full amount inside the pocket if (canCompare(lastBalance, depositAmount) && lastBalance.compareTo(depositAmount) == 0) { amountWarning.setText(R.string.amount_warn_fees_apply); amountWarning.setVisibility(View.VISIBLE); } else { amountWarning.setVisibility(View.GONE); } } else { amountWarning.setVisibility(View.GONE); sendAmount = null; boolean showErrors = shouldShowErrors(isTyping, depositAmount) || shouldShowErrors(isTyping, withdrawAmount); // ignore printing errors for null and zero amounts if (showErrors) { if (depositAmount == null || withdrawAmount == null) { amountError.setText(R.string.amount_error); } else if (depositAmount.isNegative() || withdrawAmount.isNegative()) { amountError.setText(R.string.amount_error_negative); } else if (!isAmountWithinLimits(depositAmount) || !isAmountWithinLimits(withdrawAmount)) { String message = getString(R.string.error_generic); // If the amount is dust or lower than the deposit limit if (isAmountTooSmall(depositAmount) || isAmountTooSmall(withdrawAmount)) { Value minimumDeposit = getLowestAmount(depositAmount); Value minimumWithdraw = getLowestAmount(withdrawAmount); message = getString(R.string.trade_error_min_limit, minimumDeposit.toFriendlyString() + " (" + minimumWithdraw .toFriendlyString() + ")"); } else { // If we have the amount if (canCompare(lastBalance, depositAmount) && depositAmount.compareTo(lastBalance) > 0) { message = getString(R.string.amount_error_not_enough_money, lastBalance.toFriendlyString()); } if (canCompare(maximumDeposit, depositAmount) && depositAmount.compareTo(maximumDeposit) > 0) { String maxDepositString = maximumDeposit.toFriendlyString(); if (lastRate != null && lastRate.canConvert(maximumDeposit.type, destinationType)) { maxDepositString += " (" + lastRate.convert(maximumDeposit) .toFriendlyString() + ")"; } message = getString(R.string.trade_error_max_limit, maxDepositString); } } 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); } } updateNextButtonState(); } // TODO implement private boolean isOutputsValid() { return true; } private boolean everythingValid() { return isOutputsValid() && isAmountValid(sendAmount); } private void updateNextButtonState() { // nextButton.setEnabled(everythingValid()); } /** * Decide if should show errors in the UI. */ private boolean shouldShowErrors(boolean isTyping, Value amountParsed) { if (canCompare(amountParsed, lastBalance) && amountParsed.compareTo(lastBalance) >= 0) { return true; } if (isTyping) return false; if (amountCalculatorLink.isEmpty()) return false; if (amountParsed != null && amountParsed.isZero()) return false; return true; } //////////////////////////////////////////////////////////////////////////////////////////////// // Public classes and interfaces public interface Listener { void onMakeTrade(WalletAccount fromAccount, WalletAccount toAccount, Value amount); } //////////////////////////////////////////////////////////////////////////////////////////////// // Private classes private static class AmountListener implements AmountEditView.Listener { private final Handler handler; private AmountListener(Handler handler) { this.handler = handler; } @Override public void changed() { handler.sendMessage(handler.obtainMessage(VALIDATE_AMOUNT, true)); } @Override public void focusChanged(final boolean hasFocus) { if (!hasFocus) { handler.sendMessage(handler.obtainMessage(VALIDATE_AMOUNT, false)); } } } private static class AccountListener extends ThrottlingWalletChangeListener { private final Handler handler; private AccountListener(Handler handler) { this.handler = handler; } @Override public void onThrottledWalletChanged() { handler.sendEmptyMessage(UPDATE_WALLET); } } /** * The fragment handler */ private static class MyHandler extends WeakHandler<TradeSelectFragment> { public MyHandler(TradeSelectFragment referencingObject) { super(referencingObject); } @Override protected void weakHandleMessage(TradeSelectFragment ref, Message msg) { switch (msg.what) { case UPDATE_MARKET: ref.onMarketUpdate((ShapeShiftMarketInfo) msg.obj); break; case UPDATE_MARKET_ERROR: String errorMessage = ref.getString(R.string.trade_error_market_info, ref.sourceAccount.getCoinType().getName(), ref.destinationType.getName()); Toast.makeText(ref.getActivity(), errorMessage, Toast.LENGTH_LONG).show(); break; case UPDATE_WALLET: ref.onWalletUpdate(); break; case VALIDATE_AMOUNT: ref.validateAmount((Boolean) msg.obj); break; case INITIAL_TASK_ERROR: ref.showInitialTaskErrorDialog((String) msg.obj); break; case UPDATE_AVAILABLE_COINS: ref.updateAvailableCoins((ShapeShiftCoins) msg.obj); ref.startMarketInfoTask(); break; } } } private class InitialCheckTask extends AsyncTask<Void, Void, Exception> { private Dialogs.ProgressDialogFragment busyDialog; private ShapeShiftCoins shapeShiftCoins; @Override protected void onPreExecute() { busyDialog = Dialogs.ProgressDialogFragment.newInstance( getString(R.string.contacting_exchange)); // busyDialog.setCancelable(false); busyDialog.show(getFragmentManager(), null); } @Override protected Exception doInBackground(Void... params) { if (!application.isConnected()) { return new ShapeShiftException("No connection"); } try { shapeShiftCoins = application.getShapeShift().getCoins(); return null; } catch (Exception e) { return e; } } @Override protected void onPostExecute(Exception error) { busyDialog.dismissAllowingStateLoss(); if (error != null) { log.warn("Could not get ShapeShift coins", error); handler.sendMessage(handler.obtainMessage(INITIAL_TASK_ERROR, error.getMessage())); } else { if (shapeShiftCoins.isError) { log.warn("Could not get ShapeShift coins: {}", shapeShiftCoins.errorMessage); handler.sendMessage(handler.obtainMessage(INITIAL_TASK_ERROR, shapeShiftCoins.errorMessage)); } else { handler.sendMessage(handler.obtainMessage(UPDATE_AVAILABLE_COINS, shapeShiftCoins)); } } } } /** * Task to query about the market of a particular pair */ private static class MarketInfoTask extends AsyncTask<Void, Void, ShapeShiftMarketInfo> { final ShapeShift shapeShift; final String pair; final Handler handler; private MarketInfoTask(Handler handler, ShapeShift shift, String pair) { this.shapeShift = shift; this.handler = handler; this.pair = pair; } @Override protected ShapeShiftMarketInfo doInBackground(Void... params) { return MarketInfoPollTask.getMarketInfoSync(shapeShift, pair); } @Override protected void onPostExecute(ShapeShiftMarketInfo marketInfo) { if (marketInfo != null) { handler.sendMessage(handler.obtainMessage(UPDATE_MARKET, marketInfo)); } else { handler.sendEmptyMessage(UPDATE_MARKET_ERROR); } } } 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 class AddCoinAndProceedTask extends AddCoinTask { private Dialogs.ProgressDialogFragment verifyDialog; public AddCoinAndProceedTask(CoinType type, Wallet wallet, @Nullable String password) { super(type, wallet, password); } @Override protected void onPreExecute() { verifyDialog = Dialogs.ProgressDialogFragment.newInstance( getResources().getString(R.string.adding_coin_working, type.getName())); verifyDialog.show(getFragmentManager(), null); } @Override protected void onPostExecute(Exception e, WalletAccount newAccount) { verifyDialog.dismiss(); if (e != null) { Toast.makeText(getActivity(), R.string.error_generic, Toast.LENGTH_LONG).show(); } destinationAccount = newAccount; destinationType = newAccount.getCoinType(); onHandleNext(); addCoinAndProceedTask = null; } } private DialogFragment createToAccountAndProceedDialog = new DialogFragment() { @Override @NonNull public Dialog onCreateDialog(Bundle savedInstanceState) { final LayoutInflater inflater = LayoutInflater.from(getActivity()); final View view = inflater.inflate(R.layout.get_password_dialog, null); final TextView passwordView = (TextView) view.findViewById(R.id.password); // If not encrypted, don't ask the password if (!wallet.isEncrypted()) { view.findViewById(R.id.password_message).setVisibility(View.GONE); passwordView.setVisibility(View.GONE); } String title = getString(R.string.adding_coin_confirmation_title, destinationType.getName()); return new DialogBuilder(getActivity()).setTitle(title).setView(view) .setNegativeButton(R.string.button_cancel, null) .setPositiveButton(R.string.button_add, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (wallet.isEncrypted()) { maybeStartAddCoinAndProceedTask(passwordView.getText().toString()); } else { maybeStartAddCoinAndProceedTask(null); } } }).create(); } }; }