package com.mygeopay.wallet.ui; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.database.Cursor; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.mygeopay.core.coins.CoinType; import com.mygeopay.core.coins.Value; import com.mygeopay.core.util.GenericUtils; import com.mygeopay.core.wallet.AbstractWallet; import com.mygeopay.core.wallet.WalletPocketConnectivity; import com.mygeopay.wallet.AddressBookProvider; import com.mygeopay.wallet.Configuration; import com.mygeopay.wallet.Constants; import com.mygeopay.wallet.ExchangeRatesProvider; import com.mygeopay.wallet.ExchangeRatesProvider.ExchangeRate; import com.mygeopay.wallet.R; import com.mygeopay.wallet.WalletApplication; import com.mygeopay.wallet.ui.widget.Amount; import com.mygeopay.wallet.util.ThrottlingWalletChangeListener; import com.mygeopay.wallet.util.WeakHandler; import com.google.common.collect.Lists; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.utils.Threading; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.RejectedExecutionException; import javax.annotation.Nonnull; /** * Use the {@link BalanceFragment#newInstance} factory method to * create an instance of this fragment. */ public class BalanceFragment extends Fragment implements LoaderCallbacks<List<Transaction>> { private static final Logger log = LoggerFactory.getLogger(BalanceFragment.class); private static final int WALLET_CHANGED = 0; private static final int UPDATE_VIEW = 1; private static final int AMOUNT_FULL_PRECISION = 8; private static final int AMOUNT_MEDIUM_PRECISION = 6; private static final int AMOUNT_SHORT_PRECISION = 4; private static final int AMOUNT_SHIFT = 0; private static final int ID_TRANSACTION_LOADER = 0; private static final int ID_RATE_LOADER = 1; private final Handler handler = new MyHandler(this); private static class MyHandler extends WeakHandler<BalanceFragment> { public MyHandler(BalanceFragment ref) { super(ref); } @Override protected void weakHandleMessage(BalanceFragment ref, Message msg) { switch (msg.what) { case WALLET_CHANGED: ref.updateBalance(); ref.checkEmptyPocketMessage(); ref.updateConnectivityStatus(); break; case UPDATE_VIEW: ref.updateView(); break; } } } private String accountId; private AbstractWallet pocket; private CoinType type; private Coin currentBalance; private boolean isFullAmount = false; private WalletApplication application; private ContentResolver resolver; private Configuration config; private TransactionsListAdapter adapter; private LoaderManager loaderManager; private View emptyPocketMessage; private NavigationDrawerFragment mNavigationDrawerFragment; private Amount mainAmount; private Amount localAmount; private TextView connectionLabel; private Listener listener; private final ContentObserver addressBookObserver = new ContentObserver(handler) { @Override public void onChange(final boolean selfChange) { adapter.clearLabelCache(); } }; /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param accountId of the account * @return A new instance of fragment InfoFragment. */ public static BalanceFragment newInstance(String accountId) { BalanceFragment fragment = new BalanceFragment(); Bundle args = new Bundle(); args.putSerializable(Constants.ARG_ACCOUNT_ID, accountId); fragment.setArguments(args); return fragment; } public BalanceFragment() { // Required empty public constructor } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { accountId = getArguments().getString(Constants.ARG_ACCOUNT_ID); } //TODO pocket = (AbstractWallet) application.getAccount(accountId); if (pocket == null) { Toast.makeText(getActivity(), R.string.no_such_pocket_error, Toast.LENGTH_LONG).show(); return; } type = pocket.getCoinType(); setHasOptionsMenu(true); mNavigationDrawerFragment = (NavigationDrawerFragment) getFragmentManager().findFragmentById(R.id.navigation_drawer); loaderManager.initLoader(ID_TRANSACTION_LOADER, null, this); loaderManager.initLoader(ID_RATE_LOADER, null, rateLoaderCallbacks); } @Override public void onDestroy() { loaderManager.destroyLoader(ID_TRANSACTION_LOADER); loaderManager.destroyLoader(ID_RATE_LOADER); super.onDestroy(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_balance, container, false); final ListView transactionRows = (ListView) view.findViewById(R.id.transaction_rows); View header = inflater.inflate(R.layout.fragment_balance_header, null); // Initialize your header here. transactionRows.addHeaderView(header, null, false); // Set a space in the end of the list View listFooter = new View(getActivity()); listFooter.setMinimumHeight(getResources().getDimensionPixelSize(R.dimen.activity_vertical_margin)); transactionRows.addFooterView(listFooter); emptyPocketMessage = header.findViewById(R.id.history_empty); // Hide empty message if have some transaction history if (!pocket.getTransactions(false).isEmpty()) { emptyPocketMessage.setVisibility(View.GONE); } // Init list adapter adapter = new TransactionsListAdapter(inflater.getContext(), pocket); adapter.setPrecision(AMOUNT_MEDIUM_PRECISION, 0); transactionRows.setAdapter(adapter); // Start TransactionDetailsActivity on click transactionRows.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (position >= transactionRows.getHeaderViewsCount()) { // Note the usage of getItemAtPosition() instead of adapter's getItem() because // the latter does not take into account the header (which has position 0). Object obj = parent.getItemAtPosition(position); if (obj != null && obj instanceof Transaction) { Intent intent = new Intent(getActivity(), TransactionDetailsActivity.class); intent.putExtra(Constants.ARG_ACCOUNT_ID, accountId); intent.putExtra(Constants.ARG_TRANSACTION_ID, ((Transaction) obj).getHashAsString()); startActivity(intent); } else { Toast.makeText(getActivity(), getString(R.string.get_tx_info_error), Toast.LENGTH_LONG).show(); } } } }); mainAmount = (Amount) view.findViewById(R.id.main_amount); mainAmount.setSymbol(type.getSymbol()); mainAmount.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { isFullAmount = !isFullAmount; updateView(); } }); localAmount = (Amount) view.findViewById(R.id.amount_local); localAmount.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (listener != null) listener.onLocalAmountClick(); } }); connectionLabel = (TextView) view.findViewById(R.id.connection_label); exchangeRate = ExchangeRatesProvider.getRate( application.getApplicationContext(), type.getSymbol(), config.getExchangeCurrencyCode()); // Update the amount updateBalance(pocket.getBalance()); return view; } private void setupConnectivityStatus() { // Set connected for now... setConnectivityStatus(WalletPocketConnectivity.CONNECTED); // ... but check the status in some seconds handler.sendMessageDelayed(handler.obtainMessage(WALLET_CHANGED), 2000); } @Override public void onStart() { super.onStart(); setupConnectivityStatus(); } @Override public void onStop() { super.onStop(); } @Override public void onDestroyView() { super.onDestroyView(); } private void checkEmptyPocketMessage() { if (emptyPocketMessage.isShown() && !pocket.isNew()) { handler.post(new Runnable() { @Override public void run() { emptyPocketMessage.setVisibility(View.GONE); } }); } } private void updateBalance() { updateBalance(pocket.getBalance()); } private void updateBalance(final Value newBalance) { currentBalance = newBalance.toCoin(); updateView(); } private void updateConnectivityStatus() { setConnectivityStatus(pocket.getConnectivityStatus()); } private void setConnectivityStatus(final WalletPocketConnectivity connectivity) { switch (connectivity) { case CONNECTED: connectionLabel.setVisibility(View.GONE); break; default: case DISCONNECTED: connectionLabel.setVisibility(View.VISIBLE); } } private final ThrottlingWalletChangeListener transactionChangeListener = new ThrottlingWalletChangeListener() { @Override public void onThrottledWalletChanged() { adapter.notifyDataSetChanged(); handler.sendMessage(handler.obtainMessage(WALLET_CHANGED)); } }; @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (!mNavigationDrawerFragment.isDrawerOpen()) { // Only show items in the action bar relevant to this screen // if the drawer is not showing. Otherwise, let the drawer // decide what to show in the action bar. inflater.inflate(R.menu.balance, menu); } } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { listener = (Listener) activity; resolver = activity.getContentResolver(); } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement " + Listener.class); } application = (WalletApplication) activity.getApplication(); config = application.getConfiguration(); loaderManager = getLoaderManager(); } @Override public void onDetach() { super.onDetach(); application = null; pocket = null; } @Override public void onResume() { super.onResume(); resolver.registerContentObserver(AddressBookProvider.contentUri( getActivity().getPackageName(), type), true, addressBookObserver); pocket.addEventListener(transactionChangeListener, Threading.SAME_THREAD); checkEmptyPocketMessage(); updateView(); } @Override public void onPause() { pocket.removeEventListener(transactionChangeListener); transactionChangeListener.removeCallbacks(); resolver.unregisterContentObserver(addressBookObserver); super.onPause(); } @Override public Loader<List<Transaction>> onCreateLoader(int id, Bundle args) { return new TransactionsLoader(getActivity(), pocket); } @Override public void onLoadFinished(Loader<List<Transaction>> loader, final List<Transaction> transactions) { handler.post(new Runnable() { @Override public void run() { adapter.replace(transactions); } }); } @Override public void onLoaderReset(Loader<List<Transaction>> loader) { /* ignore */ } private static class TransactionsLoader extends AsyncTaskLoader<List<Transaction>> { private final AbstractWallet walletPocket; private TransactionsLoader(final Context context, @Nonnull final AbstractWallet walletPocket) { super(context); this.walletPocket = walletPocket; } @Override protected void onStartLoading() { super.onStartLoading(); walletPocket.addEventListener(transactionAddRemoveListener, Threading.SAME_THREAD); transactionAddRemoveListener.onWalletChanged(null); // trigger at least one reload forceLoad(); } @Override protected void onStopLoading() { walletPocket.removeEventListener(transactionAddRemoveListener); transactionAddRemoveListener.removeCallbacks(); super.onStopLoading(); } @Override public List<Transaction> loadInBackground() { final List<Transaction> filteredTransactions = Lists.newArrayList(walletPocket.getTransactions(true)); Collections.sort(filteredTransactions, TRANSACTION_COMPARATOR); return filteredTransactions; } private final ThrottlingWalletChangeListener transactionAddRemoveListener = new ThrottlingWalletChangeListener() { @Override public void onThrottledWalletChanged() { try { forceLoad(); } catch (final RejectedExecutionException x) { log.info("rejected execution: " + TransactionsLoader.this.toString()); } } }; private static final Comparator<Transaction> TRANSACTION_COMPARATOR = new Comparator<Transaction>() { @Override public int compare(final Transaction tx1, final Transaction tx2) { final boolean pending1 = tx1.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING; final boolean pending2 = tx2.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING; if (pending1 != pending2) return pending1 ? -1 : 1; // TODO use dates once implemented // final Date updateTime1 = tx1.getUpdateTime(); // final long time1 = updateTime1 != null ? updateTime1.getTime() : 0; // final Date updateTime2 = tx2.getUpdateTime(); // final long time2 = updateTime2 != null ? updateTime2.getTime() : 0; // If both not pending if (!pending1 && !pending2) { final int time1 = tx1.getConfidence().getAppearedAtChainHeight(); final int time2 = tx2.getConfidence().getAppearedAtChainHeight(); if (time1 != time2) return time1 > time2 ? -1 : 1; } return tx1.getHash().compareTo(tx2.getHash()); } }; } private ExchangeRate exchangeRate; private final LoaderCallbacks<Cursor> rateLoaderCallbacks = new LoaderManager.LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(final int id, final Bundle args) { String localSymbol = config.getExchangeCurrencyCode(); String coinSymbol = type.getSymbol(); return new ExchangeRateLoader(getActivity(), config, localSymbol, coinSymbol); } @Override public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) { if (data != null && data.getCount() > 0) { data.moveToFirst(); exchangeRate = ExchangeRatesProvider.getExchangeRate(data); handler.sendEmptyMessage(UPDATE_VIEW); if (log.isInfoEnabled()) { try { log.info("Got exchange rate: {}", exchangeRate.rate.convert(type.oneCoin()).toFriendlyString()); } catch (Exception e) { log.warn(e.getMessage()); } } } } @Override public void onLoaderReset(final Loader<Cursor> loader) { } }; private void updateView() { if (currentBalance != null) { String newBalanceStr = GenericUtils.formatCoinValue(type, currentBalance, isFullAmount ? AMOUNT_FULL_PRECISION : AMOUNT_SHORT_PRECISION, AMOUNT_SHIFT); mainAmount.setAmount(newBalanceStr); } if (currentBalance != null && exchangeRate != null && getView() != null) { try { Value fiatAmount = exchangeRate.rate.convert(type, currentBalance); localAmount.setAmount(GenericUtils.formatFiatValue(fiatAmount)); localAmount.setSymbol(fiatAmount.type.getSymbol()); } catch (Exception e) { // Should not happen localAmount.setAmount(""); localAmount.setSymbol("ERROR"); } } adapter.clearLabelCache(); } public interface Listener { public void onLocalAmountClick(); } }