com.bonsai.btcreceive.ReceiveFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.bonsai.btcreceive.ReceiveFragment.java

Source

// Copyright (C) 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.btcreceive;

import java.math.BigInteger;
import java.util.Hashtable;

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

import com.google.bitcoin.core.Address;
import com.google.bitcoin.uri.BitcoinURI;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.content.LocalBroadcastManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;

public class ReceiveFragment extends Fragment {

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

    protected LocalBroadcastManager mLBM;

    private BaseWalletActivity mBase;

    private final static QRCodeWriter sQRCodeWriter = new QRCodeWriter();

    protected EditText mBTCAmountEditText = null;
    protected EditText mFiatAmountEditText = null;
    protected boolean mUserSetAmountFiat;

    protected boolean mValueSet = false;

    // In order to automatically transition to the transaction view
    // when the address receives funds we need to watch the address
    // and remember if we've already transitioned.
    protected HDAddress mHDAddress = null;
    protected boolean mTransitioned = false;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        mLogger.info("ReceiveFragment onCreate");
        super.onCreate(savedInstanceState);
        mLBM = LocalBroadcastManager.getInstance(getActivity());
        mBase = (BaseWalletActivity) getActivity();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        mLogger.info("ReceiveFragment onActivityCreated");
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mLogger.info("ReceiveFragment onCreateView");
        View view = inflater.inflate(R.layout.receive_fragment, container, false);

        // Start off presuming the user set the Fiat amount.
        mUserSetAmountFiat = true;

