package com.appboy.ui.adapters; import android.annotation.TargetApi; import android.content.Context; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import com.appboy.Constants; import com.appboy.models.cards.BannerImageCard; import com.appboy.models.cards.CaptionedImageCard; import com.appboy.models.cards.Card; import com.appboy.models.cards.CrossPromotionSmallCard; import com.appboy.models.cards.ShortNewsCard; import com.appboy.models.cards.TextAnnouncementCard; import com.appboy.support.AppboyLogger; import com.appboy.ui.widget.BannerImageCardView; import com.appboy.ui.widget.BaseCardView; import com.appboy.ui.widget.CaptionedImageCardView; import com.appboy.ui.widget.CrossPromotionSmallCardView; import com.appboy.ui.widget.DefaultCardView; import com.appboy.ui.widget.ShortNewsCardView; import com.appboy.ui.widget.TextAnnouncementCardView; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Default adapter used to display cards and log card impressions for the Appboy feed. * <p/> * This allows the stream to reuse cards when they go out of view. * <p/> * IMPORTANT - When you add a new card, be sure to add the new view type and update the view count here * <p/> * A card generates an impression once per viewing per open ListView. If a card is viewed more than once * in a particular ListView, it generates only one impression. If closed an reopened, a card will again * generate an impression. This also takes into account the case of a card being off-screen in the ListView. * The card only generates an impression when it actually scrolls onto the screen. * <p/> * IMPORTANT - You must call resetCardImpressionTracker() whenever the ListView is displayed. This will ensure * that cards that come into view will be tracked according to the description above. * <p/> * Adding and removing cards to and from the adapter should be done using the following synchronized * methods: {@link com.appboy.ui.adapters.AppboyListAdapter#add(Card)}, * {@link com.appboy.ui.adapters.AppboyListAdapter#clear()}clear(), * {@link com.appboy.ui.adapters.AppboyListAdapter#replaceFeed(java.util.List)} */ public class AppboyListAdapter extends ArrayAdapter<Card> { private static final String TAG = String.format("%s.%s", Constants.APPBOY_LOG_TAG_PREFIX, AppboyListAdapter.class.getName()); private final Context mContext; private final Set<String> mCardIdImpressions; public AppboyListAdapter(Context context, int layoutResourceId, List<Card> cards) { super(context, layoutResourceId, cards); mContext = context; mCardIdImpressions = new HashSet<String>(); } /** * Be sure to keep view count in sync with the number of card types in the stream. * It is used internally to return the correct card type and to cache views for reuse */ @Override public int getViewTypeCount() { return 8; } @Override public int getItemViewType(int position) { Card card = getItem(position); if (card instanceof BannerImageCard) { return 1; } else if (card instanceof CaptionedImageCard) { return 2; } else if (card instanceof CrossPromotionSmallCard) { return 3; } else if (card instanceof ShortNewsCard) { return 4; } else if (card instanceof TextAnnouncementCard) { return 5; } else { return 0; } } /** * Always try to use a convert view if possible, otherwise create one from scratch. The convertView should always * be of the appropriate type, but it will be recycled, so you need to fully re-populate it with data from the card. */ @Override public View getView(int position, View convertView, ViewGroup parent) { BaseCardView view; Card card = getItem(position); if (convertView == null) { if (card instanceof BannerImageCard) { view = new BannerImageCardView(mContext); } else if (card instanceof CaptionedImageCard) { view = new CaptionedImageCardView(mContext); } else if (card instanceof CrossPromotionSmallCard) { view = new CrossPromotionSmallCardView(mContext); } else if (card instanceof ShortNewsCard) { view = new ShortNewsCardView(mContext); } else if (card instanceof TextAnnouncementCard) { view = new TextAnnouncementCardView(mContext); } else { view = new DefaultCardView(mContext); } } else { AppboyLogger.v(TAG, "Reusing convertView for rendering of item " + position); view = (BaseCardView) convertView; } AppboyLogger.v(TAG, String.format("Using view of type: %s for card at position %d: %s", view.getClass().getName(), position, card.toString())); view.setCard(card); logCardImpression(card); return view; } @SuppressWarnings("checkstyle:localvariablename") public synchronized void replaceFeed(List<Card> cards) { setNotifyOnChange(false); if (cards == null) { clear(); notifyDataSetChanged(); return; } AppboyLogger.d(TAG, String.format("Replacing existing feed of %d cards with new feed containing %d cards.", getCount(), cards.size())); int i = 0; int j = 0; int newFeedSize = cards.size(); Card existingCard; Card newCard; // Iterate over the entire existing feed, skipping items at the head of the list whenever they're the same as the // head of the new list and otherwise removing them. while (i < getCount()) { existingCard = getItem(i); newCard = null; // Only consider a new card if there are any left. if (j < newFeedSize) { newCard = cards.get(j); } // If there is still a card to add and it is the same as the next existing card in the feed, continue. if (newCard != null && newCard.isEqualToCard(existingCard)) { i++; j++; } else { // Otherwise, we need to get rid of the next card in the adapter, and continue checking. remove(existingCard); } } // Now we add the remainder of the feed. if (android.os.Build.VERSION.SDK_INT < 11) { while (j < newFeedSize) { add(cards.get(j)); j++; } } else { addAllBatch(cards.subList(j, newFeedSize)); } notifyDataSetChanged(); } @Override public synchronized void add(Card card) { super.add(card); } @TargetApi(11) private synchronized void addAllBatch(Collection<Card> cards) { super.addAll(cards); } /** * Resets the list of viewed cards. This must be called every time the ListView is displayed and it will reset * the impressions. */ public void resetCardImpressionTracker() { mCardIdImpressions.clear(); } private void logCardImpression(Card card) { String cardId = card.getId(); if (!mCardIdImpressions.contains(cardId)) { mCardIdImpressions.add(cardId); card.logImpression(); AppboyLogger.v(TAG, String.format("Logged impression for card %s", cardId)); } else { AppboyLogger.v(TAG, String.format("Already counted impression for card %s", cardId)); } if (!card.getViewed()) { card.setViewed(true); } } /** * Helper method to batch set cards to visually read after either an up or down scroll of the feed. * Since scrolls can have multiple cards scrolled off screen at a time, this method can batch set those * cards to read. * * @param startIndex Where to start setting cards to viewed. The card at this index will * be set to viewed. Must be less than endIndex * @param endIndex Where to end setting cards to viewed. The card at this index will be set to viewed. */ public void batchSetCardsToRead(int startIndex, int endIndex) { if (getCount() == 0) { AppboyLogger.d(TAG, "mAdapter is empty in setting some cards to viewed."); return; } // Make sure the start and end are in bounds startIndex = Math.max(0, startIndex); endIndex = Math.min(getCount(), endIndex); for (int traversalIndex = startIndex; traversalIndex < endIndex; traversalIndex++) { // Get the card Card card = getItem(traversalIndex); if (card == null) { AppboyLogger.d(TAG, "Card was null in setting some cards to viewed."); break; } if (!card.isRead()) { card.setIsRead(true); } } } }