Java tutorial
// 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: