ru.valle.btc.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for ru.valle.btc.MainActivity.java

Source

/*
 * The MIT License (MIT)
 * <p/>
 * Copyright (c) 2013 Valentin Konovalov
 * <p/>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * <p/>
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * <p/>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package ru.valle.btc;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;

import com.d_project.qrcode.ErrorCorrectLevel;
import com.d_project.qrcode.QRCode;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

public final class MainActivity extends Activity {

    private static final int REQUEST_SCAN_PRIVATE_KEY = 0;
    private static final int REQUEST_SCAN_RECIPIENT_ADDRESS = 1;
    private static final long SEND_MAX = -1;
    private static final long AMOUNT_ERR = -2;

    private EditText addressTextEdit;
    private TextView privateKeyTypeView;
    private EditText privateKeyTextEdit;
    private View sendLayout;
    private TextView rawTxDescriptionHeaderView, rawTxDescriptionView;
    private EditText rawTxToSpendEdit;
    private EditText recipientAddressView;
    private EditText amountEdit;
    private TextView spendTxDescriptionView;
    private View spendTxWarningView;
    private TextView spendTxEdit;
    private View generateButton;

    private boolean insertingPrivateKeyProgrammatically, insertingAddressProgrammatically;
    private AsyncTask<Void, Void, KeyPair> addressGenerateTask;
    private AsyncTask<Void, Void, GenerateTransactionResult> generateTransactionTask;
    private AsyncTask<Void, Void, KeyPair> switchingCompressionTypeTask;
    private AsyncTask<Void, Void, KeyPair> decodePrivateKeyTask;
    private AsyncTask<Void, Void, Object> bip38Task;
    private AsyncTask<Void, Void, ArrayList<UnspentOutputInfo>> decodeUnspentOutputsInfoTask;

    private KeyPair currentKeyPair;
    private View scanPrivateKeyButton, scanRecipientAddressButton;
    private View showQRCodeAddressButton, showQRCodePrivateKeyButton;
    private View enterPrivateKeyAck;
    private View rawTxToSpendPasteButton;
    private Runnable clipboardListener;
    private View sendTxInBrowserButton;
    private TextView passwordButton;
    private EditText passwordEdit;
    private boolean lastBip38ActionWasDecryption;
    private ClipboardHelper clipboardHelper;

    //collected information for tx generation:
    private String verifiedRecipientAddressForTx;
    private KeyPair verifiedKeyPairForTx;
    private ArrayList<UnspentOutputInfo> verifiedUnspentOutputsForTx;
    private long verifiedAmountToSendForTx;
    private boolean verifiedUnspentOutputsComesFromJson;
    private int verifiedConfirmationsCount = -1;
    private ViewGroup mainLayout;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mainLayout = (ViewGroup) findViewById(R.id.main);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
        }
        addressTextEdit = (EditText) findViewById(R.id.address_label);
        generateButton = findViewById(R.id.generate_button);
        privateKeyTypeView = (TextView) findViewById(R.id.private_key_type_label);
        privateKeyTypeView.setMovementMethod(LinkMovementMethod.getInstance());
        privateKeyTextEdit = (EditText) findViewById(R.id.private_key_label);
        passwordButton = (TextView) findViewById(R.id.password_button);
        passwordEdit = (EditText) findViewById(R.id.password_edit);

        sendLayout = findViewById(R.id.send_layout);
        rawTxToSpendPasteButton = findViewById(R.id.paste_tx_button);
        rawTxToSpendEdit = (EditText) findViewById(R.id.raw_tx);
        recipientAddressView = (EditText) findViewById(R.id.recipient_address);
        amountEdit = (EditText) findViewById(R.id.amount);
        rawTxDescriptionHeaderView = (TextView) findViewById(R.id.raw_tx_description_header);
        rawTxDescriptionView = (TextView) findViewById(R.id.raw_tx_description);
        spendTxDescriptionView = (TextView) findViewById(R.id.spend_tx_description);
        spendTxWarningView = findViewById(R.id.spend_tx_warning_footer);
        spendTxEdit = (TextView) findViewById(R.id.spend_tx);
        sendTxInBrowserButton = findViewById(R.id.send_tx_button);
        scanPrivateKeyButton = findViewById(R.id.scan_private_key_button);
        showQRCodeAddressButton = findViewById(R.id.qr_address_button);
        showQRCodePrivateKeyButton = findViewById(R.id.qr_private_key_button);
        scanRecipientAddressButton = findViewById(R.id.scan_recipient_address_button);
        enterPrivateKeyAck = findViewById(R.id.enter_private_key_to_spend_desc);

        wireListeners();
        generateNewAddress();
    }

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

        CharSequence textInClipboard = getTextInClipboard();
        boolean hasTextInClipboard = !TextUtils.isEmpty(textInClipboard);
        if (Build.VERSION.SDK_INT >= 11) {
            if (!hasTextInClipboard) {
                clipboardListener = new Runnable() {
                    @Override
                    public void run() {
                        rawTxToSpendPasteButton.setEnabled(!TextUtils.isEmpty(getTextInClipboard()));
                    }
                };
                clipboardHelper.runOnClipboardChange(clipboardListener);
            }
            rawTxToSpendPasteButton.setEnabled(hasTextInClipboard);
        } else {
            rawTxToSpendPasteButton.setVisibility(hasTextInClipboard ? View.VISIBLE : View.GONE);
        }
    }

    @SuppressLint("NewApi")
    @Override
    protected void onPause() {
        super.onPause();
        if (Build.VERSION.SDK_INT >= 11 && clipboardListener != null) {
            clipboardHelper.removeClipboardListener(clipboardListener);
        }
    }

    @SuppressWarnings("deprecation")
    private String getTextInClipboard() {
        CharSequence textInClipboard = "";
        if (Build.VERSION.SDK_INT >= 11) {
            if (clipboardHelper.hasTextInClipboard()) {
                textInClipboard = clipboardHelper.getTextInClipboard();
            }
        } else {
            android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(
                    Context.CLIPBOARD_SERVICE);
            if (clipboard.hasText()) {
                textInClipboard = clipboard.getText();
            }
        }
        return textInClipboard == null ? "" : textInClipboard.toString();
    }

    @SuppressWarnings("deprecation")
    private void copyTextToClipboard(String label, String text) {
        if (Build.VERSION.SDK_INT >= 11) {
            clipboardHelper.copyTextToClipboard(label, text);
        } else {
            android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(
                    Context.CLIPBOARD_SERVICE);
            clipboard.setText(text);
        }
    }

    @SuppressLint("NewApi")
    private void wireListeners() {
        if (Build.VERSION.SDK_INT >= 11) {
            clipboardHelper = new ClipboardHelper(this);
        }
        addressTextEdit.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                if (!insertingAddressProgrammatically) {
                    cancelAllRunningTasks();
                    insertingPrivateKeyProgrammatically = true;
                    privateKeyTextEdit.setText("");
                    insertingPrivateKeyProgrammatically = false;
                    privateKeyTypeView.setVisibility(View.GONE);
                    updatePasswordView(null);
                    showSpendPanelForKeyPair(null);
                }
                showQRCodeAddressButton.setVisibility(
                        !TextUtils.isEmpty(s) && BTCUtils.verifyBitcoinAddress(s.toString()) ? View.VISIBLE
                                : View.GONE);
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
        generateButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                generateNewAddress();
            }
        });
        privateKeyTextEdit.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                if (!insertingPrivateKeyProgrammatically) {
                    cancelAllRunningTasks();
                    insertingAddressProgrammatically = true;
                    setTextWithoutJumping(addressTextEdit, getString(R.string.decoding));
                    insertingAddressProgrammatically = false;
                    final String privateKeyToDecode = s.toString();
                    if (!TextUtils.isEmpty(privateKeyToDecode)) {
                        decodePrivateKeyTask = new AsyncTask<Void, Void, KeyPair>() {
                            @Override
                            protected KeyPair doInBackground(Void... params) {
                                try {
                                    BTCUtils.PrivateKeyInfo privateKeyInfo = BTCUtils
                                            .decodePrivateKey(privateKeyToDecode);
                                    if (privateKeyInfo != null) {
                                        return new KeyPair(privateKeyInfo);
                                    }
                                } catch (Exception ex) {
                                    ex.printStackTrace();
                                }
                                return null;
                            }

                            @Override
                            protected void onPostExecute(KeyPair keyPair) {
                                super.onPostExecute(keyPair);
                                decodePrivateKeyTask = null;
                                onKeyPairModify(false, keyPair);
                            }
                        };
                        decodePrivateKeyTask.execute();
                    } else {
                        onKeyPairModify(true, null);
                    }
                }
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });

        passwordEdit.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

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

            @Override
            public void afterTextChanged(Editable s) {
                updatePasswordView(currentKeyPair);
            }
        });

        passwordEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == R.id.action_encrypt || actionId == R.id.action_decrypt) {
                    encryptOrDecryptPrivateKey();
                    return true;
                }
                return false;
            }
        });
        passwordButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                encryptOrDecryptPrivateKey();
            }
        });
        rawTxToSpendPasteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                rawTxToSpendEdit.setText(getTextInClipboard());
            }
        });
        rawTxToSpendEdit.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                verifiedConfirmationsCount = -1;
                onUnspentOutputsInfoChanged();
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
        recipientAddressView.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                onRecipientAddressChanged();
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
        amountEdit.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                onSendAmountChanged(getString(amountEdit));
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
        scanPrivateKeyButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivityForResult(new Intent(MainActivity.this, ScanActivity.class), REQUEST_SCAN_PRIVATE_KEY);
            }
        });
        showQRCodeAddressButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showQRCodePopupForAddress(getString(addressTextEdit));
            }
        });
        showQRCodePrivateKeyButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String[] dataTypes = getResources().getStringArray(R.array.private_keys_types_for_qr);
                String[] privateKeys = new String[3];
                if (currentKeyPair.privateKey.type == BTCUtils.PrivateKeyInfo.TYPE_MINI) {
                    privateKeys[0] = currentKeyPair.privateKey.privateKeyEncoded;
                } else if (currentKeyPair.privateKey.type == BTCUtils.Bip38PrivateKeyInfo.TYPE_BIP38) {
                    privateKeys[2] = currentKeyPair.privateKey.privateKeyEncoded;
                }
                if (currentKeyPair.privateKey.privateKeyDecoded != null) {
                    privateKeys[1] = BTCUtils.encodeWifKey(currentKeyPair.privateKey.isPublicKeyCompressed,
                            BTCUtils.getPrivateKeyBytes(currentKeyPair.privateKey.privateKeyDecoded));
                }
                showQRCodePopupForPrivateKey(getString(R.string.private_key_for, currentKeyPair.address),
                        currentKeyPair.address, privateKeys, dataTypes);
            }
        });
        scanRecipientAddressButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivityForResult(new Intent(MainActivity.this, ScanActivity.class),
                        REQUEST_SCAN_RECIPIENT_ADDRESS);
            }
        });
        sendTxInBrowserButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                copyTextToClipboard(getString(R.string.tx_description_for_clipboard, amountEdit.getText(),
                        recipientAddressView.getText()), getString(spendTxEdit));
                try {
                    startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://blockchain.info/pushtx")));
                } catch (Exception e) {
                    Toast.makeText(MainActivity.this, R.string.unable_to_open_browser, Toast.LENGTH_LONG).show();
                }
            }
        });

        if (Build.VERSION.SDK_INT < 8 || !EclairHelper.canScan(this)) {
            scanPrivateKeyButton.setVisibility(View.GONE);
            scanRecipientAddressButton.setVisibility(View.GONE);
        }
    }

    private void onRecipientAddressChanged() {
        String addressStr = getString(recipientAddressView);
        TextView recipientAddressError = (TextView) findViewById(R.id.err_recipient_address);
        if (BTCUtils.verifyBitcoinAddress(addressStr)) {
            if (verifiedKeyPairForTx != null && addressStr.equals(verifiedKeyPairForTx.address)) {
                recipientAddressError.setText(R.string.output_address_same_as_input);
            } else {
                recipientAddressError.setText("");
            }
            verifiedRecipientAddressForTx = addressStr;
            tryToGenerateSpendingTransaction();
        } else {
            verifiedRecipientAddressForTx = null;
            recipientAddressError.setText(TextUtils.isEmpty(addressStr) ? "" : getString(R.string.invalid_address));
        }
    }

    private void onUnspentOutputsInfoChanged() {
        final String unspentOutputsInfoStr = getString(rawTxToSpendEdit);
        final KeyPair keyPair = currentKeyPair;
        if (keyPair != null && keyPair.privateKey != null && keyPair.privateKey.privateKeyDecoded != null) {
            verifiedKeyPairForTx = keyPair;
            if (!TextUtils.isEmpty(verifiedRecipientAddressForTx)
                    && verifiedRecipientAddressForTx.equals(verifiedKeyPairForTx.address)) {
                ((TextView) findViewById(R.id.err_recipient_address))
                        .setText(R.string.output_address_same_as_input);
            }
            final TextView rawTxToSpendErr = (TextView) findViewById(R.id.err_raw_tx);
            if (TextUtils.isEmpty(unspentOutputsInfoStr)) {
                rawTxToSpendErr.setText("");
                verifiedUnspentOutputsForTx = null;
            } else {
                cancelAllRunningTasks();
                decodeUnspentOutputsInfoTask = new AsyncTask<Void, Void, ArrayList<UnspentOutputInfo>>() {
                    /**
                     * stores if input is a json.
                     * from Future interface spec: "Memory consistency effects: Actions taken by the asynchronous computation happen-before actions following the corresponding Future.get() in another thread."
                     * it means it don't have to be volatile, because AsyncTask uses FutureTask to deliver result.
                     */
                    boolean jsonInput;
                    String jsonParseError;

                    @Override
                    protected ArrayList<UnspentOutputInfo> doInBackground(Void... params) {
                        try {
                            byte[] outputScriptWeAreAbleToSpend = Transaction.Script
                                    .buildOutput(keyPair.address).bytes;
                            ArrayList<UnspentOutputInfo> unspentOutputs = new ArrayList<>();
                            //1. decode tx or json
                            byte[] rawTx = BTCUtils.fromHex(unspentOutputsInfoStr.trim());
                            if (rawTx != null) {
                                Transaction baseTx = new Transaction(rawTx);
                                byte[] rawTxReconstructed = baseTx.getBytes();
                                if (!Arrays.equals(rawTxReconstructed, rawTx)) {
                                    throw new IllegalArgumentException("Unable to decode given transaction");
                                }
                                jsonInput = false;
                                byte[] txHash = BTCUtils.reverse(BTCUtils.doubleSha256(rawTx));
                                for (int outputIndex = 0; outputIndex < baseTx.outputs.length; outputIndex++) {
                                    Transaction.Output output = baseTx.outputs[outputIndex];
                                    if (Arrays.equals(outputScriptWeAreAbleToSpend, output.script.bytes)) {
                                        unspentOutputs.add(new UnspentOutputInfo(txHash, output.script,
                                                output.value, outputIndex, verifiedConfirmationsCount));
                                    }
                                }
                            } else {
                                String jsonStr = unspentOutputsInfoStr.replace((char) 160, ' ').trim();//remove nbsp
                                if (!jsonStr.startsWith("{")) {
                                    jsonStr = "{" + jsonStr;
                                }
                                if (!jsonStr.endsWith("}")) {
                                    jsonStr += "}";
                                }
                                JSONObject jsonObject = new JSONObject(jsonStr);
                                jsonInput = true;
                                JSONArray unspentOutputsArray = jsonObject.getJSONArray("unspent_outputs");
                                if (unspentOutputsArray == null) {
                                    jsonParseError = getString(R.string.json_err_no_unspent_outputs);
                                    return null;
                                }
                                for (int i = 0; i < unspentOutputsArray.length(); i++) {
                                    JSONObject unspentOutput = unspentOutputsArray.getJSONObject(i);
                                    byte[] txHash = BTCUtils
                                            .reverse(BTCUtils.fromHex(unspentOutput.getString("tx_hash")));
                                    Transaction.Script script = new Transaction.Script(
                                            BTCUtils.fromHex(unspentOutput.getString("script")));
                                    if (Arrays.equals(outputScriptWeAreAbleToSpend, script.bytes)) {
                                        long value = unspentOutput.getLong("value");
                                        long confirmations = unspentOutput.getLong("confirmations");
                                        int outputIndex = (int) unspentOutput.getLong("tx_output_n");
                                        unspentOutputs.add(new UnspentOutputInfo(txHash, script, value, outputIndex,
                                                confirmations));
                                    }
                                }
                            }
                            jsonParseError = null;
                            return unspentOutputs;
                        } catch (Exception e) {
                            jsonParseError = e.getMessage();
                            return null;
                        }

                    }

                    @Override
                    protected void onPostExecute(ArrayList<UnspentOutputInfo> unspentOutputInfos) {
                        verifiedUnspentOutputsForTx = unspentOutputInfos;
                        verifiedUnspentOutputsComesFromJson = jsonInput;
                        if (unspentOutputInfos == null) {
                            if (jsonInput && !TextUtils.isEmpty(jsonParseError)) {
                                rawTxToSpendErr.setText(getString(R.string.error_unable_to_decode_json_transaction,
                                        jsonParseError));
                            } else {
                                rawTxToSpendErr.setText(R.string.error_unable_to_decode_transaction);
                            }
                        } else if (unspentOutputInfos.isEmpty()) {
                            rawTxToSpendErr
                                    .setText(getString(R.string.error_no_spendable_outputs_found, keyPair.address));
                        } else {
                            rawTxToSpendErr.setText("");
                            long availableAmount = 0;
                            for (UnspentOutputInfo unspentOutputInfo : unspentOutputInfos) {
                                availableAmount += unspentOutputInfo.value;
                            }
                            amountEdit.setHint(BTCUtils.formatValue(availableAmount));
                            if (TextUtils.isEmpty(getString(amountEdit))) {
                                verifiedAmountToSendForTx = SEND_MAX;
                            }
                            tryToGenerateSpendingTransaction();
                        }
                    }
                };
                decodeUnspentOutputsInfoTask.execute();
            }
        } else {
            verifiedKeyPairForTx = null;
        }
    }

    private void onSendAmountChanged(String amountStr) {
        TextView amountError = (TextView) findViewById(R.id.err_amount);
        if (TextUtils.isEmpty(amountStr)) {
            verifiedAmountToSendForTx = SEND_MAX;
            amountError.setText("");
            tryToGenerateSpendingTransaction();
        } else {
            try {
                double requestedAmountToSendDouble = Double.parseDouble(amountStr);
                long requestedAmountToSend = (long) (requestedAmountToSendDouble * 1e8);
                if (requestedAmountToSendDouble > 0 && requestedAmountToSendDouble < 21000000
                        && requestedAmountToSend > 0) {
                    verifiedAmountToSendForTx = requestedAmountToSend;
                    amountError.setText("");
                    tryToGenerateSpendingTransaction();
                } else {
                    verifiedAmountToSendForTx = AMOUNT_ERR;
                    amountError.setText(R.string.error_amount_parsing);
                }
            } catch (Exception e) {
                verifiedAmountToSendForTx = AMOUNT_ERR;
                amountError.setText(R.string.error_amount_parsing);
            }
        }
    }

    private static String getString(TextView textView) {
        CharSequence charSequence = textView.getText();
        return charSequence == null ? "" : charSequence.toString();
    }

    private void encryptOrDecryptPrivateKey() {
        final KeyPair inputKeyPair = currentKeyPair;
        final String password = getString(passwordEdit);
        if (inputKeyPair != null && !TextUtils.isEmpty(password)) {
            cancelAllRunningTasks();
            final boolean decrypting = inputKeyPair.privateKey.type == BTCUtils.Bip38PrivateKeyInfo.TYPE_BIP38
                    && inputKeyPair.privateKey.privateKeyDecoded == null;
            lastBip38ActionWasDecryption = decrypting;
            passwordButton.setEnabled(false);
            passwordButton.setText(decrypting ? R.string.decrypting : R.string.encrypting);
            InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(
                    Context.INPUT_METHOD_SERVICE);
            inputMethodManager.hideSoftInputFromWindow(passwordEdit.getWindowToken(), 0);

            bip38Task = new AsyncTask<Void, Void, Object>() {
                ProgressDialog dialog;
                public boolean sendLayoutVisible;

                @Override
                protected void onPreExecute() {
                    super.onPreExecute();
                    dialog = ProgressDialog.show(MainActivity.this, "",
                            (decrypting ? getString(R.string.decrypting) : getString(R.string.encrypting)), true);
                    dialog.setCancelable(true);
                    dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
                        @Override
                        public void onCancel(DialogInterface dialog) {
                            bip38Task.cancel(true);
                            bip38Task = null;
                        }
                    });
                    sendLayoutVisible = sendLayout.isShown();
                }

                @Override
                protected Object doInBackground(Void... params) {
                    try {
                        if (decrypting) {
                            return BTCUtils.bip38Decrypt(inputKeyPair.privateKey.privateKeyEncoded, password);
                        } else {
                            String encryptedPrivateKey = BTCUtils.bip38Encrypt(inputKeyPair, password);
                            return new KeyPair(new BTCUtils.Bip38PrivateKeyInfo(encryptedPrivateKey,
                                    inputKeyPair.privateKey.privateKeyDecoded, password,
                                    inputKeyPair.privateKey.isPublicKeyCompressed));
                        }
                    } catch (OutOfMemoryError e) {
                        return R.string.error_oom_bip38;
                    } catch (Throwable e) {
                        String msg = e.getMessage();
                        if (msg != null && msg.contains("OutOfMemoryError")) {
                            return R.string.error_oom_bip38;
                        } else {
                            return null;
                        }
                    }
                }

                @Override
                protected void onPostExecute(Object result) {
                    bip38Task = null;
                    dialog.dismiss();
                    if (result instanceof KeyPair) {
                        KeyPair keyPair = (KeyPair) result;
                        insertingPrivateKeyProgrammatically = true;
                        privateKeyTextEdit.setText(keyPair.privateKey.privateKeyEncoded);
                        insertingPrivateKeyProgrammatically = false;
                        onKeyPairModify(false, keyPair);
                        if (!decrypting) {
                            sendLayout.setVisibility(sendLayoutVisible ? View.VISIBLE : View.GONE);
                        }
                    } else if (result instanceof Integer || !decrypting) {
                        onKeyPairModify(false, inputKeyPair);
                        new AlertDialog.Builder(MainActivity.this)
                                .setMessage(getString(
                                        result instanceof Integer ? (Integer) result : R.string.error_unknown))
                                .setPositiveButton(android.R.string.ok, null).show();
                    } else {
                        onKeyPairModify(false, inputKeyPair);
                        ((TextView) findViewById(R.id.err_password)).setText(R.string.incorrect_password);
                    }
                }

                @Override
                protected void onCancelled() {
                    super.onCancelled();
                    bip38Task = null;
                    dialog.dismiss();
                    onKeyPairModify(false, currentKeyPair);
                }
            }.execute();
        }
    }

    private void showQRCodePopupForAddress(final String address) {
        DisplayMetrics dm = getResources().getDisplayMetrics();
        final int screenSize = Math.min(dm.widthPixels, dm.heightPixels);
        final String uriStr = SCHEME_BITCOIN + address;
        new AsyncTask<Void, Void, Bitmap>() {

            @Override
            protected Bitmap doInBackground(Void... params) {
                return QRCode.getMinimumQRCode(uriStr, ErrorCorrectLevel.M).createImage(screenSize / 2);
            }

            @Override
            protected void onPostExecute(final Bitmap bitmap) {
                if (bitmap != null) {
                    View view = getLayoutInflater().inflate(R.layout.address_qr, mainLayout, false);
                    if (view != null) {
                        final ImageView qrView = (ImageView) view.findViewById(R.id.qr_code_image);
                        qrView.setImageBitmap(bitmap);

                        final TextView bitcoinProtocolLinkView = (TextView) view.findViewById(R.id.link1);
                        SpannableStringBuilder labelUri = new SpannableStringBuilder(uriStr);
                        ClickableSpan urlSpan = new ClickableSpan() {
                            @Override
                            public void onClick(View widget) {
                                Intent intent = new Intent(Intent.ACTION_VIEW);
                                intent.setData(Uri.parse(uriStr));
                                try {
                                    startActivity(intent);
                                } catch (Exception e) {
                                    Toast.makeText(MainActivity.this, R.string.no_apps_to_view_url,
                                            Toast.LENGTH_LONG).show();
                                }
                            }
                        };
                        labelUri.setSpan(urlSpan, 0, labelUri.length(),
                                SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
                        bitcoinProtocolLinkView.setText(labelUri);
                        bitcoinProtocolLinkView.setMovementMethod(LinkMovementMethod.getInstance());

                        final TextView blockexplorerLinkView = (TextView) view.findViewById(R.id.link2);
                        SpannableStringBuilder blockexplorerLinkText = new SpannableStringBuilder(
                                "blockexplorer.com");
                        setUrlSpanForAddress("blockexplorer.com", address, blockexplorerLinkText);
                        blockexplorerLinkView.setText(blockexplorerLinkText);
                        blockexplorerLinkView.setMovementMethod(LinkMovementMethod.getInstance());

                        final TextView blockchainLinkView = (TextView) view.findViewById(R.id.link3);
                        SpannableStringBuilder blockchainLinkText = new SpannableStringBuilder("blockchain.info");
                        setUrlSpanForAddress("blockchain.info", address, blockchainLinkText);
                        blockchainLinkView.setText(blockchainLinkText);
                        blockchainLinkView.setMovementMethod(LinkMovementMethod.getInstance());

                        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                        builder.setTitle(address);
                        builder.setView(view);
                        if (systemSupportsPrint()) {
                            builder.setPositiveButton(R.string.print, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    Renderer.printQR(MainActivity.this, SCHEME_BITCOIN + address);
                                }
                            });
                            builder.setNegativeButton(android.R.string.cancel, null);
                        } else {
                            builder.setPositiveButton(android.R.string.ok, null);
                        }

                        builder.show();
                    }
                }
            }
        }.execute();
    }

    private void showQRCodePopupForPrivateKey(final String label, final String address, final String[] data,
            final String[] dataTypes) {
        DisplayMetrics dm = getResources().getDisplayMetrics();
        final int screenSize = Math.min(dm.widthPixels, dm.heightPixels);
        new AsyncTask<Void, Void, Bitmap[]>() {

            @Override
            protected Bitmap[] doInBackground(Void... params) {
                Bitmap[] result = new Bitmap[data.length];
                for (int i = 0; i < data.length; i++) {
                    if (data[i] != null) {
                        QRCode qr = QRCode.getMinimumQRCode(data[i], ErrorCorrectLevel.M);
                        result[i] = qr.createImage(screenSize / 2);
                    }
                }
                return result;
            }

            @Override
            protected void onPostExecute(final Bitmap[] bitmap) {
                if (bitmap != null) {
                    View view = getLayoutInflater().inflate(R.layout.private_key_qr, mainLayout, false);
                    if (view != null) {
                        final ToggleButton toggle1 = (ToggleButton) view.findViewById(R.id.toggle_1);
                        final ToggleButton toggle2 = (ToggleButton) view.findViewById(R.id.toggle_2);
                        final ToggleButton toggle3 = (ToggleButton) view.findViewById(R.id.toggle_3);
                        final ImageView qrView = (ImageView) view.findViewById(R.id.qr_code_image);
                        final TextView dataView = (TextView) view.findViewById(R.id.qr_code_data);

                        if (data[0] == null) {
                            toggle1.setVisibility(View.GONE);
                        } else {
                            toggle1.setTextOff(dataTypes[0]);
                            toggle1.setTextOn(dataTypes[0]);
                            toggle1.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                                @Override
                                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                                    if (isChecked) {
                                        toggle2.setChecked(false);
                                        toggle3.setChecked(false);
                                        qrView.setImageBitmap(bitmap[0]);
                                        dataView.setText(data[0]);
                                    } else if (!toggle2.isChecked() && !toggle3.isChecked()) {
                                        buttonView.setChecked(true);
                                    }
                                }
                            });
                        }
                        if (data[1] == null) {
                            toggle2.setVisibility(View.GONE);
                        } else {
                            toggle2.setTextOff(dataTypes[1]);
                            toggle2.setTextOn(dataTypes[1]);
                            toggle2.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                                @Override
                                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                                    if (isChecked) {
                                        toggle1.setChecked(false);
                                        toggle3.setChecked(false);
                                        qrView.setImageBitmap(bitmap[1]);
                                        dataView.setText(data[1]);
                                    } else if (!toggle1.isChecked() && !toggle3.isChecked()) {
                                        buttonView.setChecked(true);
                                    }
                                }
                            });
                        }
                        if (data[2] == null) {
                            toggle3.setVisibility(View.GONE);
                        } else {
                            toggle3.setTextOff(dataTypes[2]);
                            toggle3.setTextOn(dataTypes[2]);
                            toggle3.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                                @Override
                                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                                    if (isChecked) {
                                        toggle1.setChecked(false);
                                        toggle2.setChecked(false);
                                        qrView.setImageBitmap(bitmap[2]);
                                        dataView.setText(data[2]);
                                    } else if (!toggle1.isChecked() && !toggle2.isChecked()) {
                                        buttonView.setChecked(true);
                                    }
                                }
                            });
                        }
                        if (data[2] != null) {
                            toggle3.setChecked(true);
                        } else if (data[0] != null) {
                            toggle1.setChecked(true);
                        } else {
                            toggle2.setChecked(true);
                        }

                        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                        builder.setTitle(label);
                        builder.setView(view);
                        DialogInterface.OnClickListener shareClickListener = new DialogInterface.OnClickListener() {

                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                int selectedIndex;
                                if (toggle1.isChecked()) {
                                    selectedIndex = 0;
                                } else if (toggle2.isChecked()) {
                                    selectedIndex = 1;
                                } else {
                                    selectedIndex = 2;
                                }
                                Intent intent = new Intent(Intent.ACTION_SEND);
                                intent.setType("text/plain");
                                intent.putExtra(Intent.EXTRA_SUBJECT, label);
                                intent.putExtra(Intent.EXTRA_TEXT, data[selectedIndex]);
                                startActivity(
                                        Intent.createChooser(intent, getString(R.string.share_chooser_title)));
                            }
                        };
                        if (systemSupportsPrint()) {
                            builder.setPositiveButton(R.string.print, new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    int selectedIndex;
                                    if (toggle1.isChecked()) {
                                        selectedIndex = 0;
                                    } else if (toggle2.isChecked()) {
                                        selectedIndex = 1;
                                    } else {
                                        selectedIndex = 2;
                                    }
                                    Renderer.printWallet(MainActivity.this, label, SCHEME_BITCOIN + address,
                                            data[selectedIndex]);
                                }
                            });
                            builder.setNeutralButton(R.string.share, shareClickListener);
                        } else {
                            builder.setPositiveButton(R.string.share, shareClickListener);
                        }
                        builder.setNegativeButton(android.R.string.cancel, null);
                        builder.show();
                    }
                }
            }
        }.execute();
    }

    private static boolean systemSupportsPrint() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    }

    private static int getColor(Context context, int id) {
        if (Build.VERSION.SDK_INT >= 23) {
            return context.getColor(id);
        } else {
            //noinspection deprecation
            return context.getResources().getColor(id);
        }
    }

    private void onNewKeyPairGenerated(KeyPair keyPair) {
        insertingAddressProgrammatically = true;
        if (keyPair != null) {
            addressTextEdit.setText(keyPair.address);
            privateKeyTypeView.setVisibility(View.VISIBLE);
            privateKeyTypeView.setText(getPrivateKeyTypeLabel(keyPair));
            insertingPrivateKeyProgrammatically = true;
            privateKeyTextEdit.setText(keyPair.privateKey.privateKeyEncoded);
            insertingPrivateKeyProgrammatically = false;
        } else {
            privateKeyTypeView.setVisibility(View.GONE);
            addressTextEdit.setText(getString(R.string.generating_failed));
        }
        insertingAddressProgrammatically = false;
        updatePasswordView(keyPair);
        showSpendPanelForKeyPair(keyPair);
    }

    private void onKeyPairModify(boolean noPrivateKeyEntered, KeyPair keyPair) {
        insertingAddressProgrammatically = true;
        if (keyPair != null) {
            if (!TextUtils.isEmpty(keyPair.address)) {
                addressTextEdit.setText(keyPair.address);
            } else {
                addressTextEdit.setText(getString(R.string.not_decrypted_yet));
            }
            privateKeyTypeView.setVisibility(View.VISIBLE);
            privateKeyTypeView.setText(getPrivateKeyTypeLabel(keyPair));
        } else {
            privateKeyTypeView.setVisibility(View.GONE);
            addressTextEdit.setText(noPrivateKeyEntered ? "" : getString(R.string.bad_private_key));
        }
        insertingAddressProgrammatically = false;
        updatePasswordView(keyPair);
        showSpendPanelForKeyPair(keyPair);
    }

    private void updatePasswordView(KeyPair keyPair) {
        currentKeyPair = keyPair;
        String encodedPrivateKey = keyPair == null ? null : keyPair.privateKey.privateKeyEncoded;
        passwordButton
                .setEnabled(!TextUtils.isEmpty(passwordEdit.getText()) && !TextUtils.isEmpty(encodedPrivateKey));
        showQRCodePrivateKeyButton.setVisibility(keyPair == null ? View.GONE : View.VISIBLE);
        ((TextView) findViewById(R.id.err_password)).setText("");
        if (keyPair != null && keyPair.privateKey.type == BTCUtils.Bip38PrivateKeyInfo.TYPE_BIP38) {
            if (keyPair.privateKey.privateKeyDecoded == null) {
                passwordButton.setText(R.string.decrypt_private_key);
                passwordEdit.setImeActionLabel(getString(R.string.ime_decrypt), R.id.action_decrypt);
            } else {
                if (getString(passwordEdit).equals(((BTCUtils.Bip38PrivateKeyInfo) keyPair.privateKey).password)) {
                    passwordButton.setText(
                            getString(lastBip38ActionWasDecryption ? R.string.decrypted : R.string.encrypted));
                    passwordButton.setEnabled(false);
                } else {
                    passwordButton.setText(getString(R.string.encrypt_private_key));
                    passwordButton.setEnabled(true);
                }
                passwordEdit.setImeActionLabel(getString(R.string.ime_encrypt), R.id.action_encrypt);
            }
        } else {
            passwordButton.setText(R.string.encrypt_private_key);
            passwordEdit.setImeActionLabel(getString(R.string.ime_encrypt), R.id.action_encrypt);
        }
        onUnspentOutputsInfoChanged();
    }

    private void cancelAllRunningTasks() {
        if (bip38Task != null) {
            bip38Task.cancel(true);
            bip38Task = null;
        }
        if (addressGenerateTask != null) {
            addressGenerateTask.cancel(true);
            addressGenerateTask = null;
        }
        if (generateTransactionTask != null) {
            generateTransactionTask.cancel(true);
            generateTransactionTask = null;
        }
        if (switchingCompressionTypeTask != null) {
            switchingCompressionTypeTask.cancel(false);
            switchingCompressionTypeTask = null;
        }
        if (decodePrivateKeyTask != null) {
            decodePrivateKeyTask.cancel(true);
            decodePrivateKeyTask = null;
        }
        if (decodeUnspentOutputsInfoTask != null) {
            decodeUnspentOutputsInfoTask.cancel(true);
            decodeUnspentOutputsInfoTask = null;
        }
    }

    static class GenerateTransactionResult {
        static final int ERROR_SOURCE_UNKNOWN = 0;
        static final int ERROR_SOURCE_INPUT_TX_FIELD = 1;
        static final int ERROR_SOURCE_ADDRESS_FIELD = 2;
        static final int HINT_FOR_ADDRESS_FIELD = 3;
        static final int ERROR_SOURCE_AMOUNT_FIELD = 4;

        final Transaction tx;
        final String errorMessage;
        final int errorSource;
        final long fee;

        public GenerateTransactionResult(String errorMessage, int errorSource) {
            tx = null;
            this.errorMessage = errorMessage;
            this.errorSource = errorSource;
            fee = -1;
        }

        public GenerateTransactionResult(Transaction tx, long fee) {
            this.tx = tx;
            errorMessage = null;
            errorSource = ERROR_SOURCE_UNKNOWN;
            this.fee = fee;
        }
    }

    private void tryToGenerateSpendingTransaction() {
        final ArrayList<UnspentOutputInfo> unspentOutputs = verifiedUnspentOutputsForTx;
        final String outputAddress = verifiedRecipientAddressForTx;
        final long requestedAmountToSend = verifiedAmountToSendForTx;
        final KeyPair keyPair = verifiedKeyPairForTx;
        final boolean inputsComesFromJson = verifiedUnspentOutputsComesFromJson;
        final int predefinedConfirmationsCount = verifiedConfirmationsCount;

        spendTxDescriptionView.setVisibility(View.GONE);
        spendTxWarningView.setVisibility(View.GONE);
        spendTxEdit.setText("");
        spendTxEdit.setVisibility(View.GONE);
        sendTxInBrowserButton.setVisibility(View.GONE);
        findViewById(R.id.spend_tx_required_age_for_free_tx).setVisibility(View.GONE);
        //        https://blockchain.info/pushtx

        if (unspentOutputs != null && !unspentOutputs.isEmpty() && !TextUtils.isEmpty(outputAddress)
                && keyPair != null && requestedAmountToSend >= SEND_MAX && requestedAmountToSend != 0
                && !TextUtils.isEmpty(keyPair.address)) {
            cancelAllRunningTasks();
            generateTransactionTask = new AsyncTask<Void, Void, GenerateTransactionResult>() {

                @Override
                protected GenerateTransactionResult doInBackground(Void... voids) {
                    final Transaction spendTx;
                    try {
                        long availableAmount = 0;
                        for (UnspentOutputInfo unspentOutputInfo : unspentOutputs) {
                            availableAmount += unspentOutputInfo.value;
                        }
                        long amount;
                        if (availableAmount == requestedAmountToSend || requestedAmountToSend == SEND_MAX) {
                            //transfer maximum possible amount
                            amount = -1;
                        } else {
                            amount = requestedAmountToSend;
                        }
                        long extraFee;
                        SharedPreferences preferences = PreferenceManager
                                .getDefaultSharedPreferences(MainActivity.this);
                        try {
                            extraFee = preferences.getLong(PreferencesActivity.PREF_EXTRA_FEE,
                                    FeePreference.PREF_EXTRA_FEE_DEFAULT);
                        } catch (ClassCastException e) {
                            preferences.edit().remove(PreferencesActivity.PREF_EXTRA_FEE)
                                    .putLong(PreferencesActivity.PREF_EXTRA_FEE,
                                            FeePreference.PREF_EXTRA_FEE_DEFAULT)
                                    .commit();
                            extraFee = FeePreference.PREF_EXTRA_FEE_DEFAULT;
                        }
                        spendTx = BTCUtils.createTransaction(unspentOutputs, outputAddress, keyPair.address, amount,
                                extraFee, keyPair.publicKey, keyPair.privateKey);

                        //6. double check that generated transaction is valid
                        Transaction.Script[] relatedScripts = new Transaction.Script[spendTx.inputs.length];
                        for (int i = 0; i < spendTx.inputs.length; i++) {
                            Transaction.Input input = spendTx.inputs[i];
                            for (UnspentOutputInfo unspentOutput : unspentOutputs) {
                                if (Arrays.equals(unspentOutput.txHash, input.outPoint.hash)
                                        && unspentOutput.outputIndex == input.outPoint.index) {
                                    relatedScripts[i] = unspentOutput.script;
                                    break;
                                }
                            }
                        }
                        BTCUtils.verify(relatedScripts, spendTx);
                    } catch (BitcoinException e) {
                        switch (e.errorCode) {
                        case BitcoinException.ERR_INSUFFICIENT_FUNDS:
                            return new GenerateTransactionResult(getString(R.string.error_not_enough_funds),
                                    GenerateTransactionResult.ERROR_SOURCE_AMOUNT_FIELD);
                        case BitcoinException.ERR_FEE_IS_TOO_BIG:
                            return new GenerateTransactionResult(getString(R.string.generated_tx_have_too_big_fee),
                                    GenerateTransactionResult.ERROR_SOURCE_INPUT_TX_FIELD);
                        case BitcoinException.ERR_MEANINGLESS_OPERATION://input, output and change addresses are same.
                            return new GenerateTransactionResult(getString(R.string.output_address_same_as_input),
                                    GenerateTransactionResult.ERROR_SOURCE_ADDRESS_FIELD);
                        //                            case BitcoinException.ERR_INCORRECT_PASSWORD
                        //                            case BitcoinException.ERR_WRONG_TYPE:
                        //                            case BitcoinException.ERR_FEE_IS_LESS_THEN_ZERO
                        //                            case BitcoinException.ERR_CHANGE_IS_LESS_THEN_ZERO
                        //                            case BitcoinException.ERR_AMOUNT_TO_SEND_IS_LESS_THEN_ZERO
                        default:
                            return new GenerateTransactionResult(
                                    getString(R.string.error_failed_to_create_transaction) + ": " + e.getMessage(),
                                    GenerateTransactionResult.ERROR_SOURCE_UNKNOWN);
                        }
                    } catch (Exception e) {
                        return new GenerateTransactionResult(
                                getString(R.string.error_failed_to_create_transaction) + ": " + e,
                                GenerateTransactionResult.ERROR_SOURCE_UNKNOWN);
                    }

                    long inValue = 0;
                    for (Transaction.Input input : spendTx.inputs) {
                        for (UnspentOutputInfo unspentOutput : unspentOutputs) {
                            if (Arrays.equals(unspentOutput.txHash, input.outPoint.hash)
                                    && unspentOutput.outputIndex == input.outPoint.index) {
                                inValue += unspentOutput.value;
                            }
                        }
                    }
                    long outValue = 0;
                    for (Transaction.Output output : spendTx.outputs) {
                        outValue += output.value;
                    }
                    long fee = inValue - outValue;
                    return new GenerateTransactionResult(spendTx, fee);
                }

                @Override
                protected void onPostExecute(GenerateTransactionResult result) {
                    super.onPostExecute(result);
                    generateTransactionTask = null;
                    if (result != null) {
                        final TextView rawTxToSpendErr = (TextView) findViewById(R.id.err_raw_tx);
                        if (result.tx != null) {
                            String amountStr = null;
                            Transaction.Script out = null;
                            try {
                                out = Transaction.Script.buildOutput(outputAddress);
                            } catch (BitcoinException ignore) {
                            }
                            if (result.tx.outputs[0].script.equals(out)) {
                                amountStr = BTCUtils.formatValue(result.tx.outputs[0].value);
                            }
                            if (amountStr == null) {
                                rawTxToSpendErr.setText(R.string.error_unknown);
                            } else {
                                String descStr;
                                String feeStr = BTCUtils.formatValue(result.fee);
                                String changeStr;
                                if (result.tx.outputs.length == 1) {
                                    changeStr = null;
                                    descStr = getString(R.string.spend_tx_description, amountStr, keyPair.address,
                                            outputAddress, feeStr);
                                } else if (result.tx.outputs.length == 2) {
                                    changeStr = BTCUtils.formatValue(result.tx.outputs[1].value);
                                    descStr = getString(R.string.spend_tx_with_change_description, amountStr,
                                            keyPair.address, outputAddress, feeStr, changeStr);
                                } else {
                                    throw new RuntimeException();
                                }
                                SpannableStringBuilder descBuilder = new SpannableStringBuilder(descStr);

                                int spanBegin = descStr.indexOf(keyPair.address);
                                if (spanBegin >= 0) {//from
                                    ForegroundColorSpan addressColorSpan = new ForegroundColorSpan(
                                            getColor(MainActivity.this, R.color.dark_orange));
                                    descBuilder.setSpan(addressColorSpan, spanBegin,
                                            spanBegin + keyPair.address.length(),
                                            SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
                                }
                                if (spanBegin >= 0) {
                                    spanBegin = descStr.indexOf(keyPair.address, spanBegin + 1);
                                    if (spanBegin >= 0) {//change
                                        ForegroundColorSpan addressColorSpan = new ForegroundColorSpan(
                                                getColor(MainActivity.this, R.color.dark_orange));
                                        descBuilder.setSpan(addressColorSpan, spanBegin,
                                                spanBegin + keyPair.address.length(),
                                                SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
                                    }
                                }
                                spanBegin = descStr.indexOf(outputAddress);
                                if (spanBegin >= 0) {//dest
                                    ForegroundColorSpan addressColorSpan = new ForegroundColorSpan(
                                            getColor(MainActivity.this, R.color.dark_green));
                                    descBuilder.setSpan(addressColorSpan, spanBegin,
                                            spanBegin + outputAddress.length(),
                                            SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
                                }
                                final String nbspBtc = "\u00a0BTC";
                                spanBegin = descStr.indexOf(amountStr + nbspBtc);
                                if (spanBegin >= 0) {
                                    descBuilder.setSpan(new StyleSpan(Typeface.BOLD), spanBegin,
                                            spanBegin + amountStr.length() + nbspBtc.length(),
                                            SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
                                }
                                spanBegin = descStr.indexOf(feeStr + nbspBtc, spanBegin);
                                if (spanBegin >= 0) {
                                    descBuilder.setSpan(new StyleSpan(Typeface.BOLD), spanBegin,
                                            spanBegin + feeStr.length() + nbspBtc.length(),
                                            SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
                                }
                                if (changeStr != null) {
                                    spanBegin = descStr.indexOf(changeStr + nbspBtc, spanBegin);
                                    if (spanBegin >= 0) {
                                        descBuilder.setSpan(new StyleSpan(Typeface.BOLD), spanBegin,
                                                spanBegin + changeStr.length() + nbspBtc.length(),
                                                SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
                                    }
                                }
                                spendTxDescriptionView.setText(descBuilder);
                                spendTxDescriptionView.setVisibility(View.VISIBLE);
                                spendTxWarningView.setVisibility(View.VISIBLE);
                                spendTxEdit.setText(BTCUtils.toHex(result.tx.getBytes()));
                                spendTxEdit.setVisibility(View.VISIBLE);
                                sendTxInBrowserButton.setVisibility(View.VISIBLE);

                                TextView maxAgeView = (TextView) findViewById(
                                        R.id.spend_tx_required_age_for_free_tx);
                                CheckBox maxAgeCheckBox = (CheckBox) findViewById(
                                        R.id.spend_tx_required_age_for_free_tx_checkbox);
                                if (!inputsComesFromJson) {
                                    if (!showNotEligibleForNoFeeBecauseOfBasicConstrains(maxAgeView, result.tx)) {
                                        final int confirmations = (int) (BTCUtils.MIN_PRIORITY_FOR_NO_FEE
                                                * result.tx.getBytes().length / unspentOutputs.get(0).value);
                                        float daysFloat = confirmations / BTCUtils.EXPECTED_BLOCKS_PER_DAY;
                                        String timePeriodStr;
                                        if (daysFloat <= 1) {
                                            int hours = (int) Math.round(Math.ceil(daysFloat / 24));
                                            timePeriodStr = getResources().getQuantityString(R.plurals.hours, hours,
                                                    hours);
                                        } else {
                                            int days = (int) Math.round(Math.ceil(daysFloat));
                                            timePeriodStr = getResources().getQuantityString(R.plurals.days, days,
                                                    days);
                                        }
                                        maxAgeCheckBox.setText(getString(R.string.input_tx_is_old_enough,
                                                getResources().getQuantityString(R.plurals.confirmations,
                                                        confirmations, confirmations),
                                                timePeriodStr));
                                        maxAgeCheckBox.setVisibility(View.VISIBLE);
                                        maxAgeCheckBox.setOnCheckedChangeListener(null);
                                        maxAgeCheckBox.setChecked(predefinedConfirmationsCount > 0);
                                        maxAgeCheckBox.setOnCheckedChangeListener(
                                                new CompoundButton.OnCheckedChangeListener() {
                                                    @Override
                                                    public void onCheckedChanged(CompoundButton buttonView,
                                                            boolean isChecked) {
                                                        verifiedConfirmationsCount = isChecked ? confirmations : -1;
                                                        onUnspentOutputsInfoChanged();
                                                    }
                                                });
                                    } else {
                                        maxAgeCheckBox.setVisibility(View.GONE);
                                    }
                                } else {
                                    showNotEligibleForNoFeeBecauseOfBasicConstrains(maxAgeView, result.tx);
                                }
                            }
                        } else if (result.errorSource == GenerateTransactionResult.ERROR_SOURCE_INPUT_TX_FIELD) {
                            rawTxToSpendErr.setText(result.errorMessage);
                        } else if (result.errorSource == GenerateTransactionResult.ERROR_SOURCE_ADDRESS_FIELD
                                || result.errorSource == GenerateTransactionResult.HINT_FOR_ADDRESS_FIELD) {
                            ((TextView) findViewById(R.id.err_recipient_address)).setText(result.errorMessage);
                        } else if (!TextUtils.isEmpty(result.errorMessage)
                                && result.errorSource == GenerateTransactionResult.ERROR_SOURCE_UNKNOWN) {
                            new AlertDialog.Builder(MainActivity.this).setMessage(result.errorMessage)
                                    .setPositiveButton(android.R.string.ok, null).show();
                        }

                        ((TextView) findViewById(R.id.err_amount))
                                .setText(result.errorSource == GenerateTransactionResult.ERROR_SOURCE_AMOUNT_FIELD
                                        ? result.errorMessage
                                        : "");
                    }
                }

                private boolean showNotEligibleForNoFeeBecauseOfBasicConstrains(TextView maxAgeView,
                        Transaction tx) {
                    long minOutput = Long.MAX_VALUE;
                    for (Transaction.Output output : tx.outputs) {
                        minOutput = Math.min(output.value, minOutput);
                    }
                    int txLen = tx.getBytes().length;
                    if (txLen >= BTCUtils.MAX_TX_LEN_FOR_NO_FEE) {
                        maxAgeView.setText(
                                getResources().getQuantityText(R.plurals.tx_size_too_big_to_be_free, txLen));
                        maxAgeView.setVisibility(View.VISIBLE);
                        return true;
                    } else if (minOutput < BTCUtils.MIN_MIN_OUTPUT_VALUE_FOR_NO_FEE) {
                        maxAgeView
                                .setText(getString(R.string.tx_output_is_too_small, BTCUtils.formatValue(minOutput),
                                        BTCUtils.formatValue(BTCUtils.MIN_MIN_OUTPUT_VALUE_FOR_NO_FEE)));
                        maxAgeView.setVisibility(View.VISIBLE);
                        return true;
                    }
                    maxAgeView.setVisibility(View.GONE);
                    return false;
                }
            }.execute();
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        addressTextEdit.setMinLines(1);
        privateKeyTextEdit.setMinLines(1);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.action_settings) {
            startActivity(new Intent(this,
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? PreferencesActivity.class
                            : PreferencesActivityForOlderDevices.class));
            return true;
        } else {
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            String scannedResult = data.getStringExtra("data");
            String address = scannedResult;
            String privateKey = scannedResult;
            String amount = null;
            String message = "";
            if (scannedResult != null && scannedResult.startsWith(SCHEME_BITCOIN)) {
                scannedResult = scannedResult.substring(SCHEME_BITCOIN.length());
                while (scannedResult.startsWith("/")) {
                    scannedResult = scannedResult.substring(1);
                }
                privateKey = "";
                int queryStartIndex = scannedResult.indexOf('?');
                if (queryStartIndex == -1) {
                    address = scannedResult;
                } else {
                    address = scannedResult.substring(0, queryStartIndex);
                    while (address.endsWith("/")) {
                        address = address.substring(0, address.length() - 1);
                    }
                    String queryStr = scannedResult.substring(queryStartIndex + 1);
                    Map<String, String> query = splitQuery(queryStr);
                    String amountStr = query.get("amount");
                    if (!TextUtils.isEmpty(amountStr)) {
                        try {
                            amount = BTCUtils.formatValue(BTCUtils.parseValue(amountStr));
                        } catch (NumberFormatException e) {
                            Log.e("PaperWallet", "unable to parse " + amountStr);
                        }
                    }
                    StringBuilder messageSb = new StringBuilder();
                    String label = query.get("label");
                    if (!TextUtils.isEmpty(label)) {
                        messageSb.append(label);
                    }
                    String messageParam = query.get("message");
                    if (!TextUtils.isEmpty(messageParam)) {
                        if (messageSb.length() > 0) {
                            messageSb.append(": ");
                        }
                        messageSb.append(messageParam);
                    }
                    message = messageSb.toString();
                }
            }
            if (requestCode == REQUEST_SCAN_PRIVATE_KEY) {
                if (!TextUtils.isEmpty(privateKey)) {
                    privateKeyTextEdit.setText(privateKey);
                }
            } else if (requestCode == REQUEST_SCAN_RECIPIENT_ADDRESS) {
                recipientAddressView.setText(address);
                if (!TextUtils.isEmpty(amount)) {
                    amountEdit.setText(amount);
                }
                if (!TextUtils.isEmpty(message)) {
                    Toast.makeText(MainActivity.this, message,
                            message.length() > 20 ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
                }
            }
        }
    }

    private static final String SCHEME_BITCOIN = "bitcoin:";

    private static Map<String, String> splitQuery(String query) {
        Map<String, String> query_pairs = new LinkedHashMap<>();
        String[] pairs = query.split("&");
        try {
            for (String pair : pairs) {
                int idx = pair.indexOf("=");
                query_pairs.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"),
                        URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return query_pairs;
    }

    private void generateNewAddress() {
        cancelAllRunningTasks();
        if (addressGenerateTask == null) {
            insertingPrivateKeyProgrammatically = true;
            setTextWithoutJumping(privateKeyTextEdit, "");
            insertingPrivateKeyProgrammatically = false;
            insertingAddressProgrammatically = true;
            setTextWithoutJumping(addressTextEdit, getString(R.string.generating));
            insertingAddressProgrammatically = false;
            addressGenerateTask = new AsyncTask<Void, Void, KeyPair>() {
                @Override
                protected KeyPair doInBackground(Void... params) {
                    SharedPreferences preferences = PreferenceManager
                            .getDefaultSharedPreferences(MainActivity.this);
                    String privateKeyType = preferences.getString(PreferencesActivity.PREF_PRIVATE_KEY,
                            PreferencesActivity.PREF_PRIVATE_KEY_WIF_COMPRESSED);
                    if (PreferencesActivity.PREF_PRIVATE_KEY_WIF_COMPRESSED.equals(privateKeyType)) {
                        return BTCUtils.generateWifKey(true);
                    } else if (PreferencesActivity.PREF_PRIVATE_KEY_WIF_NOT_COMPRESSED.equals(privateKeyType)) {
                        return BTCUtils.generateWifKey(false);
                    } else {
                        return BTCUtils.generateMiniKey();
                    }
                }

                @Override
                protected void onPostExecute(final KeyPair key) {
                    addressGenerateTask = null;
                    onNewKeyPairGenerated(key);
                }
            }.execute();
        }
    }

    private void setTextWithoutJumping(EditText editText, String text) {
        int lineCountBefore = editText.getLineCount();
        editText.setText(text);
        if (editText.getLineCount() < lineCountBefore) {
            editText.setMinLines(lineCountBefore);
        }
    }

    private CharSequence getPrivateKeyTypeLabel(final KeyPair keyPair) {
        int typeWithCompression = keyPair.privateKey.type == BTCUtils.PrivateKeyInfo.TYPE_BRAIN_WALLET
                && keyPair.privateKey.isPublicKeyCompressed ? keyPair.privateKey.type + 1 : keyPair.privateKey.type;
        CharSequence keyType = getResources().getTextArray(R.array.private_keys_types)[typeWithCompression];
        SpannableString keyTypeLabel = new SpannableString(getString(R.string.private_key_type, keyType));
        int keyTypeStart = keyTypeLabel.toString().indexOf(keyType.toString());
        keyTypeLabel.setSpan(new StyleSpan(Typeface.BOLD), keyTypeStart, keyTypeStart + keyType.length(),
                SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
        if (keyPair.privateKey.type == BTCUtils.PrivateKeyInfo.TYPE_BRAIN_WALLET) {
            String compressionStrToSpan = keyType.toString().substring(keyType.toString().indexOf(',') + 2);
            int start = keyTypeLabel.toString().indexOf(compressionStrToSpan);
            if (start >= 0) {

                ClickableSpan switchPublicKeyCompressionSpan = new ClickableSpan() {
                    @Override
                    public void onClick(View widget) {
                        cancelAllRunningTasks();
                        switchingCompressionTypeTask = new AsyncTask<Void, Void, KeyPair>() {

                            @Override
                            protected KeyPair doInBackground(Void... params) {
                                return new KeyPair(new BTCUtils.PrivateKeyInfo(keyPair.privateKey.type,
                                        keyPair.privateKey.privateKeyEncoded, keyPair.privateKey.privateKeyDecoded,
                                        !keyPair.privateKey.isPublicKeyCompressed));
                            }

                            @Override
                            protected void onPostExecute(KeyPair keyPair) {
                                switchingCompressionTypeTask = null;
                                onKeyPairModify(false, keyPair);
                            }
                        };
                        switchingCompressionTypeTask.execute();
                    }
                };
                keyTypeLabel.setSpan(switchPublicKeyCompressionSpan, start, start + compressionStrToSpan.length(),
                        SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
            }
        }
        return keyTypeLabel;
    }

    private void showSpendPanelForKeyPair(KeyPair keyPair) {
        if (keyPair != null && keyPair.privateKey.privateKeyDecoded == null) {
            keyPair = null;
        }
        if (keyPair != null && !TextUtils.isEmpty(keyPair.address)) {
            currentKeyPair = keyPair;
            final String address = keyPair.address;
            String descStr = getString(R.string.raw_tx_description_header, address);
            SpannableStringBuilder builder = new SpannableStringBuilder(descStr);
            int spanBegin = descStr.indexOf(address);
            if (spanBegin >= 0) {
                ForegroundColorSpan addressColorSpan = new ForegroundColorSpan(
                        getColor(MainActivity.this, R.color.dark_orange));
                builder.setSpan(addressColorSpan, spanBegin, spanBegin + address.length(),
                        SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
            }
            rawTxDescriptionHeaderView.setText(builder);
            String wutLink = getString(R.string.raw_tx_description_wut_link);
            String jsonLink = getString(R.string.raw_tx_description_json_link);
            builder = new SpannableStringBuilder(getString(R.string.raw_tx_description, wutLink, jsonLink));

            spanBegin = builder.toString().indexOf(wutLink);
            ClickableSpan urlSpan = new ClickableSpan() {
                @Override
                public void onClick(View widget) {
                    SpannableStringBuilder builder = new SpannableStringBuilder(
                            getText(R.string.raw_tx_description_wut));
                    setUrlSpanForAddress("blockexplorer.com", address, builder);
                    setUrlSpanForAddress("blockchain.info", address, builder);
                    TextView messageView = new TextView(MainActivity.this);
                    messageView.setText(builder);
                    messageView.setMovementMethod(LinkMovementMethod.getInstance());
                    int padding = (int) (16 * (getResources().getDisplayMetrics().densityDpi / 160f));
                    messageView.setPadding(padding, padding, padding, padding);
                    new AlertDialog.Builder(MainActivity.this).setView(messageView)
                            .setPositiveButton(android.R.string.ok, null).show();
                }
            };
            builder.setSpan(urlSpan, spanBegin, spanBegin + wutLink.length(),
                    SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);

            spanBegin = builder.toString().indexOf(jsonLink);
            urlSpan = new URLSpan("http://blockchain.info/unspent?active=" + address);
            builder.setSpan(urlSpan, spanBegin, spanBegin + jsonLink.length(),
                    SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);

            rawTxDescriptionView.setText(builder);
            rawTxDescriptionView.setMovementMethod(LinkMovementMethod.getInstance());
            onUnspentOutputsInfoChanged();
        }
        sendLayout.setVisibility(keyPair != null ? View.VISIBLE : View.GONE);
        enterPrivateKeyAck.setVisibility(keyPair == null ? View.VISIBLE : View.GONE);
    }

    private static void setUrlSpanForAddress(String domain, String address, SpannableStringBuilder builder) {
        int spanBegin = builder.toString().indexOf(domain);
        if (spanBegin >= 0) {
            URLSpan urlSpan = new URLSpan("http://" + domain + "/address/" + address);
            builder.setSpan(urlSpan, spanBegin, spanBegin + domain.length(),
                    SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        cancelAllRunningTasks();
    }
}