com.bonsai.wallet32.SendBitcoinActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.bonsai.wallet32.SendBitcoinActivity.java

Source

// Copyright (C) 2013-2014  Bonsai Software, Inc.
// 
// 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 com.bonsai.wallet32;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.DialogFragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;

import com.bonsai.wallet32.WalletService.AmountAndFee;
import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.AddressFormatException;
import com.google.bitcoin.core.InsufficientMoneyException;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.WrongNetworkException;
import com.google.bitcoin.uri.BitcoinURI;
import com.google.bitcoin.uri.BitcoinURIParseException;

import eu.livotov.zxscan.ZXScanHelper;
import hashengineering.groestlcoin.wallet32.R;

public class SendBitcoinActivity extends BaseWalletActivity implements BitcoinSender {

    private static Logger mLogger = LoggerFactory.getLogger(SendBitcoinActivity.class);

    protected EditText mToAddressEditText;

    protected EditText mBTCAmountEditText;
    protected EditText mFiatAmountEditText;

    protected EditText mBTCFeeEditText;
    protected EditText mFiatFeeEditText;

    protected boolean mUserSetAmountFiat;
    protected boolean mUserSetFeeFiat;

    protected String mLastUnitStr = "";

    @SuppressLint({ "HandlerLeak", "DefaultLocale" })
    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_send_bitcoin);

        // Was a URI specified?
        Intent intent = getIntent();
        Bundle bundle = intent.getExtras();
        String intentURI = null;
        if (bundle != null && bundle.containsKey("uri"))
            intentURI = intent.getExtras().getString("uri");

        // Start off presuming the user set the BTC amount.
        mUserSetAmountFiat = false;
        mUserSetFeeFiat = false;

        mToAddressEditText = (EditText) findViewById(R.id.to_address);
        mToAddressEditText.addTextChangedListener(mToAddressWatcher);

        mBTCAmountEditText = (EditText) findViewById(R.id.amount_btc);
        mBTCAmountEditText.addTextChangedListener(mBTCAmountWatcher);

        mFiatAmountEditText = (EditText) findViewById(R.id.amount_fiat);
        mFiatAmountEditText.addTextChangedListener(mFiatAmountWatcher);

        mBTCFeeEditText = (EditText) findViewById(R.id.fee_btc);
        mBTCFeeEditText.addTextChangedListener(mBTCFeeWatcher);

        mFiatFeeEditText = (EditText) findViewById(R.id.fee_fiat);
        mFiatFeeEditText.addTextChangedListener(mFiatFeeWatcher);

        // Set the default fee value.
        long defaultFee = WalletService.getDefaultFee();
        String defaultFeeString = mBTCFmt.format(defaultFee);
        mBTCFeeEditText.setText(defaultFeeString);
        feeSet();

        // Is there an intent uri (from another application)?
        if (intentURI != null)
            updateToAddress(intentURI);

        mLogger.info("SendBitcoinActivity created");
    }

    @Override
    protected void onResume() {
        super.onResume();
        // Set these each time we resume in case we've visited the
        // Settings and they've changed.
        {
            TextView tv = (TextView) findViewById(R.id.amount_btc_label);
            tv.setText(mBTCFmt.unitStr());
        }
        {
            TextView tv = (TextView) findViewById(R.id.fee_btc_label);
            tv.setText(mBTCFmt.unitStr());
        }
        mLogger.info("SendBitcoinActivity resumed");
    }

    @Override
    protected void onWalletStateChanged() {
        updateAccounts();
    }

    @Override
    protected void onRateChanged() {
        updateAmountFields();
        updateFeeFields();
        updateAccounts();
    }

    protected void feeSet() {
        mLastUnitStr = mBTCFmt.unitStr();
    }

    public boolean spendUnconfirmed() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        return prefs.getBoolean("pref_spendUnconfirmed", false);
    }

    private final TextWatcher mToAddressWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence ss, int start, int count, int after) {
        }

        @Override
        public void onTextChanged(CharSequence ss, int start, int before, int count) {
        }

        @Override
        public void afterTextChanged(Editable ss) {
            String uri = mToAddressEditText.getText().toString();
            updateToAddress(uri);
        }

    };

    // NOTE - This code implements a pair of "cross updating" fields.
    // If the user changes the BTC amount the fiat field is constantly
    // updated at the current mFiatPerBTC rate.  If the user changes
    // the fiat field the BTC field is constantly updated at the
    // current rate.

    private final TextWatcher mBTCAmountWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence ss, int start, int count, int after) {
            // Note that the user changed the BTC last.
            mUserSetAmountFiat = false;
        }

        @Override
        public void onTextChanged(CharSequence ss, int start, int before, int count) {
        }

        @Override
        public void afterTextChanged(Editable ss) {
            updateAmountFields();
        }

    };

    private final TextWatcher mFiatAmountWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence ss, int start, int count, int after) {
            mUserSetAmountFiat = true;
        }

        @Override
        public void onTextChanged(CharSequence ss, int start, int before, int count) {
        }

        @Override
        public void afterTextChanged(Editable ss) {
            updateAmountFields();
        }
    };

    @SuppressLint("DefaultLocale")
    protected void updateAmountFields() {
        // Which field did the user last edit?
        if (mUserSetAmountFiat) {
            // The user set the Fiat amount.
            String ss = mFiatAmountEditText.getText().toString();

            // Avoid recursion by removing the other fields listener.
            mBTCAmountEditText.removeTextChangedListener(mBTCAmountWatcher);

            String bbs;
            try {
                double ff = parseNumberWorkaround(ss.toString());
                long bb;
                if (mFiatPerBTC == 0.0) {
                    bbs = "";
                } else {
                    bb = mBTCFmt.btcAtRate(ff, mFiatPerBTC);
                    bbs = mBTCFmt.format(bb);
                }
            } catch (final NumberFormatException ex) {
                bbs = "";
            }
            mBTCAmountEditText.setText(bbs, TextView.BufferType.EDITABLE);

            // Restore the other fields listener.
            mBTCAmountEditText.addTextChangedListener(mBTCAmountWatcher);
        } else {
            // The user set the BTC amount.
            String ss = mBTCAmountEditText.getText().toString();

            // Avoid recursion by removing the other fields listener.
            mFiatAmountEditText.removeTextChangedListener(mFiatAmountWatcher);

            String ffs;
            try {
                long bb = mBTCFmt.parse(ss.toString());
                double ff = mBTCFmt.fiatAtRate(bb, mFiatPerBTC);
                ffs = String.format("%.2f", ff);
            } catch (final NumberFormatException ex) {
                ffs = "";
            }
            mFiatAmountEditText.setText(ffs, TextView.BufferType.EDITABLE);

            // Restore the other fields listener.
            mFiatAmountEditText.addTextChangedListener(mFiatAmountWatcher);
        }
    }

    // NOTE - This code implements a pair of "cross updating" fields.
    // If the user changes the BTC fee the fiat field is constantly
    // updated at the current mFiatPerBTC rate.  If the user changes
    // the fiat field the BTC field is constantly updated at the
    // current rate.

    private final TextWatcher mBTCFeeWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence ss, int start, int count, int after) {
            // Note that the user changed the BTC last.
            mUserSetFeeFiat = false;
            feeSet();
        }

        @Override
        public void onTextChanged(CharSequence ss, int start, int before, int count) {
        }

        @Override
        public void afterTextChanged(Editable ss) {
            updateFeeFields();
        }

    };

    private final TextWatcher mFiatFeeWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence ss, int start, int count, int after) {
            mUserSetFeeFiat = true;
            feeSet();
        }

        @Override
        public void onTextChanged(CharSequence ss, int start, int before, int count) {
        }

        @Override
        public void afterTextChanged(Editable ss) {
            updateFeeFields();
        }
    };

    @SuppressLint("DefaultLocale")
    protected void updateFeeFields() {

        // If the fee units changed, clear both fields.
        if (!mBTCFmt.unitStr().equals(mLastUnitStr)) {

            // Avoid recursion ..
            mBTCFeeEditText.removeTextChangedListener(mBTCFeeWatcher);
            mFiatFeeEditText.removeTextChangedListener(mFiatFeeWatcher);

            mFiatFeeEditText.setText("", TextView.BufferType.EDITABLE);
            mBTCFeeEditText.setText("", TextView.BufferType.EDITABLE);

            mFiatFeeEditText.addTextChangedListener(mFiatFeeWatcher);
            mBTCFeeEditText.addTextChangedListener(mBTCFeeWatcher);

            return;
        }

        // Which field did the user last edit?
        if (mUserSetFeeFiat) {
            // The user set the Fiat fee.
            String ss = mFiatFeeEditText.getText().toString();

            // Avoid recursion by removing the other fields listener.
            mBTCFeeEditText.removeTextChangedListener(mBTCFeeWatcher);

            String bbs;
            try {
                double ff = parseNumberWorkaround(ss.toString());
                long bb;
                if (mFiatPerBTC == 0.0) {
                    bbs = "";
                } else {
                    bb = mBTCFmt.btcAtRate(ff, mFiatPerBTC);
                    bbs = mBTCFmt.format(bb);
                }
            } catch (final NumberFormatException ex) {
                bbs = "";
            }
            mBTCFeeEditText.setText(bbs, TextView.BufferType.EDITABLE);
            feeSet();

            // Restore the other fields listener.
            mBTCFeeEditText.addTextChangedListener(mBTCFeeWatcher);
        } else {
            // The user set the BTC fee.
            String ss = mBTCFeeEditText.getText().toString();

            // Avoid recursion by removing the other fields listener.
            mFiatFeeEditText.removeTextChangedListener(mFiatFeeWatcher);

            String ffs;
            try {
                long bb = mBTCFmt.parse(ss.toString());
                double ff = mBTCFmt.fiatAtRate(bb, mFiatPerBTC);
                ffs = String.format("%.3f", ff);
            } catch (final NumberFormatException ex) {
                ffs = "";
            }
            mFiatFeeEditText.setText(ffs, TextView.BufferType.EDITABLE);

            // Restore the other fields listener.
            mFiatFeeEditText.addTextChangedListener(mFiatFeeWatcher);
        }
    }

    private List<Integer> mAccountIds;
    private int mCheckedFromId = -1;

    private OnCheckedChangeListener mSendFromListener = new OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton cb, boolean isChecked) {
            if (cb.isChecked()) {
                TableLayout table = (TableLayout) findViewById(R.id.from_choices);

                mCheckedFromId = cb.getId();

                for (Integer acctid : mAccountIds) {
                    int rbid = acctid.intValue();
                    if (rbid != mCheckedFromId) {
                        RadioButton rb = (RadioButton) table.findViewById(rbid);
                        rb.setChecked(false);
                    }
                }
            }
        }
    };

    private void addAccountHeader(TableLayout table) {
        TableRow row = (TableRow) LayoutInflater.from(this).inflate(R.layout.send_from_header, table, false);

        TextView tv = (TextView) row.findViewById(R.id.header_btc);
        tv.setText(mBTCFmt.unitStr());

        table.addView(row);
    }

    private void addAccountRow(TableLayout table, int acctId, String acctName, long btc, double fiat) {
        TableRow row = (TableRow) LayoutInflater.from(this).inflate(R.layout.send_from_row, table, false);

        RadioButton tv0 = (RadioButton) row.findViewById(R.id.from_account);
        tv0.setId(acctId); // Change id to the acctId.
        tv0.setText(acctName);
        tv0.setOnCheckedChangeListener(mSendFromListener);
        if (acctId == mCheckedFromId)
            tv0.setChecked(true);

        TextView tv1 = (TextView) row.findViewById(R.id.row_btc);
        tv1.setText(String.format("%s", mBTCFmt.formatCol(btc, 0, true, true)));

        TextView tv2 = (TextView) row.findViewById(R.id.row_fiat);
        tv2.setText(String.format("%.02f", fiat));

        table.addView(row);
    }

    private void updateAccounts() {
        if (mWalletService == null)
            return;

        TableLayout table = (TableLayout) findViewById(R.id.from_choices);

        // Clear any existing table content.
        table.removeAllViews();

        addAccountHeader(table);

        mAccountIds = new ArrayList<Integer>();

        // double sumbtc = 0.0;
        List<Balance> balances = mWalletService.getBalances();
        if (balances != null) {
            for (Balance bal : balances) {

                // Our spendable balance depends on whether or not we
                // can spend unconfirmed balances.
                //
                long spendBalance = spendUnconfirmed() ? bal.balance : bal.available;

                // sumbtc += bal.balance;
                addAccountRow(table, bal.accountId, bal.accountName, spendBalance,
                        mBTCFmt.fiatAtRate(spendBalance, mFiatPerBTC));
                mAccountIds.add(bal.accountId);
            }
        }
    }

    private void updateToAddress(String toval) {

        // Avoid recursion by removing the field listener while
        // we possibly update the field value.
        mToAddressEditText.removeTextChangedListener(mToAddressWatcher);

        NetworkParameters params = mWalletService == null ? null : mWalletService.getParams();

        // Is this a bitcoin URI?
        try {
            BitcoinURI uri = new BitcoinURI(params, toval);
            Address addr = uri.getAddress();
            BigInteger amt = uri.getAmount();

            mToAddressEditText.setText(addr.toString(), TextView.BufferType.EDITABLE);

            if (amt != null) {
                long amtval = amt.longValue();
                String amtstr = mBTCFmt.format(amtval);
                mBTCAmountEditText.setText(amtstr, TextView.BufferType.EDITABLE);
            }
        } catch (BitcoinURIParseException ex) {

            // Is it just a plain address?
            try {
                Address addr = new Address(params, toval);

                mToAddressEditText.setText(addr.toString(), TextView.BufferType.EDITABLE);

            } catch (WrongNetworkException ex2) {
                String msg = mRes.getString(R.string.send_error_wrongnw);
                mLogger.warn(msg);
                Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
            } catch (AddressFormatException ex2) {
                String msg = mRes.getString(R.string.send_error_badqr);
                mLogger.warn(msg);
                Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
            }
        }

        // Restore the field changed listener.
        mToAddressEditText.addTextChangedListener(mToAddressWatcher);
    }

    @SuppressLint("DefaultLocale")
    protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
        if (resultCode == RESULT_OK && requestCode == 12345) {
            String scannedCode = ZXScanHelper.getScannedCode(data);
            mLogger.info("saw scannedCode " + scannedCode);
            updateToAddress(scannedCode);
        }
    }

    public void scanQR(View view) {
        // CaptureActivity
        // ZXScanHelper.setCustomScanSound(R.raw.quiet_beep);
        ZXScanHelper.setPlaySoundOnRead(false);
        ZXScanHelper.setCustomScanLayout(R.layout.scanner_layout);
        ZXScanHelper.scan(this, 12345);
    }

    public void computeFee(View view) {
        setFeeToRecommended();
    }

    public void setFeeToRecommended() {
        // Which account was selected?
        if (mCheckedFromId == -1) {
            showErrorDialog(mRes.getString(R.string.send_error_noaccount));
            return;
        }

        // Fetch the amount to send.
        long amount = 0;
        String amountString = mBTCAmountEditText.getText().toString();
        if (amountString.length() == 0) {
            showErrorDialog(mRes.getString(R.string.send_error_noamount));
            return;
        }
        try {
            amount = mBTCFmt.parse(amountString);
        } catch (NumberFormatException ex) {
            showErrorDialog(mRes.getString(R.string.send_error_badamount));
            return;
        }

        new SetFeeToRecommendedTask().execute(amount);
    }

    private class SetFeeToRecommendedTask extends AsyncTask<Long, Void, Long> {
        private ProgressDialog progressDialog;

        @Override
        protected void onPreExecute() {
            progressDialog = ProgressDialog.show(SendBitcoinActivity.this, "",
                    mRes.getString(R.string.send_computing_fee));
        }

        protected Long doInBackground(Long... params) {
            final Long amount = params[0];

            Long fee = null;
            try {
                fee = mWalletService.computeRecommendedFee(mCheckedFromId, amount, spendUnconfirmed());
            } catch (IllegalArgumentException ex) {
                // just return null fee
            } catch (InsufficientMoneyException ex) {
                // just return null fee
            }
            return fee;
        }

        @Override
        protected void onPostExecute(Long fee) {
            progressDialog.dismiss();

            if (fee == null) {
                // Pick one of these.
                // showErrorDialog(mRes.getString(R.string.send_error_dust));
                showErrorDialog(mRes.getString(R.string.send_error_insufficient));
                return;
            } else {
                mLogger.info(String.format("recommended fee is %s", mBTCFmt.format(fee)));
                String msg = mRes.getString(R.string.send_set_fee, mBTCFmt.format(fee));
                Toast.makeText(SendBitcoinActivity.this, msg, Toast.LENGTH_SHORT).show();

                String feeString = mBTCFmt.format(fee);
                mBTCFeeEditText.setText(feeString);
                feeSet();
            }
        }
    }

    public void useAll(View view) {
        // Which account was selected?
        if (mCheckedFromId == -1) {
            showErrorDialog(mRes.getString(R.string.send_error_noaccount));
            return;
        }

        new UseAllTask().execute();
    }

    private class UseAllTask extends AsyncTask<Void, Void, Void> {
        private ProgressDialog progressDialog;
        private AmountAndFee amtnfee = null;

        @Override
        protected void onPreExecute() {
            progressDialog = ProgressDialog.show(SendBitcoinActivity.this, "",
                    mRes.getString(R.string.send_computing_fee));
        }

        protected Void doInBackground(Void... arg0) {
            try {
                amtnfee = mWalletService.useAll(mCheckedFromId, spendUnconfirmed());
            } catch (InsufficientMoneyException ex) {
                // just leave amtnfee null ...
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            progressDialog.dismiss();

            if (amtnfee == null) {
                showErrorDialog(mRes.getString(R.string.send_error_insufficient));
            } else {
                mLogger.info(String.format("setting amount to %s, fee to %s", mBTCFmt.format(amtnfee.mAmount),
                        mBTCFmt.format(amtnfee.mFee)));
                String msg = mRes.getString(R.string.send_set_amt_fee, mBTCFmt.format(amtnfee.mAmount),
                        mBTCFmt.format(amtnfee.mFee));
                Toast.makeText(SendBitcoinActivity.this, msg, Toast.LENGTH_SHORT).show();

                String amtString = mBTCFmt.format(amtnfee.mAmount);
                mBTCAmountEditText.setText(amtString);

                String feeString = mBTCFmt.format(amtnfee.mFee);
                mBTCFeeEditText.setText(feeString);
                feeSet();
            }
        }
    }

    public void sendBitcoin(View view) {
        if (mWalletService == null) {
            showErrorDialog(mRes.getString(R.string.send_error_nowallet));
            return;
        }

        // Which account was selected?
        if (mCheckedFromId == -1) {
            showErrorDialog(mRes.getString(R.string.send_error_noaccount));
            return;
        }
        String acctName = mWalletService.getAccount(mCheckedFromId).getName();

        // Fetch the address.
        String addrString = mToAddressEditText.getText().toString();
        if (addrString.length() == 0) {
            showErrorDialog(mRes.getString(R.string.send_error_noaddr));
            return;
        }

        // Fetch the amount to send.
        long amount = 0;
        String amountString = mBTCAmountEditText.getText().toString();
        if (amountString.length() == 0) {
            showErrorDialog(mRes.getString(R.string.send_error_noamount));
            return;
        }
        try {
            amount = mBTCFmt.parse(amountString);
        } catch (NumberFormatException ex) {
            showErrorDialog(mRes.getString(R.string.send_error_badamount));
            return;
        }

        // Fetch the fee amount.
        long fee = 0;
        String feeString = mBTCFeeEditText.getText().toString();
        if (feeString.length() == 0) {
            showErrorDialog(mRes.getString(R.string.send_error_nofee));
            return;
        }
        try {
            fee = mBTCFmt.parse(feeString);
        } catch (NumberFormatException ex) {
            showErrorDialog(mRes.getString(R.string.send_error_badfee));
            return;
        }

        // Check to make sure we have enough money for this send.
        long avail = spendUnconfirmed() ? mWalletService.balanceForAccount(mCheckedFromId)
                : mWalletService.availableForAccount(mCheckedFromId);
        if (amount + fee > avail) {
            showErrorDialog(mRes.getString(R.string.send_error_insufficient));
            return;
        }

        // Check the recommended fee, generate warning dialog or
        // confirm send dialog ...
        try {
            long recFee = mWalletService.computeRecommendedFee(mCheckedFromId, amount, spendUnconfirmed());

            if (fee > recFee) {
                // Warn that fee is larger than recommended.
                mLogger.info(String.format("fee %s larger than recommended %s", mBTCFmt.format(fee),
                        mBTCFmt.format(recFee)));
                showFeeAdjustDialog(
                        mRes.getString(R.string.send_feeadjust_large, mBTCFmt.format(fee), mBTCFmt.format(recFee)),
                        mCheckedFromId, acctName, addrString, amount, fee, mFiatPerBTC);
            } else if (fee < recFee) {
                // Warn that fee is less than recommended.
                mLogger.info(String.format("fee %s less than recommended %s", mBTCFmt.format(fee),
                        mBTCFmt.format(recFee)));
                showFeeAdjustDialog(
                        mRes.getString(R.string.send_feeadjust_small, mBTCFmt.format(fee), mBTCFmt.format(recFee)),
                        mCheckedFromId, acctName, addrString, amount, fee, mFiatPerBTC);
            } else {
                // Looks good, confirm the send.
                mLogger.info(
                        String.format("fee %s equals recommended %s", mBTCFmt.format(fee), mBTCFmt.format(recFee)));
                showSendConfirmDialog(mCheckedFromId, acctName, addrString, amount, fee, mFiatPerBTC);
            }

        } catch (IllegalArgumentException ex) {
            showErrorDialog(mRes.getString(R.string.send_error_dust));
            return;
        } catch (InsufficientMoneyException ex) {
            // An InsufficientMoneyException here means the send amount
            // is too large for the recommended fee.
            mLogger.info("insufficient funds for recommended fee");
            // FIXME - Add fee-too-small dialog.
        }
    }

    public void onShowSendConfirmDialog(int acctId, String acctName, String addrString, long amount, long fee) {
        showSendConfirmDialog(acctId, acctName, addrString, amount, fee, mFiatPerBTC);
    }

    public void onAdjustFee() {
        setFeeToRecommended();
    }

    public void onSendBitcoin(int acctId, String addrString, long amount, long fee) {

        try {
            mLogger.info(String.format("send from %d, to %s, amount %s, fee %s starting", acctId, addrString,
                    mBTCFmt.format(amount), mBTCFmt.format(fee)));

            mWalletService.sendCoinsFromAccount(acctId, addrString, amount, fee, spendUnconfirmed());

            mLogger.info("send finished");

            // Head to the transaction view for this account ...
            Intent intent = new Intent(this, ViewTransactionsActivity.class);
            Bundle bundle = new Bundle();
            bundle.putInt("accountId", mCheckedFromId);
            intent.putExtras(bundle);
            startActivity(intent);

            // We're done here ...
            finish();

        } catch (RuntimeException ex) {
            mLogger.error(ex.toString());
            showErrorDialog(ex.getMessage());
            return;
        }
    }

    public static class SendConfirmDialogFragment extends DialogFragment {
        private int mAcctId;
        private String mAcctStr;
        private String mAddr;
        private long mAmount;
        private long mFee;
        private double mRate;

        private BitcoinSender mSender;

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setRetainInstance(true);
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            super.onCreateDialog(savedInstanceState);

            mAcctId = getArguments().getInt("acctId");
            mAcctStr = getArguments().getString("acctStr");
            mAddr = getArguments().getString("addr");
            mAmount = getArguments().getLong("amount");
            mFee = getArguments().getLong("fee");
            mRate = getArguments().getDouble("rate");

            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

            builder.setTitle(R.string.send_confirm_title);

            LayoutInflater inflater = getActivity().getLayoutInflater();
            View dv = inflater.inflate(R.layout.dialog_confirm_send, null);

            {
                TextView tv = (TextView) dv.findViewById(R.id.to_addr);
                tv.setText(mAddr);
            }
            {
                TextView tv = (TextView) dv.findViewById(R.id.from_account);
                tv.setText(mAcctStr);
            }
            {
                TextView tv = (TextView) dv.findViewById(R.id.header_btc);
                tv.setText(mBTCFmt.unitStr());
            }
            {
                TextView tv = (TextView) dv.findViewById(R.id.amount_btc);
                tv.setText(mBTCFmt.formatCol(mAmount, 0, true, true));
            }
            {
                TextView tv = (TextView) dv.findViewById(R.id.amount_fiat);
                tv.setText(String.format("%.2f", mBTCFmt.fiatAtRate(mAmount, mRate)));
            }
            {
                TextView tv = (TextView) dv.findViewById(R.id.fee_btc);
                tv.setText(mBTCFmt.formatCol(mFee, 0, true, true));
            }
            {
                TextView tv = (TextView) dv.findViewById(R.id.fee_fiat);
                tv.setText(String.format("%.2f", mBTCFmt.fiatAtRate(mFee, mRate)));
            }
            {
                TextView tv = (TextView) dv.findViewById(R.id.total_btc);
                tv.setText(mBTCFmt.formatCol(mAmount + mFee, 0, true, true));
            }
            {
                TextView tv = (TextView) dv.findViewById(R.id.total_fiat);
                tv.setText(String.format("%.2f", mBTCFmt.fiatAtRate((mAmount + mFee), mRate)));
            }

            builder.setView(dv);

            builder.setPositiveButton(R.string.send_confirm_send, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface di, int id) {
                    mLogger.info("send confirmed");
                    mSender.onSendBitcoin(mAcctId, mAddr, mAmount, mFee);
                }
            });

            builder.setNegativeButton(R.string.send_confirm_cancel, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface di, int id) {
                    mLogger.info("send canceled");
                }
            });

            return builder.create();
        }

        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            try {
                mSender = (BitcoinSender) activity;
            } catch (ClassCastException e) {
                throw new ClassCastException(activity.toString() + " must implement BitcoinSender");
            }
        }
    }

    private DialogFragment showSendConfirmDialog(int acctId, String acctStr, String addrStr, long amount, long fee,
            double rate) {
        DialogFragment df = new SendConfirmDialogFragment();
        Bundle args = new Bundle();
        args.putInt("acctId", acctId);
        args.putString("acctStr", acctStr);
        args.putString("addr", addrStr);
        args.putLong("amount", amount);
        args.putLong("fee", fee);
        args.putDouble("rate", rate);
        df.setArguments(args);
        df.show(getSupportFragmentManager(), "sendconfirm");
        return df;
    }

    public static class FeeAdjustDialogFragment extends DialogFragment {
        private String mMsg;
        private int mAcctId;
        private String mAcctStr;
        private String mAddr;
        private long mAmount;
        private long mFee;
        private double mRate;

        private BitcoinSender mSender;

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setRetainInstance(true);
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            super.onCreateDialog(savedInstanceState);

            mMsg = getArguments().getString("msg");
            mAcctId = getArguments().getInt("acctId");
            mAcctStr = getArguments().getString("acctStr");
            mAddr = getArguments().getString("addr");
            mAmount = getArguments().getLong("amount");
            mFee = getArguments().getLong("fee");
            mRate = getArguments().getDouble("rate");

            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

            builder.setTitle(R.string.send_feeadjust_title);
            builder.setMessage(mMsg);
            builder.setPositiveButton(R.string.send_feeadjust_send, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface di, int id) {
                    mLogger.info("send anyway");
                    mSender.onShowSendConfirmDialog(mAcctId, mAcctStr, mAddr, mAmount, mFee);
                }
            });

            builder.setNegativeButton(R.string.send_feeadjust_adjust, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface di, int id) {
                    mLogger.info("adjust fee");
                    mSender.onAdjustFee();
                }
            });

            return builder.create();
        }

        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            try {
                mSender = (BitcoinSender) activity;
            } catch (ClassCastException e) {
                throw new ClassCastException(activity.toString() + " must implement BitcoinSender");
            }
        }
    }

    private DialogFragment showFeeAdjustDialog(String msg, int acctId, String acctStr, String addrStr, long amount,
            long fee, double rate) {
        DialogFragment df = new FeeAdjustDialogFragment();
        Bundle args = new Bundle();
        args.putString("msg", msg);
        args.putInt("acctId", acctId);
        args.putString("acctStr", acctStr);
        args.putString("addr", addrStr);
        args.putLong("amount", amount);
        args.putLong("fee", fee);
        args.putDouble("rate", rate);
        df.setArguments(args);
        df.show(getSupportFragmentManager(), "sendconfirm");
        return df;
    }

    public double parseNumberWorkaround(String valstr) throws NumberFormatException {
        // Some countries use comma as the decimal separator.
        // Android's numberDecimal EditText fields don't handle this
        // correctly (https://code.google.com/p/android/issues/detail?id=2626).
        // As a workaround we substitute ',' -> '.' manually ...
        double dval = Double.parseDouble(valstr.toString().replace(',', '.'));
        return dval;
    }
}

// Local Variables:
// mode: java
// c-basic-offset: 4
// tab-width: 4
// End: