/*
* Copyright 2011-2014 the original author or authors.
*
* 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 devcoin.wallet.ui;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.content.*;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.database.Cursor;
import android.media.RingtoneManager;
import android.net.Uri;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.preference.PreferenceManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;
import android.widget.*;
import android.widget.CompoundButton.OnCheckedChangeListener;
import com.actionbarsherlock.app.SherlockFragment;
import com.actionbarsherlock.view.ActionMode;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import com.google.devcoin.core.*;
import com.google.devcoin.core.TransactionConfidence.ConfidenceType;
import com.google.devcoin.core.Wallet.BalanceType;
import com.google.devcoin.core.Wallet.SendRequest;
import devcoin.wallet.*;
import devcoin.wallet.ExchangeRatesProvider.ExchangeRate;
import devcoin.wallet.integration.android.BitcoinIntegration;
import devcoin.wallet.offline.SendBluetoothTask;
import devcoin.wallet.ui.InputParser.StringInputParser;
import devcoin.wallet.util.GenericUtils;
import devcoin.wallet.util.WalletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.math.BigInteger;
/**
* @author Andreas Schildbach
*/
public final class SendCoinsFragment extends SherlockFragment
{
private AbstractBindServiceActivity activity;
private WalletApplication application;
private Wallet wallet;
private ContentResolver contentResolver;
private LoaderManager loaderManager;
private SharedPreferences prefs;
@CheckForNull
private BluetoothAdapter bluetoothAdapter;
private int btcPrecision;
private int btcShift;
private final Handler handler = new Handler();
private HandlerThread backgroundThread;
private Handler backgroundHandler;
private AutoCompleteTextView receivingAddressView;
private View receivingStaticView;
private TextView receivingStaticAddressView;
private TextView receivingStaticLabelView;
private CheckBox bluetoothEnableView;
private TextView bluetoothMessageView;
private ListView sentTransactionView;
private TransactionsListAdapter sentTransactionListAdapter;
private Button viewGo;
private Button viewCancel;
private TextView popupMessageView;
private View popupAvailableView;
private PopupWindow popupWindow;
private CurrencyCalculatorLink amountCalculatorLink;
private MenuItem scanAction;
private AddressAndLabel validatedAddress = null;
private boolean isValidAmounts = false;
@CheckForNull
private String bluetoothMac;
private Boolean bluetoothAck = null;
private State state = State.INPUT;
private Transaction sentTransaction = null;
private static final int ID_RATE_LOADER = 0;
private static final int REQUEST_CODE_SCAN = 0;
private static final int REQUEST_CODE_ENABLE_BLUETOOTH = 1;
private static final Logger log = LoggerFactory.getLogger(SendCoinsFragment.class);
private enum State
{
INPUT, PREPARATION, SENDING, SENT, FAILED
}
private final class ReceivingAddressListener implements OnFocusChangeListener, TextWatcher
{
@Override
public void onFocusChange(final View v, final boolean hasFocus)
{
if (!hasFocus)
validateReceivingAddress(true);
}
@Override
public void afterTextChanged(final Editable s)
{
dismissPopup();
validateReceivingAddress(false);
}
@Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after)
{
}
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count)
{
}
}
private final ReceivingAddressListener receivingAddressListener = new ReceivingAddressListener();
private final class ReceivingAddressActionMode implements ActionMode.Callback
{
@Override
public boolean onCreateActionMode(final ActionMode mode, final Menu menu)
{
final MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.send_coins_address_context, menu);
return true;
}
@Override
public boolean onPrepareActionMode(final ActionMode mode, final Menu menu)
{
return false;
}
@Override
public boolean onActionItemClicked(final ActionMode mode, final MenuItem item)
{
switch (item.getItemId())
{
case R.id.send_coins_address_context_edit_address:
handleEditAddress();
mode.finish();
return true;
case R.id.send_coins_address_context_clear:
handleClear();
mode.finish();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(final ActionMode mode)
{
if (receivingStaticView.hasFocus())
amountCalculatorLink.requestFocus();
}
private void handleEditAddress()
{
EditAddressBookEntryFragment.edit(getFragmentManager(), validatedAddress.address.toString());
}
private void handleClear()
{
// switch from static to input
validatedAddress = null;
receivingAddressView.setText(null);
receivingStaticAddressView.setText(null);
updateView();
receivingAddressView.requestFocus();
}
}
private final CurrencyAmountView.Listener amountsListener = new CurrencyAmountView.Listener()
{
@Override
public void changed()
{
dismissPopup();
validateAmounts(false);
}
@Override
public void done()
{
validateAmounts(true);
viewGo.requestFocusFromTouch();
}
@Override
public void focusChanged(final boolean hasFocus)
{
if (!hasFocus)
{
validateAmounts(true);
}
}
};
private final ContentObserver contentObserver = new ContentObserver(handler)
{
@Override
public void onChange(final boolean selfChange)
{
updateView();
}
};
private final TransactionConfidence.Listener sentTransactionConfidenceListener = new TransactionConfidence.Listener()
{
@Override
public void onConfidenceChanged(final Transaction tx, final TransactionConfidence.Listener.ChangeReason reason)
{
activity.runOnUiThread(new Runnable()
{
@Override
public void run()
{
sentTransactionListAdapter.notifyDataSetChanged();
final TransactionConfidence confidence = sentTransaction.getConfidence();
final ConfidenceType confidenceType = confidence.getConfidenceType();
final int numBroadcastPeers = confidence.numBroadcastPeers();
if (state == State.SENDING)
{
if (confidenceType == ConfidenceType.DEAD)
state = State.FAILED;
else if (numBroadcastPeers > 1 || confidenceType == ConfidenceType.BUILDING)
state = State.SENT;
updateView();
}
if (reason == ChangeReason.SEEN_PEERS && confidenceType == ConfidenceType.PENDING)
{
// play sound effect
final int soundResId = getResources().getIdentifier("send_coins_broadcast_" + numBroadcastPeers, "raw",
activity.getPackageName());
if (soundResId > 0)
RingtoneManager.getRingtone(activity, Uri.parse("android.resource://" + activity.getPackageName() + "/" + soundResId))
.play();
}
}
});
}
};
private final LoaderCallbacks<Cursor> rateLoaderCallbacks = new LoaderManager.LoaderCallbacks<Cursor>()
{
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args)
{
return new ExchangeRateLoader(activity);
}
@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor data)
{
if (data != null)
{
data.moveToFirst();
final ExchangeRate exchangeRate = ExchangeRatesProvider.getExchangeRate(data);
if (state == State.INPUT)
amountCalculatorLink.setExchangeRate(exchangeRate);
}
}
@Override
public void onLoaderReset(final Loader<Cursor> loader)
{
}
};
@Override
public void onAttach(final Activity activity)
{
super.onAttach(activity);
this.activity = (AbstractBindServiceActivity) activity;
this.application = (WalletApplication) activity.getApplication();
this.prefs = PreferenceManager.getDefaultSharedPreferences(activity);
this.wallet = application.getWallet();
this.contentResolver = activity.getContentResolver();
this.loaderManager = getLoaderManager();
}
@Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
backgroundThread = new HandlerThread("backgroundThread", Process.THREAD_PRIORITY_BACKGROUND);
backgroundThread.start();
backgroundHandler = new Handler(backgroundThread.getLooper());
final String precision = prefs.getString(Constants.PREFS_KEY_BTC_PRECISION, Constants.PREFS_DEFAULT_BTC_PRECISION);
btcPrecision = precision.charAt(0) - '0';
btcShift = precision.length() == 3 ? precision.charAt(2) - '0' : 0;
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState)
{
final View view = inflater.inflate(R.layout.send_coins_fragment, container);
receivingAddressView = (AutoCompleteTextView) view.findViewById(R.id.send_coins_receiving_address);
receivingAddressView.setAdapter(new AutoCompleteAddressAdapter(activity, null));
receivingAddressView.setOnFocusChangeListener(receivingAddressListener);
receivingAddressView.addTextChangedListener(receivingAddressListener);
receivingStaticView = view.findViewById(R.id.send_coins_receiving_static);
receivingStaticAddressView = (TextView) view.findViewById(R.id.send_coins_receiving_static_address);
receivingStaticLabelView = (TextView) view.findViewById(R.id.send_coins_receiving_static_label);
receivingStaticView.setOnFocusChangeListener(new OnFocusChangeListener()
{
private ActionMode actionMode;
@Override
public void onFocusChange(final View v, final boolean hasFocus)
{
if (hasFocus)
actionMode = activity.startActionMode(new ReceivingAddressActionMode());
else
actionMode.finish();
}
});
final CurrencyAmountView btcAmountView = (CurrencyAmountView) view.findViewById(R.id.send_coins_amount_btc);
btcAmountView.setCurrencySymbol(btcShift == 0 ? Constants.CURRENCY_CODE_BTC : Constants.CURRENCY_CODE_MBTC);
btcAmountView.setInputPrecision(btcShift == 0 ? Constants.BTC_MAX_PRECISION : Constants.MBTC_MAX_PRECISION);
btcAmountView.setHintPrecision(btcPrecision);
btcAmountView.setShift(btcShift);
final CurrencyAmountView localAmountView = (CurrencyAmountView) view.findViewById(R.id.send_coins_amount_local);
localAmountView.setInputPrecision(Constants.LOCAL_PRECISION);
localAmountView.setHintPrecision(Constants.LOCAL_PRECISION);
amountCalculatorLink = new CurrencyCalculatorLink(btcAmountView, localAmountView);
bluetoothEnableView = (CheckBox) view.findViewById(R.id.send_coins_bluetooth_enable);
bluetoothEnableView.setChecked(bluetoothAdapter != null && bluetoothAdapter.isEnabled());
bluetoothEnableView.setOnCheckedChangeListener(new OnCheckedChangeListener()
{
@Override
public void onCheckedChanged(final CompoundButton buttonView, final boolean isChecked)
{
if (isChecked && !bluetoothAdapter.isEnabled())
{
// try to enable bluetooth
startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_CODE_ENABLE_BLUETOOTH);
}
}
});
bluetoothMessageView = (TextView) view.findViewById(R.id.send_coins_bluetooth_message);
sentTransactionView = (ListView) view.findViewById(R.id.send_coins_sent_transaction);
sentTransactionListAdapter = new TransactionsListAdapter(activity, wallet, application.maxConnectedPeers(), false);
sentTransactionView.setAdapter(sentTransactionListAdapter);
viewGo = (Button) view.findViewById(R.id.send_coins_go);
viewGo.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(final View v)
{
validateReceivingAddress(true);
validateAmounts(true);
if (everythingValid())
handleGo();
}
});
viewCancel = (Button) view.findViewById(R.id.send_coins_cancel);
viewCancel.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(final View v)
{
if (state == State.INPUT)
activity.setResult(Activity.RESULT_CANCELED);
activity.finish();
}
});
popupMessageView = (TextView) inflater.inflate(R.layout.send_coins_popup_message, container);
popupAvailableView = inflater.inflate(R.layout.send_coins_popup_available, container);
if (savedInstanceState != null)
{
restoreInstanceState(savedInstanceState);
}
else
{
final Intent intent = activity.getIntent();
final String action = intent.getAction();
final Uri intentUri = intent.getData();
final String scheme = intentUri != null ? intentUri.getScheme() : null;
if ((Intent.ACTION_VIEW.equals(action) || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) && intentUri != null
&& "devcoin".equals(scheme))
initStateFromBitcoinUri(intentUri);
else if (intent.hasExtra(SendCoinsActivity.INTENT_EXTRA_ADDRESS))
initStateFromIntentExtras(intent.getExtras());
}
return view;
}
private void initStateFromIntentExtras(@Nonnull final Bundle extras)
{
final String address = extras.getString(SendCoinsActivity.INTENT_EXTRA_ADDRESS);
final String addressLabel = extras.getString(SendCoinsActivity.INTENT_EXTRA_ADDRESS_LABEL);
final BigInteger amount = (BigInteger) extras.getSerializable(SendCoinsActivity.INTENT_EXTRA_AMOUNT);
final String bluetoothMac = extras.getString(SendCoinsActivity.INTENT_EXTRA_BLUETOOTH_MAC);
update(address, addressLabel, amount, bluetoothMac);
}
private void initStateFromBitcoinUri(@Nonnull final Uri bitcoinUri)
{
final String input = bitcoinUri.toString();
new StringInputParser(input)
{
@Override
protected void bitcoinRequest(final Address address, final String addressLabel, final BigInteger amount, final String bluetoothMac)
{
update(address.toString(), addressLabel, amount, bluetoothMac);
}
@Override
protected void directTransaction(final Transaction transaction)
{
cannotClassify(input);
}
@Override
protected void error(final int messageResId, final Object... messageArgs)
{
dialog(activity, dismissListener, 0, messageResId, messageArgs);
}
private final DialogInterface.OnClickListener dismissListener = new DialogInterface.OnClickListener()
{
@Override
public void onClick(final DialogInterface dialog, final int which)
{
activity.finish();
}
};
}.parse();
}
@Override
public void onResume()
{
super.onResume();
contentResolver.registerContentObserver(AddressBookProvider.contentUri(activity.getPackageName()), true, contentObserver);
amountCalculatorLink.setListener(amountsListener);
loaderManager.initLoader(ID_RATE_LOADER, null, rateLoaderCallbacks);
updateView();
}
@Override
public void onPause()
{
loaderManager.destroyLoader(ID_RATE_LOADER);
amountCalculatorLink.setListener(null);
contentResolver.unregisterContentObserver(contentObserver);
super.onPause();
}
@Override
public void onDetach()
{
handler.removeCallbacksAndMessages(null);
super.onDetach();
}
@Override
public void onDestroy()
{
backgroundThread.getLooper().quit();
if (sentTransaction != null)
sentTransaction.getConfidence().removeEventListener(sentTransactionConfidenceListener);
super.onDestroy();
}
@Override
public void onSaveInstanceState(final Bundle outState)
{
super.onSaveInstanceState(outState);
saveInstanceState(outState);
}
private void saveInstanceState(final Bundle outState)
{
outState.putSerializable("state", state);
if (validatedAddress != null)
outState.putParcelable("validated_address", validatedAddress);
outState.putBoolean("is_valid_amounts", isValidAmounts);
if (sentTransaction != null)
outState.putSerializable("sent_transaction_hash", sentTransaction.getHash());
outState.putString("bluetooth_mac", bluetoothMac);
if (bluetoothAck != null)
outState.putBoolean("bluetooth_ack", bluetoothAck);
}
private void restoreInstanceState(final Bundle savedInstanceState)
{
state = (State) savedInstanceState.getSerializable("state");
validatedAddress = savedInstanceState.getParcelable("validated_address");
isValidAmounts = savedInstanceState.getBoolean("is_valid_amounts");
if (savedInstanceState.containsKey("sent_transaction_hash"))
{
sentTransaction = wallet.getTransaction((Sha256Hash) savedInstanceState.getSerializable("sent_transaction_hash"));
sentTransaction.getConfidence().addEventListener(sentTransactionConfidenceListener);
}
bluetoothMac = savedInstanceState.getString("bluetooth_mac");
if (savedInstanceState.containsKey("bluetooth_ack"))
bluetoothAck = savedInstanceState.getBoolean("bluetooth_ack");
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent intent)
{
if (requestCode == REQUEST_CODE_SCAN)
{
if (resultCode == Activity.RESULT_OK)
{
final String input = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT);
new StringInputParser(input)
{
@Override
protected void bitcoinRequest(final Address address, final String addressLabel, final BigInteger amount, final String bluetoothMac)
{
SendCoinsActivity.start(activity, address != null ? address.toString() : null, addressLabel, amount, bluetoothMac);
}
@Override
protected void directTransaction(final Transaction transaction)
{
cannotClassify(input);
}
@Override
protected void error(final int messageResId, final Object... messageArgs)
{
dialog(activity, null, R.string.button_scan, messageResId, messageArgs);
}
}.parse();
}
}
else if (requestCode == REQUEST_CODE_ENABLE_BLUETOOTH)
{
bluetoothEnableView.setChecked(resultCode == Activity.RESULT_OK);
}
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater)
{
inflater.inflate(R.menu.send_coins_fragment_options, menu);
scanAction = menu.findItem(R.id.send_coins_options_scan);
final PackageManager pm = activity.getPackageManager();
scanAction.setVisible(pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) || pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT));
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
switch (item.getItemId())
{
case R.id.send_coins_options_scan:
handleScan();
return true;
case R.id.send_coins_options_empty:
handleEmpty();
return true;
}
return super.onOptionsItemSelected(item);
}
private void validateReceivingAddress(final boolean popups)
{
try
{
final String addressStr = receivingAddressView.getText().toString().trim();
if (!addressStr.isEmpty())
{
final NetworkParameters addressParams = Address.getParametersFromAddress(addressStr);
if (addressParams != null && !addressParams.equals(Constants.NETWORK_PARAMETERS))
{
// address is valid, but from different known network
if (popups)
popupMessage(receivingAddressView,
getString(R.string.send_coins_fragment_receiving_address_error_cross_network, addressParams.getId()));
}
else if (addressParams == null)
{
// address is valid, but from different unknown network
if (popups)
popupMessage(receivingAddressView, getString(R.string.send_coins_fragment_receiving_address_error_cross_network_unknown));
}
else
{
// valid address
final String label = AddressBookProvider.resolveLabel(activity, addressStr);
validatedAddress = new AddressAndLabel(Constants.NETWORK_PARAMETERS, addressStr, label);
receivingAddressView.setText(null);
}
}
else
{
// empty field should not raise error message
}
}
catch (final AddressFormatException x)
{
// could not decode address at all
if (popups)
popupMessage(receivingAddressView, getString(R.string.send_coins_fragment_receiving_address_error));
}
updateView();
}
private void validateAmounts(final boolean popups)
{
isValidAmounts = false;
final BigInteger amount = amountCalculatorLink.getAmount();
if (amount == null)
{
// empty amount
if (popups)
popupMessage(amountCalculatorLink.activeView(), getString(R.string.send_coins_fragment_amount_empty));
}
else if (amount.signum() > 0)
{
final BigInteger estimated = wallet.getBalance(BalanceType.ESTIMATED);
final BigInteger available = wallet.getBalance(BalanceType.AVAILABLE);
final BigInteger pending = estimated.subtract(available);
// TODO subscribe to wallet changes
final BigInteger availableAfterAmount = available.subtract(amount);
final boolean enoughFundsForAmount = availableAfterAmount.signum() >= 0;
if (enoughFundsForAmount)
{
// everything fine
isValidAmounts = true;
}
else
{
// not enough funds for amount
if (popups)
popupAvailable(amountCalculatorLink.activeView(), available, pending);
}
}
else
{
// invalid amount
if (popups)
popupMessage(amountCalculatorLink.activeView(), getString(R.string.send_coins_fragment_amount_error));
}
updateView();
}
private void popupMessage(@Nonnull final View anchor, @Nonnull final String message)
{
dismissPopup();
popupMessageView.setText(message);
popupMessageView.setMaxWidth(getView().getWidth());
popup(anchor, popupMessageView);
}
private void popupAvailable(@Nonnull final View anchor, @Nonnull final BigInteger available, @Nonnull final BigInteger pending)
{
dismissPopup();
final CurrencyTextView viewAvailable = (CurrencyTextView) popupAvailableView.findViewById(R.id.send_coins_popup_available_amount);
viewAvailable.setPrefix(btcShift == 0 ? Constants.CURRENCY_CODE_BTC : Constants.CURRENCY_CODE_MBTC);
viewAvailable.setPrecision(btcPrecision, btcShift);
viewAvailable.setAmount(available);
final TextView viewPending = (TextView) popupAvailableView.findViewById(R.id.send_coins_popup_available_pending);
viewPending.setVisibility(pending.signum() > 0 ? View.VISIBLE : View.GONE);
final int precision = btcShift == 0 ? Constants.BTC_MAX_PRECISION : Constants.MBTC_MAX_PRECISION;
viewPending.setText(getString(R.string.send_coins_fragment_pending, GenericUtils.formatValue(pending, precision, btcShift)));
popup(anchor, popupAvailableView);
}
private void popup(@Nonnull final View anchor, @Nonnull final View contentView)
{
contentView.measure(MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0), MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0));
popupWindow = new PopupWindow(contentView, contentView.getMeasuredWidth(), contentView.getMeasuredHeight(), false);
popupWindow.showAsDropDown(anchor);
// hack
contentView.setBackgroundResource(popupWindow.isAboveAnchor() ? R.drawable.popup_frame_above : R.drawable.popup_frame_below);
}
private void dismissPopup()
{
if (popupWindow != null)
{
popupWindow.dismiss();
popupWindow = null;
}
}
private void handleGo()
{
state = State.PREPARATION;
updateView();
// create spend
final BigInteger amount = amountCalculatorLink.getAmount();
final SendRequest sendRequest = SendRequest.to(validatedAddress.address, amount);
sendRequest.changeAddress = WalletUtils.pickOldestKey(wallet).toAddress(Constants.NETWORK_PARAMETERS);
sendRequest.emptyWallet = amount.equals(wallet.getBalance(BalanceType.AVAILABLE));
new SendCoinsOfflineTask(wallet, backgroundHandler)
{
@Override
protected void onSuccess(final Transaction transaction)
{
sentTransaction = transaction;
state = State.SENDING;
updateView();
sentTransaction.getConfidence().addEventListener(sentTransactionConfidenceListener);
if (bluetoothAdapter != null && bluetoothAdapter.isEnabled() && bluetoothMac != null && bluetoothEnableView.isChecked())
{
new SendBluetoothTask(bluetoothAdapter, backgroundHandler)
{
@Override
protected void onResult(final boolean ack)
{
bluetoothAck = ack;
if (state == State.SENDING)
state = State.SENT;
updateView();
}
}.send(bluetoothMac, transaction); // send asynchronously
}
application.broadcastTransaction(sentTransaction);
final Intent result = new Intent();
BitcoinIntegration.transactionHashToResult(result, sentTransaction.getHashAsString());
activity.setResult(Activity.RESULT_OK, result);
}
@Override
protected void onFailure()
{
state = State.FAILED;
updateView();
activity.longToast(R.string.send_coins_error_msg);
}
}.sendCoinsOffline(sendRequest); // send asynchronously
}
private void handleScan()
{
startActivityForResult(new Intent(activity, ScanActivity.class), REQUEST_CODE_SCAN);
}
private void handleEmpty()
{
final BigInteger available = wallet.getBalance(BalanceType.AVAILABLE);
amountCalculatorLink.setBtcAmount(available);
}
public class AutoCompleteAddressAdapter extends CursorAdapter
{
public AutoCompleteAddressAdapter(final Context context, final Cursor c)
{
super(context, c);
}
@Override
public View newView(final Context context, final Cursor cursor, final ViewGroup parent)
{
final LayoutInflater inflater = LayoutInflater.from(context);
return inflater.inflate(R.layout.address_book_row, parent, false);
}
@Override
public void bindView(final View view, final Context context, final Cursor cursor)
{
final String label = cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_LABEL));
final String address = cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_ADDRESS));
final ViewGroup viewGroup = (ViewGroup) view;
final TextView labelView = (TextView) viewGroup.findViewById(R.id.address_book_row_label);
labelView.setText(label);
final TextView addressView = (TextView) viewGroup.findViewById(R.id.address_book_row_address);
addressView.setText(WalletUtils.formatHash(address, Constants.ADDRESS_FORMAT_GROUP_SIZE, Constants.ADDRESS_FORMAT_LINE_SIZE));
}
@Override
public CharSequence convertToString(final Cursor cursor)
{
return cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_ADDRESS));
}
@Override
public Cursor runQueryOnBackgroundThread(final CharSequence constraint)
{
final Cursor cursor = activity.managedQuery(AddressBookProvider.contentUri(activity.getPackageName()), null,
AddressBookProvider.SELECTION_QUERY, new String[] { constraint.toString() }, null);
return cursor;
}
}
private void updateView()
{
if (validatedAddress != null)
{
receivingAddressView.setVisibility(View.GONE);
receivingStaticView.setVisibility(View.VISIBLE);
receivingStaticAddressView.setText(WalletUtils.formatAddress(validatedAddress.address, Constants.ADDRESS_FORMAT_GROUP_SIZE,
Constants.ADDRESS_FORMAT_LINE_SIZE));
final String addressBookLabel = AddressBookProvider.resolveLabel(activity, validatedAddress.address.toString());
final String staticLabel;
if (addressBookLabel != null)
staticLabel = addressBookLabel;
else if (validatedAddress.label != null)
staticLabel = validatedAddress.label;
else
staticLabel = getString(R.string.address_unlabeled);
receivingStaticLabelView.setText(staticLabel);
receivingStaticLabelView.setTextColor(getResources().getColor(
validatedAddress.label != null ? R.color.fg_significant : R.color.fg_insignificant));
}
else
{
receivingStaticView.setVisibility(View.GONE);
receivingAddressView.setVisibility(View.VISIBLE);
}
receivingAddressView.setEnabled(state == State.INPUT);
receivingStaticView.setEnabled(state == State.INPUT);
amountCalculatorLink.setEnabled(state == State.INPUT);
bluetoothEnableView.setVisibility(bluetoothAdapter != null && bluetoothMac != null ? View.VISIBLE : View.GONE);
bluetoothEnableView.setEnabled(state == State.INPUT);
if (sentTransaction != null)
{
final String precision = prefs.getString(Constants.PREFS_KEY_BTC_PRECISION, Constants.PREFS_DEFAULT_BTC_PRECISION);
final int btcPrecision = precision.charAt(0) - '0';
final int btcShift = precision.length() == 3 ? precision.charAt(2) - '0' : 0;
sentTransactionView.setVisibility(View.VISIBLE);
sentTransactionListAdapter.setPrecision(btcPrecision, btcShift);
sentTransactionListAdapter.replace(sentTransaction);
}
else
{
sentTransactionView.setVisibility(View.GONE);
sentTransactionListAdapter.clear();
}
if (bluetoothAck != null)
{
bluetoothMessageView.setVisibility(View.VISIBLE);
bluetoothMessageView.setText(bluetoothAck ? R.string.send_coins_fragment_bluetooth_ack : R.string.send_coins_fragment_bluetooth_nack);
}
else
{
bluetoothMessageView.setVisibility(View.GONE);
}
viewCancel.setEnabled(state != State.PREPARATION);
viewGo.setEnabled(everythingValid());
if (state == State.INPUT)
{
viewCancel.setText(R.string.button_cancel);
viewGo.setText(R.string.send_coins_fragment_button_send);
}
else if (state == State.PREPARATION)
{
viewCancel.setText(R.string.button_cancel);
viewGo.setText(R.string.send_coins_preparation_msg);
}
else if (state == State.SENDING)
{
viewCancel.setText(R.string.send_coins_fragment_button_back);
viewGo.setText(R.string.send_coins_sending_msg);
}
else if (state == State.SENT)
{
viewCancel.setText(R.string.send_coins_fragment_button_back);
viewGo.setText(R.string.send_coins_sent_msg);
}
else if (state == State.FAILED)
{
viewCancel.setText(R.string.send_coins_fragment_button_back);
viewGo.setText(R.string.send_coins_failed_msg);
}
if (scanAction != null)
scanAction.setEnabled(state == State.INPUT);
}
private boolean everythingValid()
{
return state == State.INPUT && validatedAddress != null && isValidAmounts;
}
public void update(final String receivingAddress, final String receivingLabel, @Nullable final BigInteger amount,
@Nullable final String bluetoothMac)
{
try
{
validatedAddress = new AddressAndLabel(Constants.NETWORK_PARAMETERS, receivingAddress, receivingLabel);
receivingAddressView.setText(null);
}
catch (final Exception x)
{
receivingAddressView.setText(receivingAddress);
validatedAddress = null;
log.info("problem parsing address: '" + receivingAddress + "'", x);
}
if (amount != null)
amountCalculatorLink.setBtcAmount(amount);
// focus
if (receivingAddress != null && amount == null)
amountCalculatorLink.requestFocus();
else if (receivingAddress != null && amount != null)
viewGo.requestFocus();
this.bluetoothMac = bluetoothMac;
bluetoothAck = null;
updateView();
handler.postDelayed(new Runnable()
{
@Override
public void run()
{
validateReceivingAddress(true);
validateAmounts(true);
}
}, 500);
}
}