com.bonsai.wallet32.SweepKeyActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.bonsai.wallet32.SweepKeyActivity.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.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.util.encoders.Hex;

import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.AddressFormatException;
import com.google.bitcoin.core.CoinDefinition;
import com.google.bitcoin.core.DumpedPrivateKey;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.core.WrongNetworkException;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.script.ScriptBuilder;
import com.google.bitcoin.uri.BitcoinURI;
import com.google.bitcoin.uri.BitcoinURIParseException;

import eu.livotov.zxscan.ZXScanHelper;

import android.annotation.SuppressLint;
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.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 hashengineering.groestlcoin.wallet32.R;

public class SweepKeyActivity extends BaseWalletActivity {

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

    private EditText mPrivateKeyEditText;

    private TextView mBalanceBTCText;
    private TextView mBalanceFiatText;

    private EditText mBTCFeeEditText;
    private EditText mFiatFeeEditText;

    private boolean mUserSetFeeFiat;

    private JSONArray mUnspentOutputs;

    private ECKey mKey = null;
    private Address mAddr = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {

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

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

        mPrivateKeyEditText = (EditText) findViewById(R.id.private_key);
        mPrivateKeyEditText.addTextChangedListener(mPrivateKeyWatcher);

        mBalanceBTCText = (TextView) findViewById(R.id.balance_btc);
        mBalanceFiatText = (TextView) findViewById(R.id.balance_fiat);

        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);

        mUnspentOutputs = null;

