/* * 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 java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Typeface; import android.text.Html; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import com.google.devcoin.core.Address; import com.google.devcoin.core.ScriptException; import com.google.devcoin.core.Transaction; import com.google.devcoin.core.Transaction.Purpose; import com.google.devcoin.core.TransactionConfidence; import com.google.devcoin.core.TransactionConfidence.ConfidenceType; import com.google.devcoin.core.Wallet; import com.google.devcoin.core.Wallet.DefaultCoinSelector; import devcoin.wallet.AddressBookProvider; import devcoin.wallet.Constants; import devcoin.wallet.util.CircularProgressView; import devcoin.wallet.util.WalletUtils; import devcoin.wallet.R; /** * @author Andreas Schildbach */ public class TransactionsListAdapter extends BaseAdapter { private final Context context; private final LayoutInflater inflater; private final Wallet wallet; private final int maxConnectedPeers; private final List<Transaction> transactions = new ArrayList<Transaction>(); private int precision = 0; private int shift = 0; private boolean showEmptyText = false; private boolean showBackupWarning = false; private final int colorSignificant; private final int colorInsignificant; private final int colorError; private final int colorCircularBuilding = Color.parseColor("#44ff44"); private final String textCoinBase; private final String textInternal; private final Map<String, String> labelCache = new HashMap<String, String>(); private final static String CACHE_NULL_MARKER = ""; private static final String CONFIDENCE_SYMBOL_DEAD = "\u271D"; // latin cross private static final String CONFIDENCE_SYMBOL_UNKNOWN = "?"; private static final int VIEW_TYPE_TRANSACTION = 0; private static final int VIEW_TYPE_WARNING = 1; public TransactionsListAdapter(final Context context, @Nonnull final Wallet wallet, final int maxConnectedPeers, final boolean showBackupWarning) { this.context = context; inflater = LayoutInflater.from(context); this.wallet = wallet; this.maxConnectedPeers = maxConnectedPeers; this.showBackupWarning = showBackupWarning; final Resources resources = context.getResources(); colorSignificant = resources.getColor(R.color.fg_significant); colorInsignificant = resources.getColor(R.color.fg_insignificant); colorError = resources.getColor(R.color.fg_error); textCoinBase = context.getString(R.string.wallet_transactions_fragment_coinbase); textInternal = context.getString(R.string.wallet_transactions_fragment_internal); } public void setPrecision(final int precision, final int shift) { this.precision = precision; this.shift = shift; notifyDataSetChanged(); } public void clear() { transactions.clear(); notifyDataSetChanged(); } public void replace(@Nonnull final Transaction tx) { transactions.clear(); transactions.add(tx); notifyDataSetChanged(); } public void replace(@Nonnull final Collection<Transaction> transactions) { this.transactions.clear(); this.transactions.addAll(transactions); showEmptyText = true; notifyDataSetChanged(); } @Override public boolean isEmpty() { return showEmptyText && super.isEmpty(); } @Override public int getCount() { int count = transactions.size(); if (count == 1 && showBackupWarning) count++; return count; } @Override public Transaction getItem(final int position) { if (position == transactions.size() && showBackupWarning) return null; return transactions.get(position); } @Override public long getItemId(final int position) { if (position == transactions.size() && showBackupWarning) return 0; return WalletUtils.longHash(transactions.get(position).getHash()); } @Override public int getViewTypeCount() { return 2; } @Override public int getItemViewType(final int position) { if (position == transactions.size() && showBackupWarning) return VIEW_TYPE_WARNING; else return VIEW_TYPE_TRANSACTION; } @Override public boolean hasStableIds() { return true; } @Override public View getView(final int position, View row, final ViewGroup parent) { final int type = getItemViewType(position); if (type == VIEW_TYPE_TRANSACTION) { if (row == null) row = inflater.inflate(R.layout.transaction_row_extended, null); final Transaction tx = getItem(position); bindView(row, tx); } else if (type == VIEW_TYPE_WARNING) { if (row == null) row = inflater.inflate(R.layout.transaction_row_warning, null); final TextView messageView = (TextView) row.findViewById(R.id.transaction_row_warning_message); messageView.setText(Html.fromHtml(context.getString(R.string.wallet_transactions_row_warning_backup))); } else { throw new IllegalStateException("unknown type: " + type); } return row; } public void bindView(@Nonnull final View row, @Nonnull final Transaction tx) { final TransactionConfidence confidence = tx.getConfidence(); final ConfidenceType confidenceType = confidence.getConfidenceType(); final boolean isOwn = confidence.getSource().equals(TransactionConfidence.Source.SELF); final boolean isCoinBase = tx.isCoinBase(); final boolean isInternal = WalletUtils.isInternal(tx); try { final BigInteger value = tx.getValue(wallet); final boolean sent = value.signum() < 0; final CircularProgressView rowConfidenceCircular = (CircularProgressView) row.findViewById(R.id.transaction_row_confidence_circular); final TextView rowConfidenceTextual = (TextView) row.findViewById(R.id.transaction_row_confidence_textual); // confidence if (confidenceType == ConfidenceType.PENDING) { rowConfidenceCircular.setVisibility(View.VISIBLE); rowConfidenceTextual.setVisibility(View.GONE); rowConfidenceCircular.setProgress(1); rowConfidenceCircular.setMaxProgress(1); rowConfidenceCircular.setSize(confidence.numBroadcastPeers()); rowConfidenceCircular.setMaxSize(maxConnectedPeers / 2); // magic value rowConfidenceCircular.setColors(colorInsignificant, colorInsignificant); } else if (confidenceType == ConfidenceType.BUILDING) { rowConfidenceCircular.setVisibility(View.VISIBLE); rowConfidenceTextual.setVisibility(View.GONE); rowConfidenceCircular.setProgress(confidence.getDepthInBlocks()); rowConfidenceCircular.setMaxProgress(isCoinBase ? Constants.NETWORK_PARAMETERS.getSpendableCoinbaseDepth() : Constants.MAX_NUM_CONFIRMATIONS); rowConfidenceCircular.setSize(1); rowConfidenceCircular.setMaxSize(1); rowConfidenceCircular.setColors(colorCircularBuilding, Color.DKGRAY); } else if (confidenceType == ConfidenceType.DEAD) { rowConfidenceCircular.setVisibility(View.GONE); rowConfidenceTextual.setVisibility(View.VISIBLE); rowConfidenceTextual.setText(CONFIDENCE_SYMBOL_DEAD); rowConfidenceTextual.setTextColor(Color.RED); } else { rowConfidenceCircular.setVisibility(View.GONE); rowConfidenceTextual.setVisibility(View.VISIBLE); rowConfidenceTextual.setText(CONFIDENCE_SYMBOL_UNKNOWN); rowConfidenceTextual.setTextColor(colorInsignificant); } // spendability final int textColor; if (confidenceType == ConfidenceType.DEAD) textColor = Color.RED; else textColor = DefaultCoinSelector.isSelectable(tx) ? colorSignificant : colorInsignificant; // time final TextView rowTime = (TextView) row.findViewById(R.id.transaction_row_time); if (rowTime != null) { final Date time = tx.getUpdateTime(); rowTime.setText(time != null ? (DateUtils.getRelativeTimeSpanString(context, time.getTime())) : null); rowTime.setTextColor(textColor); } // receiving or sending final TextView rowFromTo = (TextView) row.findViewById(R.id.transaction_row_fromto); if (isInternal) rowFromTo.setText(R.string.symbol_internal); else if (sent) rowFromTo.setText(R.string.symbol_to); else rowFromTo.setText(R.string.symbol_from); rowFromTo.setTextColor(textColor); // coinbase final View rowCoinbase = row.findViewById(R.id.transaction_row_coinbase); rowCoinbase.setVisibility(isCoinBase ? View.VISIBLE : View.GONE); // address final TextView rowAddress = (TextView) row.findViewById(R.id.transaction_row_address); final Address address = sent ? WalletUtils.getFirstToAddress(tx) : WalletUtils.getFirstFromAddress(tx); final String label; if (isCoinBase) label = textCoinBase; else if (isInternal) label = textInternal; else if (address != null) label = resolveLabel(address.toString()); else label = "?"; rowAddress.setTextColor(textColor); rowAddress.setText(label != null ? label : address.toString()); rowAddress.setTypeface(label != null ? Typeface.DEFAULT : Typeface.MONOSPACE); // value final CurrencyTextView rowValue = (CurrencyTextView) row.findViewById(R.id.transaction_row_value); rowValue.setTextColor(textColor); rowValue.setAlwaysSigned(true); rowValue.setPrecision(precision, shift); rowValue.setAmount(value); // extended message final View rowExtend = row.findViewById(R.id.transaction_row_extend); if (rowExtend != null) { final TextView rowMessage = (TextView) row.findViewById(R.id.transaction_row_message); final boolean isTimeLocked = tx.isTimeLocked(); rowExtend.setVisibility(View.GONE); if (tx.getPurpose() == Purpose.KEY_ROTATION) { rowExtend.setVisibility(View.VISIBLE); rowMessage.setText(Html.fromHtml(context.getString(R.string.transaction_row_message_purpose_key_rotation))); rowMessage.setTextColor(colorSignificant); } else if (isOwn && confidenceType == ConfidenceType.PENDING && confidence.numBroadcastPeers() <= 1) { rowExtend.setVisibility(View.VISIBLE); rowMessage.setText(R.string.transaction_row_message_own_unbroadcasted); rowMessage.setTextColor(colorInsignificant); } else if (!sent && value.compareTo(Transaction.MIN_NONDUST_OUTPUT) < 0) { rowExtend.setVisibility(View.VISIBLE); rowMessage.setText(R.string.transaction_row_message_received_dust); rowMessage.setTextColor(colorInsignificant); } else if (!sent && confidenceType == ConfidenceType.PENDING && isTimeLocked) { rowExtend.setVisibility(View.VISIBLE); rowMessage.setText(R.string.transaction_row_message_received_unconfirmed_locked); rowMessage.setTextColor(colorError); } else if (!sent && confidenceType == ConfidenceType.PENDING && !isTimeLocked) { rowExtend.setVisibility(View.VISIBLE); rowMessage.setText(R.string.transaction_row_message_received_unconfirmed_unlocked); rowMessage.setTextColor(colorInsignificant); } else if (!sent && confidenceType == ConfidenceType.DEAD) { rowExtend.setVisibility(View.VISIBLE); rowMessage.setText(R.string.transaction_row_message_received_dead); rowMessage.setTextColor(colorError); } } } catch (final ScriptException x) { throw new RuntimeException(x); } } private String resolveLabel(@Nonnull final String address) { final String cachedLabel = labelCache.get(address); if (cachedLabel == null) { final String label = AddressBookProvider.resolveLabel(context, address); if (label != null) labelCache.put(address, label); else labelCache.put(address, CACHE_NULL_MARKER); return label; } else { return cachedLabel != CACHE_NULL_MARKER ? cachedLabel : null; } } public void clearLabelCache() { labelCache.clear(); notifyDataSetChanged(); } }