package com.greenaddress.greenbits.ui;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.greenaddress.greenapi.JSONMap;
import com.greenaddress.greenbits.GaService;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Sha256Hash;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observer;
public class MainFragment extends SubaccountFragment {
private static final String TAG = MainFragment.class.getSimpleName();
private MaterialDialog mUnconfirmedDialog;
private List<TransactionItem> mTxItems;
private Map<Sha256Hash, List<Sha256Hash> > replacedTxs;
private int mSubaccount;
private Observer mVerifiedTxObserver;
private Observer mNewTxObserver;
private final Runnable mDialogCB = new Runnable() { public void run() { mUnconfirmedDialog = null; } };
private SwipeRefreshLayout mSwipeRefreshLayout;
private Boolean mIsExchanger = false;
private void updateBalance() {
Log.d(TAG, "Updating balance");
if (isZombie())
return;
final GaService service = getGAService();
final Coin balance = service.getCoinBalance(mSubaccount);
if (service.getLoginData() == null || balance == null)
return;
final TextView balanceUnit = UI.find(mView, R.id.mainBalanceUnit);
final TextView balanceText = UI.find(mView, R.id.mainBalanceText);
UI.setCoinText(service, balanceUnit, balanceText, balance);
final Coin verifiedBalance = service.getSPVVerifiedBalance(mSubaccount);
// Hide balance question mark if we know our balance is verified
// (or we are in watch only mode and so have no SPV to verify it with)
final boolean verified = balance.equals(verifiedBalance) || !service.isSPVEnabled();
final TextView balanceQuestionMark = UI.find(mView, R.id.mainBalanceQuestionMark);
UI.hideIf(verified, balanceQuestionMark);
final TextView balanceFiatText = UI.find(mView, R.id.mainLocalBalanceText);
final FontAwesomeTextView balanceFiatIcon = UI.find(mView, R.id.mainLocalBalanceIcon);
final int nChars = balanceText.getText().length() + balanceQuestionMark.getText().length() + balanceUnit.getText().length();
final int size = Math.min(50 - nChars, 34);
balanceText.setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
balanceUnit.setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
UI.setAmountText(balanceFiatText, service.getFiatBalance(mSubaccount));
if (!GaService.IS_ELEMENTS)
AmountFields.changeFiatIcon(balanceFiatIcon, service.getFiatCurrency());
else {
balanceUnit.setText(service.getAssetSymbol() + ' ');
balanceText.setText(service.getAssetFormat().format(balance));
if (!mIsExchanger) {
// No fiat values in elements multiasset
UI.hide(UI.find(mView, R.id.mainLocalBalance));
// Currently no SPV either
UI.hide(balanceQuestionMark);
}
}
if (service.showBalanceInTitle())
UI.hide(balanceText, balanceUnit);
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
Log.d(TAG, "onCreateView -> " + TAG);
if (isZombieNoView())
return null;
final GaService service = getGAService();
popupWaitDialog(R.string.loading_transactions);
if (savedInstanceState != null)
mIsExchanger = savedInstanceState.getBoolean("isExchanger", false);
if (mIsExchanger)
mView = inflater.inflate(R.layout.fragment_exchanger_txs, container, false);
else
mView = inflater.inflate(R.layout.fragment_main, container, false);
final RecyclerView txView = UI.find(mView, R.id.mainTransactionList);
txView.setHasFixedSize(true);
txView.addItemDecoration(new DividerItem(getActivity()));
final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
txView.setLayoutManager(layoutManager);
mSubaccount = service.getCurrentSubAccount();
if (!mIsExchanger) {
final TextView firstP = UI.find(mView, R.id.mainFirstParagraphText);
final TextView secondP = UI.find(mView, R.id.mainSecondParagraphText);
final TextView thirdP = UI.find(mView, R.id.mainThirdParagraphText);
if (GaService.IS_ELEMENTS)
UI.hide(firstP); // Don't show a Bitcoin message for elements
else
firstP.setMovementMethod(LinkMovementMethod.getInstance());
secondP.setMovementMethod(LinkMovementMethod.getInstance());
thirdP.setMovementMethod(LinkMovementMethod.getInstance());
}
final TextView balanceText = UI.find(mView, R.id.mainBalanceText);
final TextView balanceQuestionMark = UI.find(mView, R.id.mainBalanceQuestionMark);
final View.OnClickListener unconfirmedClickListener = new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (mUnconfirmedDialog == null && balanceQuestionMark.getVisibility() == View.VISIBLE) {
// Question mark is visible and dialog not shown, so show it
mUnconfirmedDialog = UI.popup(getActivity(), R.string.unconfirmedBalanceTitle, 0)
.content(R.string.unconfirmedBalanceText).build();
UI.setDialogCloseHandler(mUnconfirmedDialog, mDialogCB);
mUnconfirmedDialog.show();
}
}
};
balanceText.setOnClickListener(unconfirmedClickListener);
balanceQuestionMark.setOnClickListener(unconfirmedClickListener);
makeBalanceObserver(mSubaccount);
if (IsPageSelected() && service.getCoinBalance(mSubaccount) != null) {
updateBalance();
reloadTransactions(false, true);
}
if (!mIsExchanger) {
mSwipeRefreshLayout = UI.find(mView, R.id.mainTransactionListSwipe);
mSwipeRefreshLayout.setColorSchemeColors(ContextCompat.getColor(getContext(), R.color.accent));
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
Log.d(TAG, "onRefresh -> " + TAG);
// user action to force reload balance and tx list
onBalanceUpdated();
}
});
}
registerReceiver();
return mView;
}
@Override
protected void onBalanceUpdated() {
Log.d(TAG, "onBalanceUpdated -> " + TAG);
updateBalance();
reloadTransactions(false, false);
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause -> " + TAG);
detachObservers();
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume -> " + TAG);
if (getGAService() != null)
attachObservers();
setIsDirty(true);
}
@Override
public void attachObservers() {
if (mVerifiedTxObserver == null) {
mNewTxObserver = makeUiObserver(new Runnable() { public void run() { onNewTx(); } });
getGAService().addNewTxObserver(mNewTxObserver);
}
if (mVerifiedTxObserver == null) {
mVerifiedTxObserver = makeUiObserver(new Runnable() { public void run() { onVerifiedTx(); } });
getGAService().addVerifiedTxObserver(mVerifiedTxObserver);
}
super.attachObservers();
}
@Override
public void detachObservers() {
super.detachObservers();
if (mVerifiedTxObserver != null) {
getGAService().deleteNewTxObserver(mNewTxObserver);
mNewTxObserver = null;
}
if (mVerifiedTxObserver != null) {
getGAService().deleteVerifiedTxObserver(mVerifiedTxObserver);
mVerifiedTxObserver = null;
}
}
// Called when a new transaction is seen
private void onNewTx() {
if (!IsPageSelected()) {
Log.d(TAG, "New transaction while page hidden");
setIsDirty(true);
return;
}
reloadTransactions(false, false);
}
// Called when a new verified transaction is seen
private void onVerifiedTx() {
if (mTxItems == null)
return;
final GaService service = getGAService();
for (final TransactionItem txItem : mTxItems)
txItem.spvVerified = service.isSPVVerified(txItem.txHash);
final RecyclerView txView = UI.find(mView, R.id.mainTransactionList);
txView.getAdapter().notifyDataSetChanged();
}
private void showTxView(final boolean doShow) {
UI.showIf(doShow, UI.find(mView, R.id.mainTransactionList));
if (!mIsExchanger)
UI.hideIf(doShow, UI.find(mView, R.id.mainEmptyTransText));
}
private void reloadTransactions(final boolean newAdapter, final boolean showWaitDialog) {
final Activity activity = getActivity();
final GaService service = getGAService();
final RecyclerView txView;
if (isZombie())
return;
// Mark ourselves as clean before fetching. This means that while the callback
// is running, we may be marked dirty again if a new block arrives, which
// is required to avoid missing updates while the RPC is in flight.
setIsDirty(false);
txView = UI.find(mView, R.id.mainTransactionList);
if (mTxItems == null || mTxItems.isEmpty() || showWaitDialog) {
// Show a wait dialog only when initially loading transactions
popupWaitDialog(R.string.loading_transactions);
}
if (mTxItems == null || newAdapter) {
mTxItems = new ArrayList<>();
txView.setAdapter(new ListTransactionsAdapter(activity, service, mTxItems, mIsExchanger));
// FIXME, more efficient to use swap
// txView.swapAdapter(lta, false);
}
if (replacedTxs == null || newAdapter)
replacedTxs = new HashMap<>();
Futures.addCallback(service.getMyTransactions(mSubaccount),
new FutureCallback<Map<String, Object>>() {
@Override
public void onSuccess(final Map<String, Object> result) {
final List txList = (List) result.get("list");
final int currentBlock = ((Integer) result.get("cur_block"));
activity.runOnUiThread(new Runnable() {
public void run() {
if (mSwipeRefreshLayout != null)
mSwipeRefreshLayout.setRefreshing(false);
if (!IsPageSelected()) {
Log.d(TAG, "Callback after hiding, ignoring");
// Mark ourselves as dirty so we reload when next shown
setIsDirty(true);
return;
}
showTxView(!txList.isEmpty());
final Sha256Hash oldTop = !mTxItems.isEmpty() ? mTxItems.get(0).txHash : null;
mTxItems.clear();
replacedTxs.clear();
for (final Object tx : txList) {
try {
final JSONMap txJSON = (JSONMap) tx;
final ArrayList<String> replacedList = txJSON.get("replaced_by");
if (replacedList == null) {
mTxItems.add(new TransactionItem(service, txJSON, currentBlock));
continue;
}
for (final String replacedBy : replacedList) {
final Sha256Hash replacedHash = Sha256Hash.wrap(replacedBy);
if (!replacedTxs.containsKey(replacedHash))
replacedTxs.put(replacedHash, new ArrayList<Sha256Hash>());
replacedTxs.get(replacedHash).add(txJSON.getHash("txhash"));
}
} catch (final ParseException e) {
e.printStackTrace();
}
}
final Iterator<TransactionItem> iterator = mTxItems.iterator();
while (iterator.hasNext()) {
final TransactionItem txItem = iterator.next();
final boolean isExchangerAddress = service.cfg().getBoolean("exchanger_address_" + txItem.receivedOn, false);
if (isExchangerAddress && txItem.memo == null) {
txItem.memo = Exchanger.TAG_EXCHANGER_TX_MEMO;
CB.after(service.changeMemo(txItem.txHash, Exchanger.TAG_EXCHANGER_TX_MEMO),
new CB.Toast<Boolean>(activity) {
@Override
public void onSuccess(final Boolean result) {
}
});
} else if (mIsExchanger && (txItem.memo == null || !txItem.memo.contains(Exchanger.TAG_EXCHANGER_TX_MEMO))) {
// FIXME should be better to filter list with api query
iterator.remove();
}
if (replacedTxs.containsKey(txItem.txHash))
for (final Sha256Hash replaced : replacedTxs.get(txItem.txHash))
txItem.replacedHashes.add(replaced);
}
txView.getAdapter().notifyDataSetChanged();
final Sha256Hash newTop = !mTxItems.isEmpty() ? mTxItems.get(0).txHash : null;
if (oldTop != null && newTop != null && !oldTop.equals(newTop)) {
// A new tx has arrived; scroll to the top to show it
txView.smoothScrollToPosition(0);
}
hideWaitDialog();
}
});
}
@Override
public void onFailure(final Throwable t) {
t.printStackTrace();
activity.runOnUiThread(new Runnable() {
public void run() {
hideWaitDialog();
if (mSwipeRefreshLayout != null)
mSwipeRefreshLayout.setRefreshing(false);
}
});
}
}, service.getExecutor());
}
@Override
protected void onSubaccountChanged(final int newSubAccount) {
mSubaccount = newSubAccount;
makeBalanceObserver(mSubaccount);
if (!IsPageSelected()) {
Log.d(TAG, "Subaccount changed while page hidden");
setIsDirty(true);
return;
}
reloadTransactions(false, true);
updateBalance();
}
@Override
public void setUserVisibleHint(final boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
hideKeyboard();
}
}
public void setPageSelected(final boolean isSelected) {
final boolean needReload = isSelected && !IsPageSelected() && isDirty();
super.setPageSelected(isSelected);
if (needReload) {
Log.d(TAG, "Dirty, reloading");
reloadTransactions(false, true);
updateBalance();
if (!isZombie())
setIsDirty(false);
}
}
public void setIsExchanger(final boolean isExchanger) {
mIsExchanger = isExchanger;
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("isExchanger", mIsExchanger);
}
}