devcoin.wallet.ui.SendCoinsFragment.java Source code

Java tutorial

Introduction

Here is the source code for devcoin.wallet.ui.SendCoinsFragment.java

Source

/*
 * Copyright 2011-2014 the original author or authors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package devcoin.wallet.ui;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.content.*;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.database.Cursor;
import android.media.RingtoneManager;
import android.net.Uri;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.preference.PreferenceManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;
import android.widget.*;
import android.widget.CompoundButton.OnCheckedChangeListener;
import com.actionbarsherlock.app.SherlockFragment;
import com.actionbarsherlock.view.ActionMode;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import com.google.devcoin.core.*;
import com.google.devcoin.core.TransactionConfidence.ConfidenceType;
import com.google.devcoin.core.Wallet.BalanceType;
import com.google.devcoin.core.Wallet.SendRequest;
import devcoin.wallet.*;
import devcoin.wallet.ExchangeRatesProvider.ExchangeRate;
import devcoin.wallet.integration.android.BitcoinIntegration;
import devcoin.wallet.offline.SendBluetoothTask;
import devcoin.wallet.ui.InputParser.StringInputParser;
import devcoin.wallet.util.GenericUtils;
import devcoin.wallet.util.WalletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.math.BigInteger;

/**
 * @author Andreas Schildbach
 */
public final class SendCoinsFragment extends SherlockFragment {
    private AbstractBindServiceActivity activity;
    private WalletApplication application;
    private Wallet wallet;
    private ContentResolver contentResolver;
    private LoaderManager loaderManager;
    private SharedPreferences prefs;
    @CheckForNull
    private BluetoothAdapter bluetoothAdapter;

    private int btcPrecision;
    private int btcShift;

    private final Handler handler = new Handler();
    private HandlerThread backgroundThread;
    private Handler backgroundHandler;

    private AutoCompleteTextView receivingAddressView;
    private View receivingStaticView;
    private TextView receivingStaticAddressView;
    private TextView receivingStaticLabelView;
    private CheckBox bluetoothEnableView;

    private TextView bluetoothMessageView;
    private ListView sentTransactionView;
    private TransactionsListAdapter sentTransactionListAdapter;
    private Button viewGo;
    private Button viewCancel;

    private TextView popupMessageView;
    private View popupAvailableView;
    private PopupWindow popupWindow;

    private CurrencyCalculatorLink amountCalculatorLink;

    private MenuItem scanAction;

    private AddressAndLabel validatedAddress = null;
    private boolean isValidAmounts = false;

    @CheckForNull
    private String bluetoothMac;
    private Boolean bluetoothAck = null;

    private State state = State.INPUT;
    private Transaction sentTransaction = null;

    private static final int ID_RATE_LOADER = 0;

    private static final int REQUEST_CODE_SCAN = 0;
    private static final int REQUEST_CODE_ENABLE_BLUETOOTH = 1;

    private static final Logger log = LoggerFactory.getLogger(SendCoinsFragment.class);

    private enum State {
        INPUT, PREPARATION, SENDING, SENT, FAILED
    }

    private final class ReceivingAddressListener implements OnFocusChangeListener, TextWatcher {
        @Override
        public void onFocusChange(final View v, final boolean hasFocus) {
            if (!hasFocus)
                validateReceivingAddress(true);
        }

        @Override
        public void afterTextChanged(final Editable s) {
            dismissPopup();

            validateReceivingAddress(false);
        }