        return view;
    }

    @Override
    public void onResume() {
        mLogger.info("ReceiveFragment onResume");
        super.onResume();

        mLBM.registerReceiver(mWalletStateChangedReceiver, new IntentFilter("wallet-state-changed"));
        mLBM.registerReceiver(mRateChangedReceiver, new IntentFilter("rate-changed"));

        mBTCAmountEditText = (EditText) getActivity().findViewById(R.id.receive_btc_amount);
        mFiatAmountEditText = (EditText) getActivity().findViewById(R.id.receive_fiat_amount);

        mBTCAmountEditText.addTextChangedListener(mBTCAmountWatcher);
        mFiatAmountEditText.addTextChangedListener(mFiatAmountWatcher);

        mBTCAmountEditText.setOnEditorActionListener(checkForDone);
        mFiatAmountEditText.setOnEditorActionListener(checkForDone);

        mBTCAmountEditText.setOnTouchListener(touchListener);
        mFiatAmountEditText.setOnTouchListener(touchListener);

        if (mValueSet)
            showAddress();
        else
            hideAddress();

        BTCFmt btcfmt = mBase.getBTCFmt();

        // Set these each time we resume in case we've visited the
        // Settings and they've changed.
        {
            TextView tv = (TextView) getActivity().findViewById(R.id.receive_btc_label);
            tv.setText(btcfmt.unitStr());
        }
    }

    @Override
    public void onPause() {
        mLogger.info("ReceiveFragment onPause");
        mLBM.unregisterReceiver(mWalletStateChangedReceiver);
        mLBM.unregisterReceiver(mRateChangedReceiver);
        super.onPause();
    }

    private BroadcastReceiver mWalletStateChangedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // Transition once to the transactions view if the
            // address in question receives a transaction.

            // Do we currently have an address to watch?
            if (mHDAddress == null)
                return;

            // Have we already fired?
            if (mTransitioned)
                return;

            // Have there been any transactions?
            if (mHDAddress.numTrans() == 0)
                return;

            mTransitioned = true;

            // Take down the address.
            hideAddress();
            mValueSet = false;
            mBTCAmountEditText.setText("");
            mFiatAmountEditText.setText("");
            maybeShowKeyboard();

            // Transition to the transactions list.
            MainActivity main = (MainActivity) getActivity();
            main.setPagerItem(1);
        }
    };

    private BroadcastReceiver mRateChangedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // Only update the rate while we haven't finalized a
            // value.  Once the user has picked a value we should
            // freeze the rate updates.
            //
            if (!mValueSet)
                updateAmountFields();
        }
    };

    public void maybeShowKeyboard() {
        // Called by our parent when it would be good for us to
        // bring up the keyboard.

        mLogger.info("maybeShowKeyboard starting");

        // Does this ever happen?
        if (mFiatAmountEditText == null || mBTCAmountEditText == null)
            return;

        // If the user has the value set already we don't want the
        // keyboard.
        if (mValueSet)
            return;

        mFiatAmountEditText.setFocusable(true);
        mFiatAmountEditText.setFocusableInTouchMode(true);
        mBTCAmountEditText.setFocusable(true);
        mBTCAmountEditText.setFocusableInTouchMode(true);

        Activity activity = getActivity();
        InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);

        if (mUserSetAmountFiat) {
            mLogger.info("maybeShowKeyboard fiat");
            imm.showSoftInput(mFiatAmountEditText, InputMethodManager.SHOW_IMPLICIT);
            mFiatAmountEditText.requestFocus();
        } else {
            mLogger.info("maybeShowKeyboard btc");
            imm.showSoftInput(mBTCAmountEditText, InputMethodManager.SHOW_IMPLICIT);
            mBTCAmountEditText.requestFocus();
        }
    }

    public void hideKeyboard() {
        MainActivity main = (MainActivity) getActivity();
        main.hideKeyboard();
    }

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

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

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

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

    };

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

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

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

    protected void updateAmountFields() {
        BTCFmt btcfmt = mBase.getBTCFmt();
        double fiatPerBTC = mBase.fiatPerBTC();

        // Which field did the user last edit?
        if (mUserSetAmountFiat) {
            // The user set the Fiat amount.
            String ss = mFiatAmountEditText.getText().toString();

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

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

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

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

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

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

    private View.OnTouchListener touchListener = new View.OnTouchListener() {
        public boolean onTouch(View vv, MotionEvent event) {
            if (mValueSet) {
                hideAddress(); // User wants to change the amount.
                mValueSet = false;
                maybeShowKeyboard();
                updateAmountFields(); // They could be stale.
            }
            return false;
        }
    };

    private TextView.OnEditorActionListener checkForDone = new TextView.OnEditorActionListener() {
        public boolean onEditorAction(TextView view, int actionId, KeyEvent event) {
            mLogger.info("onEditorAction");
            mValueSet = true;
            hideKeyboard();
            showAddress();
            return false; // so it will take down the keyboard.
        }
    };

    public void showAddress() {
        BTCFmt btcfmt = mBase.getBTCFmt();

        Address addr = mBase.getWalletService().nextReceiveAddress();
        String addrstr = addr.toString();

        TextView addrtv = (TextView) getActivity().findViewById(R.id.receive_addr);
        addrtv.setText(addrstr);
        addrtv.setVisibility(View.VISIBLE);

        String ss = mBTCAmountEditText.getText().toString();
        long bb = btcfmt.parse(ss.toString());
        BigInteger amt = bb == 0 ? null : BigInteger.valueOf(bb);

        String uri = BitcoinURI.convertToBitcoinURI(addrstr, amt, null, null);

        mLogger.info("view address uri=" + uri);

        final int size = (int) (240 * getResources().getDisplayMetrics().density);

        // Load the QR bitmap.
        Bitmap bm = createBitmap(uri, size);
        if (bm != null) {
            ImageView iv = (ImageView) getActivity().findViewById(R.id.receive_qr_view);
            iv.setImageBitmap(bm);
            iv.setVisibility(View.VISIBLE);
        }

        // Find the HDAddress object associated with this address.
        HDAddressDescription addrdesc = mBase.getWalletService().findAddress(addr);
        mHDAddress = addrdesc.hdAddress;
        mTransitioned = false;

        mFiatAmountEditText.setFocusable(false);
        mFiatAmountEditText.setFocusableInTouchMode(false);
        mBTCAmountEditText.setFocusable(false);
        mBTCAmountEditText.setFocusableInTouchMode(false);
    }

    public void hideAddress() {
        TextView addrtv = (TextView) getActivity().findViewById(R.id.receive_addr);
        addrtv.setVisibility(View.GONE);

        ImageView iv = (ImageView) getActivity().findViewById(R.id.receive_qr_view);
        iv.setVisibility(View.GONE);

        // Clear the HDAddress associated with this address.
        mHDAddress = null;
        mTransitioned = false;

        mFiatAmountEditText.setFocusable(true);
        mFiatAmountEditText.setFocusableInTouchMode(true);
        mBTCAmountEditText.setFocusable(true);
        mBTCAmountEditText.setFocusableInTouchMode(true);
    }

    private Bitmap createBitmap(String content, final int size) {
        final Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
        hints.put(EncodeHintType.MARGIN, 0);
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        BitMatrix result;
        try {
            result = sQRCodeWriter.encode(content, BarcodeFormat.QR_CODE, size, size, hints);
        } catch (WriterException ex) {
            mLogger.warn("qr encoder failed: " + ex.toString());
            return null;
        }

        final int width = result.getWidth();
        final int height = result.getHeight();
        final int[] pixels = new int[width * height];

        for (int y = 0; y < height; y++) {
            final int offset = y * width;
            for (int x = 0; x < width; x++) {
                pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
            }
        }

        final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        bitmap.setPixels(pixels, 0, width, 0, 0, width, height);

        return bitmap;
    }

    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(',', '.'));
    }
}