        mLogger.info("SweepKeyActivity 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.balance_btc_label);
            tv.setText(mBTCFmt.unitStr());
        }
        {
            TextView tv = (TextView) findViewById(R.id.fee_btc_label);
            tv.setText(mBTCFmt.unitStr());
        }
        mLogger.info("SweepKeyActivity resumed");
    }

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

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

    // 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;
        }

        @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;
        }

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

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

    @SuppressLint("DefaultLocale")
    protected void updateFeeFields() {
        // 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);

            // 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("%.2f", 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 mAccountId = -1;

    private OnCheckedChangeListener mReceiveToListener = new OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton cb, boolean isChecked) {
            if (cb.isChecked()) {
                TableLayout table = (TableLayout) findViewById(R.id.to_choices);
                mAccountId = cb.getId();
                for (Integer acctid : mAccountIds) {
                    int rbid = acctid.intValue();
                    if (rbid != mAccountId) {
                        RadioButton rb = (RadioButton) table.findViewById(rbid);
                        rb.setChecked(false);
                    }
                }
            }
        }
    };

    private void addAccountHeader(TableLayout table) {
        TableRow row = (TableRow) LayoutInflater.from(this).inflate(R.layout.receive_to_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.receive_to_row, table, false);

        RadioButton tv0 = (RadioButton) row.findViewById(R.id.to_account);
        tv0.setId(acctId); // Change id to the acctId.
        tv0.setText(acctName);
        tv0.setOnCheckedChangeListener(mReceiveToListener);
        if (acctId == mAccountId)
            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.to_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) {
                // sumbtc += bal.balance;
                addAccountRow(table, bal.accountId, bal.accountName, bal.balance,
                        mBTCFmt.fiatAtRate(bal.balance, mFiatPerBTC));
                mAccountIds.add(bal.accountId);
            }
        }
    }

    private final TextWatcher mPrivateKeyWatcher = 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 val = mPrivateKeyEditText.getText().toString();
            updatePrivateKey(val);
        }

    };

    private void updateBalance() {
        String ss = mBalanceBTCText.getText().toString();
        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 = "";
        }
        mBalanceFiatText.setText(ffs, TextView.BufferType.NORMAL);
    }

    public static class MyDialogFragment extends DialogFragment {
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            super.onCreateDialog(savedInstanceState);
            String msg = getArguments().getString("msg");
            String title = getArguments().getString("title");
            boolean hasOK = getArguments().getBoolean("hasOK");
            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
            builder.setTitle(title);
            builder.setMessage(msg);
            if (hasOK) {
                builder.setPositiveButton(R.string.base_error_ok, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface di, int id) {
                    }
                });
            }
            return builder.create();
        }
    }

    protected DialogFragment showModalDialog(String title, String msg) {
        DialogFragment df = new MyDialogFragment();
        Bundle args = new Bundle();
        args.putString("title", title);
        args.putString("msg", msg);
        args.putBoolean("hasOK", false);
        df.setArguments(args);
        df.show(getSupportFragmentManager(), "wait");
        return df;
    }

    private class FetchUnspentTask extends AsyncTask<String, Void, String> {

        String baseUrl = CoinDefinition.UNSPENT_API_URL;//"https://blockchain.info/unspent?active=";

        DialogFragment mDF = null;

        String address = null;

        @Override
        protected void onPreExecute() {
            // Show the wait dialog ...
            mDF = showModalDialog(mRes.getString(R.string.sweep_wait_title),
                    mRes.getString(R.string.sweep_wait_fetching));
        }

        @Override
        protected String doInBackground(String... params) {
            final String addr = params[0];
            address = addr;

            mLogger.info("fetching unspent outputs for " + addr);

            String url = baseUrl;// + addr;
            url += "&key=d47da926b82e"; //Cryptoid API key
            url += "&active=" + addr;

            mLogger.info("sweep url: " + url);

            try {
                DefaultHttpClient httpClient = new DefaultHttpClient();
                HttpGet httpGet = new HttpGet(url);
                HttpResponse httpResponse = httpClient.execute(httpGet);
                HttpEntity httpEntity = httpResponse.getEntity();
                InputStream is = httpEntity.getContent();
                BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                StringBuilder sb = new StringBuilder();
                String line = null;
                while ((line = reader.readLine()) != null) {
                    sb.append(line + "\n");
                }
                is.close();
                mLogger.info("sweep result: " + sb.toString());
                return sb.toString();

            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(String jsonstr) {
            mDF.dismissAllowingStateLoss();

            if (jsonstr == null) {
                showErrorDialog(mRes.getString(R.string.sweep_error_blockchain));
                mBalanceBTCText.setText("0.0", TextView.BufferType.NORMAL);
                updateBalance();
                mUnspentOutputs = null;
                return;
            }

            else if (jsonstr.contains("{\"unspent_outputs\":[]}")) {
                showErrorDialog(mRes.getString(R.string.sweep_no_unspent));
                mBalanceBTCText.setText("0.0", TextView.BufferType.NORMAL);
                updateBalance();
                mUnspentOutputs = null;
                return;
            }

            else
                try {
                    JSONObject jsonobj = new JSONObject(jsonstr);
                    JSONArray outputs = jsonobj.getJSONArray("unspent_outputs");
                    long balance = 0;
                    for (int ii = 0; ii < outputs.length(); ++ii) {
                        JSONObject output = outputs.getJSONObject(ii);
                        balance += output.getLong("value");

                        if (!output.has("script")) {
                            Address addressObject = new Address(MainNetParams.get(), this.address);
                            output.put("script", ScriptBuilder.createOutputScript(addressObject).getProgram());
                            mLogger.info("address: " + addressObject.toString() + " script value: "
                                    + Utils.bytesToHexString(
                                            ScriptBuilder.createOutputScript(addressObject).getProgram()));
                        }
                    }

                    mLogger.info(String.format("key balance %d", balance));

                    String ffs = String.format("%s", mBTCFmt.format(balance));
                    mBalanceBTCText.setText(ffs, TextView.BufferType.NORMAL);
                    updateBalance();
                    mUnspentOutputs = outputs;

                    return;

                } catch (JSONException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }

            showErrorDialog(mRes.getString(R.string.sweep_error_parse));
            return;
        }
    }

    private final Handler mHandleKeyChanged = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // Fetch unspent outputs from service.
            FetchUnspentTask task = new FetchUnspentTask();
            task.execute(mAddr.toString());
        }
    };

    private void updatePrivateKey(String privstr) {

        // Sets mKey to the private key, null otherwise.

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

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

        mKey = null;
        mAddr = null;

        try {
            // If we can decode a private key we're set.
            mKey = new DumpedPrivateKey(params, privstr).getKey();

            mAddr = mKey.toAddress(params);

            mPrivateKeyEditText.setText(privstr, TextView.BufferType.EDITABLE);

        } catch (AddressFormatException e) {

            // Is this a bitcoin URI?
            try {
                BitcoinURI uri = new BitcoinURI(params, privstr);
                mAddr = uri.getAddress();

            } catch (BitcoinURIParseException ex) {

                // Is it just a plain address?
                try {
                    mAddr = new Address(params, privstr);
                } catch (WrongNetworkException ex2) {
                    String msg = mRes.getString(R.string.sweep_error_wrongnw);
                    mLogger.warn(msg);
                    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
                } catch (AddressFormatException ex2) {
                    String msg = mRes.getString(R.string.sweep_error_badqr);
                    mLogger.warn(msg);
                    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
                }
            }

            if (mAddr != null) {
                mPrivateKeyEditText.setText(privstr, TextView.BufferType.EDITABLE);

                String msg = mRes.getString(R.string.sweep_needprivate);
                mLogger.warn(msg);
                Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
            }
        }

        // Clear any existing outputs.
        mUnspentOutputs = null;

        if (mAddr != null) {
            // Send a message to update the unspent outputs.  Can't do it
            // directly here because we are in a bad context ...
            Message msgObj = mHandleKeyChanged.obtainMessage();
            mHandleKeyChanged.sendMessage(msgObj);
        }

        // Restore the field changed listener.
        mPrivateKeyEditText.addTextChangedListener(mPrivateKeyWatcher);
    }

    @SuppressLint("DefaultLocale")
    protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
        if (resultCode == RESULT_OK && requestCode == 12346) {
            String scannedCode = ZXScanHelper.getScannedCode(data);
            mLogger.info("saw scannedCode " + scannedCode);
            updatePrivateKey(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, 12346);
    }

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

        // Fetch the private key.
        if (mKey == null) {
            showErrorDialog(mRes.getString(R.string.sweep_error_nokey));
            return;
        }

        // Make sure we have fetched the unspent outputs.
        if (mUnspentOutputs == null) {
            showErrorDialog(mRes.getString(R.string.sweep_error_nooutputs));
            return;
        }

        // Fetch the fee amount.
        long fee = 0;
        EditText feeEditText = (EditText) findViewById(R.id.fee_btc);
        String feeString = feeEditText.getText().toString();
        if (feeString.length() == 0) {
            showErrorDialog(mRes.getString(R.string.sweep_error_nofee));
            return;
        }
        try {
            fee = mBTCFmt.parse(feeString);
        } catch (NumberFormatException ex) {
            showErrorDialog(mRes.getString(R.string.sweep_error_badfee));
            return;
        }

        // Which account was selected?
        if (mAccountId == -1) {
            showErrorDialog(mRes.getString(R.string.sweep_error_noaccount));
            return;
        }

        // Sweep!
        mWalletService.sweepKey(mKey, fee, mAccountId, mUnspentOutputs);

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

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

    private static double parseNumberWorkaround(String numstr) 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 ...
        return Double.parseDouble(numstr.toString().replace(',', '.'));
    }
}

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