        @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) {
        }
    }

    private final ReceivingAddressListener receivingAddressListener = new ReceivingAddressListener();

    private final class ReceivingAddressActionMode implements ActionMode.Callback {
        @Override
        public boolean onCreateActionMode(final ActionMode mode, final Menu menu) {
            final MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.send_coins_address_context, menu);

            return true;
        }

        @Override
        public boolean onPrepareActionMode(final ActionMode mode, final Menu menu) {
            return false;
        }

        @Override
        public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) {
            switch (item.getItemId()) {
            case R.id.send_coins_address_context_edit_address:
                handleEditAddress();

                mode.finish();
                return true;

            case R.id.send_coins_address_context_clear:
                handleClear();

                mode.finish();
                return true;
            }

            return false;
        }

        @Override
        public void onDestroyActionMode(final ActionMode mode) {
            if (receivingStaticView.hasFocus())
                amountCalculatorLink.requestFocus();
        }

        private void handleEditAddress() {
            EditAddressBookEntryFragment.edit(getFragmentManager(), validatedAddress.address.toString());
        }

        private void handleClear() {
            // switch from static to input
            validatedAddress = null;
            receivingAddressView.setText(null);
            receivingStaticAddressView.setText(null);

            updateView();

            receivingAddressView.requestFocus();
        }
    }

    private final CurrencyAmountView.Listener amountsListener = new CurrencyAmountView.Listener() {
        @Override
        public void changed() {
            dismissPopup();

            validateAmounts(false);
        }

        @Override
        public void done() {
            validateAmounts(true);

            viewGo.requestFocusFromTouch();
        }

        @Override
        public void focusChanged(final boolean hasFocus) {
            if (!hasFocus) {
                validateAmounts(true);
            }
        }
    };

    private final ContentObserver contentObserver = new ContentObserver(handler) {
        @Override
        public void onChange(final boolean selfChange) {
            updateView();
        }
    };

    private final TransactionConfidence.Listener sentTransactionConfidenceListener = new TransactionConfidence.Listener() {
        @Override
        public void onConfidenceChanged(final Transaction tx,
                final TransactionConfidence.Listener.ChangeReason reason) {
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    sentTransactionListAdapter.notifyDataSetChanged();

                    final TransactionConfidence confidence = sentTransaction.getConfidence();
                    final ConfidenceType confidenceType = confidence.getConfidenceType();
                    final int numBroadcastPeers = confidence.numBroadcastPeers();

                    if (state == State.SENDING) {
                        if (confidenceType == ConfidenceType.DEAD)
                            state = State.FAILED;
                        else if (numBroadcastPeers > 1 || confidenceType == ConfidenceType.BUILDING)
                            state = State.SENT;

                        updateView();
                    }

                    if (reason == ChangeReason.SEEN_PEERS && confidenceType == ConfidenceType.PENDING) {
                        // play sound effect
                        final int soundResId = getResources().getIdentifier(
                                "send_coins_broadcast_" + numBroadcastPeers, "raw", activity.getPackageName());
                        if (soundResId > 0)
                            RingtoneManager
                                    .getRingtone(activity, Uri.parse(
                                            "android.resource://" + activity.getPackageName() + "/" + soundResId))
                                    .play();
                    }
                }
            });
        }
    };

    private final LoaderCallbacks<Cursor> rateLoaderCallbacks = new LoaderManager.LoaderCallbacks<Cursor>() {
        @Override
        public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
            return new ExchangeRateLoader(activity);
        }

        @Override
        public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
            if (data != null) {
                data.moveToFirst();
                final ExchangeRate exchangeRate = ExchangeRatesProvider.getExchangeRate(data);

                if (state == State.INPUT)
                    amountCalculatorLink.setExchangeRate(exchangeRate);
            }
        }

        @Override
        public void onLoaderReset(final Loader<Cursor> loader) {
        }
    };

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);

        this.activity = (AbstractBindServiceActivity) activity;
        this.application = (WalletApplication) activity.getApplication();
        this.prefs = PreferenceManager.getDefaultSharedPreferences(activity);
        this.wallet = application.getWallet();
        this.contentResolver = activity.getContentResolver();
        this.loaderManager = getLoaderManager();
    }

    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setHasOptionsMenu(true);

        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        backgroundThread = new HandlerThread("backgroundThread", Process.THREAD_PRIORITY_BACKGROUND);
        backgroundThread.start();
        backgroundHandler = new Handler(backgroundThread.getLooper());

        final String precision = prefs.getString(Constants.PREFS_KEY_BTC_PRECISION,
                Constants.PREFS_DEFAULT_BTC_PRECISION);
        btcPrecision = precision.charAt(0) - '0';
        btcShift = precision.length() == 3 ? precision.charAt(2) - '0' : 0;
    }

    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
            final Bundle savedInstanceState) {
        final View view = inflater.inflate(R.layout.send_coins_fragment, container);

        receivingAddressView = (AutoCompleteTextView) view.findViewById(R.id.send_coins_receiving_address);
        receivingAddressView.setAdapter(new AutoCompleteAddressAdapter(activity, null));
        receivingAddressView.setOnFocusChangeListener(receivingAddressListener);
        receivingAddressView.addTextChangedListener(receivingAddressListener);

        receivingStaticView = view.findViewById(R.id.send_coins_receiving_static);
        receivingStaticAddressView = (TextView) view.findViewById(R.id.send_coins_receiving_static_address);
        receivingStaticLabelView = (TextView) view.findViewById(R.id.send_coins_receiving_static_label);

        receivingStaticView.setOnFocusChangeListener(new OnFocusChangeListener() {
            private ActionMode actionMode;

            @Override
            public void onFocusChange(final View v, final boolean hasFocus) {
                if (hasFocus)
                    actionMode = activity.startActionMode(new ReceivingAddressActionMode());
                else
                    actionMode.finish();
            }
        });

        final CurrencyAmountView btcAmountView = (CurrencyAmountView) view.findViewById(R.id.send_coins_amount_btc);
        btcAmountView.setCurrencySymbol(btcShift == 0 ? Constants.CURRENCY_CODE_BTC : Constants.CURRENCY_CODE_MBTC);
        btcAmountView.setInputPrecision(btcShift == 0 ? Constants.BTC_MAX_PRECISION : Constants.MBTC_MAX_PRECISION);
        btcAmountView.setHintPrecision(btcPrecision);
        btcAmountView.setShift(btcShift);

        final CurrencyAmountView localAmountView = (CurrencyAmountView) view
                .findViewById(R.id.send_coins_amount_local);
        localAmountView.setInputPrecision(Constants.LOCAL_PRECISION);
        localAmountView.setHintPrecision(Constants.LOCAL_PRECISION);
        amountCalculatorLink = new CurrencyCalculatorLink(btcAmountView, localAmountView);

        bluetoothEnableView = (CheckBox) view.findViewById(R.id.send_coins_bluetooth_enable);
        bluetoothEnableView.setChecked(bluetoothAdapter != null && bluetoothAdapter.isEnabled());
        bluetoothEnableView.setOnCheckedChangeListener(new OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked) {
                if (isChecked && !bluetoothAdapter.isEnabled()) {
                    // try to enable bluetooth
                    startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE),
                            REQUEST_CODE_ENABLE_BLUETOOTH);
                }
            }
        });

        bluetoothMessageView = (TextView) view.findViewById(R.id.send_coins_bluetooth_message);

        sentTransactionView = (ListView) view.findViewById(R.id.send_coins_sent_transaction);
        sentTransactionListAdapter = new TransactionsListAdapter(activity, wallet, application.maxConnectedPeers(),
                false);
        sentTransactionView.setAdapter(sentTransactionListAdapter);

        viewGo = (Button) view.findViewById(R.id.send_coins_go);
        viewGo.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                validateReceivingAddress(true);
                validateAmounts(true);

                if (everythingValid())
                    handleGo();
            }
        });

        viewCancel = (Button) view.findViewById(R.id.send_coins_cancel);
        viewCancel.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                if (state == State.INPUT)
                    activity.setResult(Activity.RESULT_CANCELED);

                activity.finish();
            }
        });

        popupMessageView = (TextView) inflater.inflate(R.layout.send_coins_popup_message, container);

        popupAvailableView = inflater.inflate(R.layout.send_coins_popup_available, container);

        if (savedInstanceState != null) {
            restoreInstanceState(savedInstanceState);
        } else {
            final Intent intent = activity.getIntent();
            final String action = intent.getAction();
            final Uri intentUri = intent.getData();
            final String scheme = intentUri != null ? intentUri.getScheme() : null;

            if ((Intent.ACTION_VIEW.equals(action) || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action))
                    && intentUri != null && "devcoin".equals(scheme))
                initStateFromBitcoinUri(intentUri);
            else if (intent.hasExtra(SendCoinsActivity.INTENT_EXTRA_ADDRESS))
                initStateFromIntentExtras(intent.getExtras());
        }

        return view;
    }

    private void initStateFromIntentExtras(@Nonnull final Bundle extras) {
        final String address = extras.getString(SendCoinsActivity.INTENT_EXTRA_ADDRESS);
        final String addressLabel = extras.getString(SendCoinsActivity.INTENT_EXTRA_ADDRESS_LABEL);
        final BigInteger amount = (BigInteger) extras.getSerializable(SendCoinsActivity.INTENT_EXTRA_AMOUNT);
        final String bluetoothMac = extras.getString(SendCoinsActivity.INTENT_EXTRA_BLUETOOTH_MAC);

        update(address, addressLabel, amount, bluetoothMac);
    }

    private void initStateFromBitcoinUri(@Nonnull final Uri bitcoinUri) {
        final String input = bitcoinUri.toString();

        new StringInputParser(input) {
            @Override
            protected void bitcoinRequest(final Address address, final String addressLabel, final BigInteger amount,
                    final String bluetoothMac) {
                update(address.toString(), addressLabel, amount, bluetoothMac);
            }

            @Override
            protected void directTransaction(final Transaction transaction) {
                cannotClassify(input);
            }

            @Override
            protected void error(final int messageResId, final Object... messageArgs) {
                dialog(activity, dismissListener, 0, messageResId, messageArgs);
            }

            private final DialogInterface.OnClickListener dismissListener = new DialogInterface.OnClickListener() {
                @Override
                public void onClick(final DialogInterface dialog, final int which) {
                    activity.finish();
                }
            };
        }.parse();
    }

    @Override
    public void onResume() {
        super.onResume();

        contentResolver.registerContentObserver(AddressBookProvider.contentUri(activity.getPackageName()), true,
                contentObserver);

        amountCalculatorLink.setListener(amountsListener);

        loaderManager.initLoader(ID_RATE_LOADER, null, rateLoaderCallbacks);

        updateView();
    }

    @Override
    public void onPause() {
        loaderManager.destroyLoader(ID_RATE_LOADER);

        amountCalculatorLink.setListener(null);

        contentResolver.unregisterContentObserver(contentObserver);

        super.onPause();
    }

    @Override
    public void onDetach() {
        handler.removeCallbacksAndMessages(null);

        super.onDetach();
    }

    @Override
    public void onDestroy() {
        backgroundThread.getLooper().quit();

        if (sentTransaction != null)
            sentTransaction.getConfidence().removeEventListener(sentTransactionConfidenceListener);

        super.onDestroy();
    }

    @Override
    public void onSaveInstanceState(final Bundle outState) {
        super.onSaveInstanceState(outState);

        saveInstanceState(outState);
    }

    private void saveInstanceState(final Bundle outState) {
        outState.putSerializable("state", state);

        if (validatedAddress != null)
            outState.putParcelable("validated_address", validatedAddress);

        outState.putBoolean("is_valid_amounts", isValidAmounts);

        if (sentTransaction != null)
            outState.putSerializable("sent_transaction_hash", sentTransaction.getHash());

        outState.putString("bluetooth_mac", bluetoothMac);

        if (bluetoothAck != null)
            outState.putBoolean("bluetooth_ack", bluetoothAck);
    }

    private void restoreInstanceState(final Bundle savedInstanceState) {
        state = (State) savedInstanceState.getSerializable("state");

        validatedAddress = savedInstanceState.getParcelable("validated_address");

        isValidAmounts = savedInstanceState.getBoolean("is_valid_amounts");

        if (savedInstanceState.containsKey("sent_transaction_hash")) {
            sentTransaction = wallet
                    .getTransaction((Sha256Hash) savedInstanceState.getSerializable("sent_transaction_hash"));
            sentTransaction.getConfidence().addEventListener(sentTransactionConfidenceListener);
        }

        bluetoothMac = savedInstanceState.getString("bluetooth_mac");

        if (savedInstanceState.containsKey("bluetooth_ack"))
            bluetoothAck = savedInstanceState.getBoolean("bluetooth_ack");
    }

    @Override
    public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
        if (requestCode == REQUEST_CODE_SCAN) {
            if (resultCode == Activity.RESULT_OK) {
                final String input = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);

                new StringInputParser(input) {
                    @Override
                    protected void bitcoinRequest(final Address address, final String addressLabel,
                            final BigInteger amount, final String bluetoothMac) {
                        SendCoinsActivity.start(activity, address != null ? address.toString() : null, addressLabel,
                                amount, bluetoothMac);
                    }

                    @Override
                    protected void directTransaction(final Transaction transaction) {
                        cannotClassify(input);
                    }

                    @Override
                    protected void error(final int messageResId, final Object... messageArgs) {
                        dialog(activity, null, R.string.button_scan, messageResId, messageArgs);
                    }
                }.parse();
            }
        } else if (requestCode == REQUEST_CODE_ENABLE_BLUETOOTH) {
            bluetoothEnableView.setChecked(resultCode == Activity.RESULT_OK);
        }
    }

    @Override
    public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
        inflater.inflate(R.menu.send_coins_fragment_options, menu);

        scanAction = menu.findItem(R.id.send_coins_options_scan);

        final PackageManager pm = activity.getPackageManager();
        scanAction.setVisible(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)
                || pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT));

        super.onCreateOptionsMenu(menu, inflater);
    }

    @Override
    public boolean onOptionsItemSelected(final MenuItem item) {
        switch (item.getItemId()) {
        case R.id.send_coins_options_scan:
            handleScan();
            return true;

        case R.id.send_coins_options_empty:
            handleEmpty();
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void validateReceivingAddress(final boolean popups) {
        try {
            final String addressStr = receivingAddressView.getText().toString().trim();
            if (!addressStr.isEmpty()) {
                final NetworkParameters addressParams = Address.getParametersFromAddress(addressStr);
                if (addressParams != null && !addressParams.equals(Constants.NETWORK_PARAMETERS)) {
                    // address is valid, but from different known network
                    if (popups)
                        popupMessage(receivingAddressView,
                                getString(R.string.send_coins_fragment_receiving_address_error_cross_network,
                                        addressParams.getId()));
                } else if (addressParams == null) {
                    // address is valid, but from different unknown network
                    if (popups)
                        popupMessage(receivingAddressView, getString(
                                R.string.send_coins_fragment_receiving_address_error_cross_network_unknown));
                } else {
                    // valid address
                    final String label = AddressBookProvider.resolveLabel(activity, addressStr);
                    validatedAddress = new AddressAndLabel(Constants.NETWORK_PARAMETERS, addressStr, label);
                    receivingAddressView.setText(null);
                }
            } else {
                // empty field should not raise error message
            }
        } catch (final AddressFormatException x) {
            // could not decode address at all
            if (popups)
                popupMessage(receivingAddressView, getString(R.string.send_coins_fragment_receiving_address_error));
        }

        updateView();
    }

    private void validateAmounts(final boolean popups) {
        isValidAmounts = false;

        final BigInteger amount = amountCalculatorLink.getAmount();

        if (amount == null) {
            // empty amount
            if (popups)
                popupMessage(amountCalculatorLink.activeView(),
                        getString(R.string.send_coins_fragment_amount_empty));
        } else if (amount.signum() > 0) {
            final BigInteger estimated = wallet.getBalance(BalanceType.ESTIMATED);
            final BigInteger available = wallet.getBalance(BalanceType.AVAILABLE);
            final BigInteger pending = estimated.subtract(available);
            // TODO subscribe to wallet changes

            final BigInteger availableAfterAmount = available.subtract(amount);
            final boolean enoughFundsForAmount = availableAfterAmount.signum() >= 0;

            if (enoughFundsForAmount) {
                // everything fine
                isValidAmounts = true;
            } else {
                // not enough funds for amount
                if (popups)
                    popupAvailable(amountCalculatorLink.activeView(), available, pending);
            }
        } else {
            // invalid amount
            if (popups)
                popupMessage(amountCalculatorLink.activeView(),
                        getString(R.string.send_coins_fragment_amount_error));
        }

        updateView();
    }

    private void popupMessage(@Nonnull final View anchor, @Nonnull final String message) {
        dismissPopup();

        popupMessageView.setText(message);
        popupMessageView.setMaxWidth(getView().getWidth());

        popup(anchor, popupMessageView);
    }

    private void popupAvailable(@Nonnull final View anchor, @Nonnull final BigInteger available,
            @Nonnull final BigInteger pending) {
        dismissPopup();

        final CurrencyTextView viewAvailable = (CurrencyTextView) popupAvailableView
                .findViewById(R.id.send_coins_popup_available_amount);
        viewAvailable.setPrefix(btcShift == 0 ? Constants.CURRENCY_CODE_BTC : Constants.CURRENCY_CODE_MBTC);
        viewAvailable.setPrecision(btcPrecision, btcShift);
        viewAvailable.setAmount(available);

        final TextView viewPending = (TextView) popupAvailableView
                .findViewById(R.id.send_coins_popup_available_pending);
        viewPending.setVisibility(pending.signum() > 0 ? View.VISIBLE : View.GONE);
        final int precision = btcShift == 0 ? Constants.BTC_MAX_PRECISION : Constants.MBTC_MAX_PRECISION;
        viewPending.setText(getString(R.string.send_coins_fragment_pending,
                GenericUtils.formatValue(pending, precision, btcShift)));

        popup(anchor, popupAvailableView);
    }

    private void popup(@Nonnull final View anchor, @Nonnull final View contentView) {
        contentView.measure(MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0),
                MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0));

        popupWindow = new PopupWindow(contentView, contentView.getMeasuredWidth(), contentView.getMeasuredHeight(),
                false);
        popupWindow.showAsDropDown(anchor);

        // hack
        contentView.setBackgroundResource(
                popupWindow.isAboveAnchor() ? R.drawable.popup_frame_above : R.drawable.popup_frame_below);
    }

    private void dismissPopup() {
        if (popupWindow != null) {
            popupWindow.dismiss();
            popupWindow = null;
        }
    }

    private void handleGo() {
        state = State.PREPARATION;
        updateView();

        // create spend
        final BigInteger amount = amountCalculatorLink.getAmount();
        final SendRequest sendRequest = SendRequest.to(validatedAddress.address, amount);
        sendRequest.changeAddress = WalletUtils.pickOldestKey(wallet).toAddress(Constants.NETWORK_PARAMETERS);
        sendRequest.emptyWallet = amount.equals(wallet.getBalance(BalanceType.AVAILABLE));

        new SendCoinsOfflineTask(wallet, backgroundHandler) {
            @Override
            protected void onSuccess(final Transaction transaction) {
                sentTransaction = transaction;

                state = State.SENDING;
                updateView();

                sentTransaction.getConfidence().addEventListener(sentTransactionConfidenceListener);

                if (bluetoothAdapter != null && bluetoothAdapter.isEnabled() && bluetoothMac != null
                        && bluetoothEnableView.isChecked()) {
                    new SendBluetoothTask(bluetoothAdapter, backgroundHandler) {
                        @Override
                        protected void onResult(final boolean ack) {
                            bluetoothAck = ack;

                            if (state == State.SENDING)
                                state = State.SENT;

                            updateView();
                        }
                    }.send(bluetoothMac, transaction); // send asynchronously
                }

                application.broadcastTransaction(sentTransaction);

                final Intent result = new Intent();
                BitcoinIntegration.transactionHashToResult(result, sentTransaction.getHashAsString());
                activity.setResult(Activity.RESULT_OK, result);
            }

            @Override
            protected void onFailure() {
                state = State.FAILED;
                updateView();

                activity.longToast(R.string.send_coins_error_msg);
            }
        }.sendCoinsOffline(sendRequest); // send asynchronously
    }

    private void handleScan() {
        startActivityForResult(new Intent(activity, ScanActivity.class), REQUEST_CODE_SCAN);
    }

    private void handleEmpty() {
        final BigInteger available = wallet.getBalance(BalanceType.AVAILABLE);

        amountCalculatorLink.setBtcAmount(available);
    }

    public class AutoCompleteAddressAdapter extends CursorAdapter {
        public AutoCompleteAddressAdapter(final Context context, final Cursor c) {
            super(context, c);
        }

        @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(WalletUtils.formatHash(address, Constants.ADDRESS_FORMAT_GROUP_SIZE,
                    Constants.ADDRESS_FORMAT_LINE_SIZE));
        }

        @Override
        public CharSequence convertToString(final Cursor cursor) {
            return cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_ADDRESS));
        }

        @Override
        public Cursor runQueryOnBackgroundThread(final CharSequence constraint) {
            final Cursor cursor = activity.managedQuery(AddressBookProvider.contentUri(activity.getPackageName()),
                    null, AddressBookProvider.SELECTION_QUERY, new String[] { constraint.toString() }, null);
            return cursor;
        }
    }

    private void updateView() {
        if (validatedAddress != null) {
            receivingAddressView.setVisibility(View.GONE);

            receivingStaticView.setVisibility(View.VISIBLE);
            receivingStaticAddressView.setText(WalletUtils.formatAddress(validatedAddress.address,
                    Constants.ADDRESS_FORMAT_GROUP_SIZE, Constants.ADDRESS_FORMAT_LINE_SIZE));
            final String addressBookLabel = AddressBookProvider.resolveLabel(activity,
                    validatedAddress.address.toString());
            final String staticLabel;
            if (addressBookLabel != null)
                staticLabel = addressBookLabel;
            else if (validatedAddress.label != null)
                staticLabel = validatedAddress.label;
            else
                staticLabel = getString(R.string.address_unlabeled);
            receivingStaticLabelView.setText(staticLabel);
            receivingStaticLabelView.setTextColor(getResources()
                    .getColor(validatedAddress.label != null ? R.color.fg_significant : R.color.fg_insignificant));
        } else {
            receivingStaticView.setVisibility(View.GONE);

            receivingAddressView.setVisibility(View.VISIBLE);
        }

        receivingAddressView.setEnabled(state == State.INPUT);

        receivingStaticView.setEnabled(state == State.INPUT);

        amountCalculatorLink.setEnabled(state == State.INPUT);

        bluetoothEnableView
                .setVisibility(bluetoothAdapter != null && bluetoothMac != null ? View.VISIBLE : View.GONE);
        bluetoothEnableView.setEnabled(state == State.INPUT);

        if (sentTransaction != null) {
            final String precision = prefs.getString(Constants.PREFS_KEY_BTC_PRECISION,
                    Constants.PREFS_DEFAULT_BTC_PRECISION);
            final int btcPrecision = precision.charAt(0) - '0';
            final int btcShift = precision.length() == 3 ? precision.charAt(2) - '0' : 0;

            sentTransactionView.setVisibility(View.VISIBLE);
            sentTransactionListAdapter.setPrecision(btcPrecision, btcShift);
            sentTransactionListAdapter.replace(sentTransaction);
        } else {
            sentTransactionView.setVisibility(View.GONE);
            sentTransactionListAdapter.clear();
        }

        if (bluetoothAck != null) {
            bluetoothMessageView.setVisibility(View.VISIBLE);
            bluetoothMessageView.setText(bluetoothAck ? R.string.send_coins_fragment_bluetooth_ack
                    : R.string.send_coins_fragment_bluetooth_nack);
        } else {
            bluetoothMessageView.setVisibility(View.GONE);
        }

        viewCancel.setEnabled(state != State.PREPARATION);
        viewGo.setEnabled(everythingValid());

        if (state == State.INPUT) {
            viewCancel.setText(R.string.button_cancel);
            viewGo.setText(R.string.send_coins_fragment_button_send);
        } else if (state == State.PREPARATION) {
            viewCancel.setText(R.string.button_cancel);
            viewGo.setText(R.string.send_coins_preparation_msg);
        } else if (state == State.SENDING) {
            viewCancel.setText(R.string.send_coins_fragment_button_back);
            viewGo.setText(R.string.send_coins_sending_msg);
        } else if (state == State.SENT) {
            viewCancel.setText(R.string.send_coins_fragment_button_back);
            viewGo.setText(R.string.send_coins_sent_msg);
        } else if (state == State.FAILED) {
            viewCancel.setText(R.string.send_coins_fragment_button_back);
            viewGo.setText(R.string.send_coins_failed_msg);
        }

        if (scanAction != null)
            scanAction.setEnabled(state == State.INPUT);
    }

    private boolean everythingValid() {
        return state == State.INPUT && validatedAddress != null && isValidAmounts;
    }

    public void update(final String receivingAddress, final String receivingLabel,
            @Nullable final BigInteger amount, @Nullable final String bluetoothMac) {
        try {
            validatedAddress = new AddressAndLabel(Constants.NETWORK_PARAMETERS, receivingAddress, receivingLabel);
            receivingAddressView.setText(null);
        } catch (final Exception x) {
            receivingAddressView.setText(receivingAddress);
            validatedAddress = null;
            log.info("problem parsing address: '" + receivingAddress + "'", x);
        }

        if (amount != null)
            amountCalculatorLink.setBtcAmount(amount);

        // focus
        if (receivingAddress != null && amount == null)
            amountCalculatorLink.requestFocus();
        else if (receivingAddress != null && amount != null)
            viewGo.requestFocus();

        this.bluetoothMac = bluetoothMac;

        bluetoothAck = null;

        updateView();

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                validateReceivingAddress(true);
                validateAmounts(true);
            }
        }, 500);
    }
}