package com.mygeopay.wallet.ui; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ContentResolver; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.mygeopay.core.coins.CoinType; import com.mygeopay.core.exchange.shapeshift.ShapeShift; import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftEmail; import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftException; import com.mygeopay.core.exchange.shapeshift.data.ShapeShiftTxStatus; import com.mygeopay.core.wallet.WalletAccount; import com.mygeopay.wallet.Constants; import com.mygeopay.wallet.ExchangeHistoryProvider; import com.mygeopay.wallet.ExchangeHistoryProvider.ExchangeEntry; import com.mygeopay.wallet.R; import com.mygeopay.wallet.WalletApplication; import com.mygeopay.wallet.util.Fonts; import com.mygeopay.wallet.util.WeakHandler; import org.bitcoinj.core.Address; import org.bitcoinj.core.Transaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; import java.util.Timer; import java.util.TimerTask; import static com.mygeopay.core.Preconditions.checkNotNull; import static com.mygeopay.wallet.util.UiUtils.setGone; import static com.mygeopay.wallet.util.UiUtils.setInvisible; import static com.mygeopay.wallet.util.UiUtils.setVisible; /** * @author John L. Jegutanis */ public class TradeStatusFragment extends Fragment { private static final Logger log = LoggerFactory.getLogger(TradeStatusFragment.class); private static final int UPDATE_SHAPESHIFT_STATUS = 0; private static final int UPDATE_STATUS = 1; private static final int ERROR_MESSAGE = 2; private static final int ID_STATUS_LOADER = 0; private static final long POLLING_MS = 3000; private static final String ARG_SHOW_EXIT_BUTTON = "show_exit_button"; private Listener mListener; private ContentResolver contentResolver; private LoaderManager loaderManager; private TextView exchangeInfo; private TextView depositIcon; private ProgressBar depositProgress; private TextView depositText; private TextView exchangeIcon; private ProgressBar exchangeProgress; private TextView exchangeText; private TextView errorIcon; private TextView errorText; private Button viewTransaction; private Button emailReceipt; private MenuItem actionDeleteMenu; private boolean showExitButton; private Uri statusUri; private ExchangeEntry exchangeStatus; private StatusPollTask pollTask; private final Handler handler = new MyHandler(this); private Timer timer; private WalletApplication application; private static class StatusPollTask extends TimerTask { private final ShapeShift shapeShift; private final Address depositAddress; private final Handler handler; private StatusPollTask(ShapeShift shapeShift, Address depositAddress, Handler handler) { this.shapeShift = shapeShift; this.depositAddress = depositAddress; this.handler = handler; } @Override public void run() { for (int tries = 3; tries > 0; tries--) { try { log.info("Polling status for deposit: {}", depositAddress); ShapeShiftTxStatus newStatus = shapeShift.getTxStatus(depositAddress); handler.sendMessage(handler.obtainMessage(UPDATE_SHAPESHIFT_STATUS, newStatus)); break; } catch (ShapeShiftException e) { log.warn("Error occurred while polling", e); handler.sendMessage(handler.obtainMessage(ERROR_MESSAGE, e.getMessage())); break; } catch (IOException e) { /* ignore and retry */ } } } } private static class MyHandler extends WeakHandler<TradeStatusFragment> { public MyHandler(TradeStatusFragment ref) { super(ref); } @Override protected void weakHandleMessage(TradeStatusFragment ref, Message msg) { switch (msg.what) { case UPDATE_SHAPESHIFT_STATUS: ref.updateShapeShiftStatus((ShapeShiftTxStatus) msg.obj); break; case UPDATE_STATUS: ref.updateStatus((ExchangeEntry) msg.obj); break; case ERROR_MESSAGE: ref.errorIcon.setVisibility(View.VISIBLE); ref.errorText.setVisibility(View.VISIBLE); ref.errorText.setText(ref.getString(R.string.trade_status_failed_detail, msg.obj)); ref.stopPolling(); break; } } } private void updateStatus(ExchangeEntry newStatus) { exchangeStatus = checkNotNull(newStatus); updateView(); } private void updateShapeShiftStatus(ShapeShiftTxStatus shapeShiftTxStatus) { ExchangeEntry newStatus = new ExchangeEntry(exchangeStatus, shapeShiftTxStatus); // If updated status, save it if (exchangeStatus.status != newStatus.status) { contentResolver.update(statusUri, newStatus.getContentValues(), null, null); } } public static TradeStatusFragment newInstance(ExchangeEntry exchangeEntry) { return newInstance(exchangeEntry, false); } public static TradeStatusFragment newInstance(ExchangeEntry exchangeEntry, boolean showExitButton) { TradeStatusFragment fragment = new TradeStatusFragment(); Bundle args = new Bundle(); args.putSerializable(Constants.ARG_EXCHANGE_ENTRY, exchangeEntry); args.putBoolean(ARG_SHOW_EXIT_BUTTON, showExitButton); fragment.setArguments(args); return fragment; } public TradeStatusFragment() {} @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Bundle args = getArguments(); showExitButton = args.getBoolean(ARG_SHOW_EXIT_BUTTON, false); exchangeStatus = (ExchangeEntry) args.getSerializable(Constants.ARG_EXCHANGE_ENTRY); Address deposit = exchangeStatus.depositAddress; statusUri = ExchangeHistoryProvider.contentUri(application.getPackageName(), deposit); loaderManager.initLoader(ID_STATUS_LOADER, null, statusLoaderCallbacks); setHasOptionsMenu(true); } @Override public void onDestroy() { loaderManager.destroyLoader(ID_STATUS_LOADER); super.onDestroy(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_trade_status, container, false); exchangeInfo = (TextView) view.findViewById(R.id.exchange_status_info); depositIcon = (TextView) view.findViewById(R.id.trade_deposit_status_icon); depositProgress = (ProgressBar) view.findViewById(R.id.trade_deposit_status_progress); depositText = (TextView) view.findViewById(R.id.trade_deposit_status_text); exchangeIcon = (TextView) view.findViewById(R.id.trade_exchange_status_icon); exchangeProgress = (ProgressBar) view.findViewById(R.id.trade_exchange_status_progress); exchangeText = (TextView) view.findViewById(R.id.trade_exchange_status_text); errorIcon = (TextView) view.findViewById(R.id.trade_error_status_icon); errorText = (TextView) view.findViewById(R.id.trade_error_status_text); Fonts.setTypeface(depositIcon, Fonts.Font.COINOMI_FONT_ICONS); Fonts.setTypeface(exchangeIcon, Fonts.Font.COINOMI_FONT_ICONS); Fonts.setTypeface(errorIcon, Fonts.Font.COINOMI_FONT_ICONS); viewTransaction = (Button) view.findViewById(R.id.trade_view_transaction); emailReceipt = (Button) view.findViewById(R.id.trade_email_receipt); if (showExitButton) { view.findViewById(R.id.button_exit).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onExitPressed(); } }); } else { view.findViewById(R.id.button_exit).setVisibility(View.GONE); } updateView(); view.findViewById(R.id.powered_by_shapeshift).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new AlertDialog.Builder(getActivity()) .setTitle(R.string.about_shapeshift_title) .setMessage(R.string.about_shapeshift_message) .setPositiveButton(R.string.button_ok, null) .create().show(); } }); return view; } @Override public void onPause() { super.onPause(); stopPolling(); } @Override public void onResume() { super.onResume(); startPolling(); } private void startPolling() { if (timer == null && exchangeStatus.status != ExchangeEntry.STATUS_COMPLETE) { ShapeShift shapeShift = application.getShapeShift(); pollTask = new StatusPollTask(shapeShift, exchangeStatus.depositAddress, handler); timer = new Timer(); timer.schedule(pollTask, 0, POLLING_MS); } } private void stopPolling() { if (timer != null) { timer.cancel(); timer.purge(); timer = null; pollTask.cancel(); pollTask = null; } } public void onExitPressed() { stopPolling(); if (mListener != null) { mListener.onFinish(); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.exchange_status, menu); actionDeleteMenu = menu.findItem(R.id.action_delete); updateView(); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_delete: new AlertDialog.Builder(getActivity()) .setMessage(R.string.trade_status_delete_message) .setNegativeButton(R.string.button_cancel, null) .setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { contentResolver.delete(statusUri, null, null); onExitPressed(); } }) .create().show(); return true; default: return super.onOptionsItemSelected(item); } } private void updateView() { switch (exchangeStatus.status) { case ExchangeEntry.STATUS_INITIAL: if (actionDeleteMenu != null) actionDeleteMenu.setVisible(false); exchangeInfo.setText(R.string.trade_status_message); setGone(depositIcon); setVisible(depositProgress); setVisible(depositText); depositText.setText(R.string.trade_status_waiting_deposit); setInvisible(exchangeIcon); setGone(exchangeProgress); setInvisible(exchangeText); setGone(errorIcon); setGone(errorText); setInvisible(viewTransaction); setInvisible(emailReceipt); break; case ExchangeEntry.STATUS_PROCESSING: if (actionDeleteMenu != null) actionDeleteMenu.setVisible(false); exchangeInfo.setText(R.string.trade_status_message); setVisible(depositIcon); setGone(depositProgress); setVisible(depositText); depositText.setText(getString(R.string.trade_status_received_deposit, exchangeStatus.depositAmount)); setGone(exchangeIcon); setVisible(exchangeProgress); setVisible(exchangeText); exchangeText.setText(R.string.trade_status_waiting_trade); setGone(errorIcon); setGone(errorText); setInvisible(viewTransaction); setInvisible(emailReceipt); break; case ExchangeEntry.STATUS_COMPLETE: if (actionDeleteMenu != null) actionDeleteMenu.setVisible(true); exchangeInfo.setText(R.string.trade_status_complete_message); setVisible(depositIcon); setGone(depositProgress); setVisible(depositText); depositText.setText(getString(R.string.trade_status_received_deposit, exchangeStatus.depositAmount)); setVisible(exchangeIcon); setGone(exchangeProgress); setVisible(exchangeText); exchangeText.setText(getString(R.string.trade_status_complete_detail, exchangeStatus.withdrawAmount)); setGone(errorIcon); setGone(errorText); updateViewTransaction(); updateEmailReceipt(); stopPolling(); break; case ExchangeEntry.STATUS_FAILED: if (actionDeleteMenu != null) actionDeleteMenu.setVisible(true); setVisible(errorIcon); setVisible(errorText); errorText.setText(R.string.trade_status_failed); stopPolling(); } } private void updateViewTransaction() { final String txId = exchangeStatus.withdrawTransactionId; final Address withdrawAddress = exchangeStatus.withdrawAddress; final CoinType withdrawType = (CoinType) withdrawAddress.getParameters(); final List<WalletAccount> accounts = application.getAccounts(withdrawAddress); if (!accounts.isEmpty() || Constants.COINS_BLOCK_EXPLORERS.containsKey(withdrawType)) { setVisible(viewTransaction); } else { setGone(viewTransaction); } viewTransaction.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String accountId = null; for (WalletAccount account : accounts) { if (account.getTransaction(txId) != null) { accountId = account.getId(); break; } } if (accountId != null && txId != null) { Intent intent = new Intent(getActivity(), TransactionDetailsActivity.class); intent.putExtra(Constants.ARG_ACCOUNT_ID, accountId); intent.putExtra(Constants.ARG_TRANSACTION_ID, txId); startActivity(intent); } else { // Take to an external blockchain explorer if (Constants.COINS_BLOCK_EXPLORERS.containsKey(withdrawType)) { String url = String.format(Constants.COINS_BLOCK_EXPLORERS.get(withdrawType), txId); Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); startActivity(i); } else { // Should not happen Toast.makeText(getActivity(), R.string.error_generic, Toast.LENGTH_SHORT).show(); } } } }); } private void updateEmailReceipt() { // TODO enable setInvisible(emailReceipt); // setVisible(emailReceipt); // emailReceipt.setOnClickListener(new View.OnClickListener() { // @Override // public void onClick(View v) { // emailReceiptDialog.show(getFragmentManager(), null); // } // }); } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mListener = (Listener) activity; contentResolver = activity.getContentResolver(); application = (WalletApplication) activity.getApplication(); loaderManager = getLoaderManager(); } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement " + TradeStatusFragment.Listener.class); } } @Override public void onDetach() { super.onDetach(); mListener = null; } public interface Listener { public void onFinish(); } private final LoaderCallbacks<Cursor> statusLoaderCallbacks = new LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(final int id, final Bundle args) { return new CursorLoader(application.getApplicationContext(), statusUri, null, null, null, null); } @Override public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) { if (data != null && data.getCount() > 0) { data.moveToFirst(); ExchangeEntry newStatus = ExchangeHistoryProvider.getExchangeEntry(data); handler.sendMessage(handler.obtainMessage(UPDATE_STATUS, newStatus)); } } @Override public void onLoaderReset(final Loader<Cursor> loader) { } }; private DialogFragment emailReceiptDialog = new DialogFragment() { @Override @NonNull public Dialog onCreateDialog(Bundle savedInstanceState) { final LayoutInflater inflater = LayoutInflater.from(getActivity()); final View view = inflater.inflate(R.layout.email_receipt_dialog, null); final TextView emailView = (TextView) view.findViewById(R.id.email); return new DialogBuilder(getActivity()) .setTitle(R.string.email_receipt_title) .setView(view) .setNegativeButton(R.string.button_cancel, null) .setPositiveButton(R.string.button_add, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // TODO implement async task String email = emailView.getText().toString(); // ShapeShiftEmail reply = application.getShapeShift().requestEmailReceipt(email, exchangeStatus.getShapeShiftTxStatus()); } }).create(); } }; }