package com.mopub.nativeads; import android.content.Context; import android.database.DataSetObserver; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.View; import android.view.ViewGroup; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ListAdapter; import android.widget.ListView; import com.mopub.common.Preconditions; import com.mopub.common.Preconditions.NoThrow; import com.mopub.common.VisibleForTesting; import com.mopub.nativeads.MoPubNativeAdPositioning.MoPubClientPositioning; import com.mopub.nativeads.MoPubNativeAdPositioning.MoPubServerPositioning; import java.util.List; import java.util.WeakHashMap; import static android.widget.AdapterView.OnItemClickListener; import static android.widget.AdapterView.OnItemLongClickListener; import static android.widget.AdapterView.OnItemSelectedListener; import static com.mopub.nativeads.VisibilityTracker.VisibilityTrackerListener; /** * {@code MoPubAdAdapter} facilitates placing ads into an Android {@link android.widget.ListView} or * other widgets that use a {@link android.widget.ListAdapter}. * * For your content items, this class will call your original adapter with the original position of * content before ads were loaded. * * This adapter uses a {@link com.mopub.nativeads.MoPubStreamAdPlacer} object internally. If you * wish to avoid wrapping your original adapter, you can use {@code MoPubStreamAdPlacer} directly. */ public class MoPubAdAdapter extends BaseAdapter { @NonNull private final WeakHashMap<View, Integer> mViewPositionMap; @NonNull private final Adapter mOriginalAdapter; @NonNull private final MoPubStreamAdPlacer mStreamAdPlacer; @NonNull private final VisibilityTracker mVisibilityTracker; @Nullable private MoPubNativeAdLoadedListener mAdLoadedListener; /** * Creates a new MoPubAdAdapter object. * * By default, the adapter will contact the server to determine ad positions. If you * wish to hard-code positions in your app, see {@link MoPubAdAdapter(Context, * MoPubClientPositioning)}. * * @param context The activity context. * @param originalAdapter Your original adapter. */ public MoPubAdAdapter(@NonNull final Context context, @NonNull final Adapter originalAdapter) { this(context, originalAdapter, MoPubNativeAdPositioning.serverPositioning()); } /** * Creates a new MoPubAdAdapter object, using server positioning. * * @param context The activity context. * @param originalAdapter Your original adapter. * @param adPositioning A positioning object for specifying where ads will be placed in your * stream. See {@link MoPubNativeAdPositioning#serverPositioning()}. */ public MoPubAdAdapter(@NonNull final Context context, @NonNull final Adapter originalAdapter, @NonNull final MoPubServerPositioning adPositioning) { this(new MoPubStreamAdPlacer(context, adPositioning), originalAdapter, new VisibilityTracker(context)); } /** * Creates a new MoPubAdAdapter object, using client positioning. * * @param context The activity context. * @param originalAdapter Your original adapter. * @param adPositioning A positioning object for specifying where ads will be placed in your * stream. See {@link MoPubNativeAdPositioning#clientPositioning()}. */ public MoPubAdAdapter(@NonNull final Context context, @NonNull final Adapter originalAdapter, @NonNull final MoPubClientPositioning adPositioning) { this(new MoPubStreamAdPlacer(context, adPositioning), originalAdapter, new VisibilityTracker(context)); } @VisibleForTesting MoPubAdAdapter(@NonNull final MoPubStreamAdPlacer streamAdPlacer, @NonNull final Adapter originalAdapter, @NonNull final VisibilityTracker visibilityTracker) { mOriginalAdapter = originalAdapter; mStreamAdPlacer = streamAdPlacer; mViewPositionMap = new WeakHashMap<View, Integer>(); mVisibilityTracker = visibilityTracker; mVisibilityTracker.setVisibilityTrackerListener(new VisibilityTrackerListener() { @Override public void onVisibilityChanged(@NonNull final List<View> visibleViews, final List<View> invisibleViews) { handleVisibilityChange(visibleViews); } }); mOriginalAdapter.registerDataSetObserver(new DataSetObserver() { @Override public void onChanged() { mStreamAdPlacer.setItemCount(mOriginalAdapter.getCount()); notifyDataSetChanged(); } @Override public void onInvalidated() { notifyDataSetInvalidated(); } }); mStreamAdPlacer.setAdLoadedListener(new MoPubNativeAdLoadedListener() { @Override public void onAdLoaded(final int position) { handleAdLoaded(position); } @Override public void onAdRemoved(final int position) { handleAdRemoved(position); } }); mStreamAdPlacer.setItemCount(mOriginalAdapter.getCount()); } @VisibleForTesting void handleAdLoaded(final int position) { if (mAdLoadedListener != null) { mAdLoadedListener.onAdLoaded(position); } notifyDataSetChanged(); } @VisibleForTesting void handleAdRemoved(final int position) { if (mAdLoadedListener != null) { mAdLoadedListener.onAdRemoved(position); } notifyDataSetChanged(); } /** * Registers a {@link MoPubNativeAdRenderer} to use when displaying ads in your stream. * * This renderer will automatically create and render your view when you call {@link #getView}. * If you register a second renderer, it will replace the first, although this behavior is * subject to change in a future SDK version. * * @param adRenderer The ad renderer. */ public final void registerAdRenderer(@NonNull final MoPubAdRenderer adRenderer) { if (!Preconditions.NoThrow.checkNotNull( adRenderer, "Tried to set a null ad renderer on the placer.")) { return; } mStreamAdPlacer.registerAdRenderer(adRenderer); } /** * Sets a listener that will be called after the SDK loads new ads from the server and places * them into your stream. * * The listener will be active between when you call {@link #loadAds} and when you call {@link * #destroy()}. You can also set the listener to {@code null} to remove the listener. * * Note that there is not a one to one correspondence between calls to {@link #loadAds} and this * listener. The SDK will call the listener every time an ad loads. * * @param listener The listener. */ public final void setAdLoadedListener(@Nullable final MoPubNativeAdLoadedListener listener) { mAdLoadedListener = listener; } /** * Start loading ads from the MoPub server. * * We recommend using {@link #loadAds(String, RequestParameters)} instead of this method, in * order to pass targeting information to the server. * * @param adUnitId The ad unit ID to use when loading ads. */ public void loadAds(@NonNull final String adUnitId) { mStreamAdPlacer.loadAds(adUnitId); } /** * Start loading ads from the MoPub server, using the given request targeting information. * * When loading ads, {@link MoPubNativeAdLoadedListener#onAdLoaded(int)} will be called for each * ad that is added to the stream. * * To refresh ads in your stream, call {@link #refreshAds(ListView, String)}. When new ads load, * they will replace the current ads in your stream. If you are using {@code * MoPubNativeAdLoadedListener} you will see a call to {@code onAdRemoved} for each of the old * ads, followed by a calls to {@code onAdLoaded}. * * @param adUnitId The ad unit ID to use when loading ads. * @param requestParameters Targeting information to pass to the ad server. */ public void loadAds(@NonNull final String adUnitId, @Nullable final RequestParameters requestParameters) { mStreamAdPlacer.loadAds(adUnitId, requestParameters); } /** * Whether the given position is an ad. * * This will return {@code true} only if there is an ad loaded for this position. You can also * listen for ads to load using {@link MoPubNativeAdLoadedListener#onAdLoaded(int)}. * * @param position The position to check for an ad, expressed in terms of the position in the * stream including ads. * @return Whether there is an ad at the given position. */ public boolean isAd(final int position) { return mStreamAdPlacer.isAd(position); } /** * Stops loading ads, immediately clearing any ads currently in the stream. * * This method also stops ads from loading as the user moves through the stream. If you want to * refresh ads, call {@link #refreshAds(ListView, String, RequestParameters)} instead of this * method. * * When ads are cleared, {@link MoPubNativeAdLoadedListener#onAdRemoved} will be called for each * ad that is removed from the stream. */ public void clearAds() { mStreamAdPlacer.clearAds(); } /** * Destroys the ad adapter, preventing it from future use. * * You must call this method before the hosting activity for this class is destroyed in order to * avoid a memory leak. Typically you should destroy the adapter in the life-cycle method that * is counterpoint to the method you used to create the adapter. For example, if you created the * adapter in {@code Fragment#onCreateView} you should destroy it in {code * Fragment#onDestroyView}. */ public void destroy() { mStreamAdPlacer.destroy(); mVisibilityTracker.destroy(); } @Override public boolean areAllItemsEnabled() { return mOriginalAdapter instanceof ListAdapter && ((ListAdapter) mOriginalAdapter).areAllItemsEnabled(); } @Override public boolean isEnabled(final int position) { return isAd(position) || (mOriginalAdapter instanceof ListAdapter && ((ListAdapter) mOriginalAdapter).isEnabled(mStreamAdPlacer.getOriginalPosition( position))); } /** * Returns the number of items in your stream, including ads. * * @return The count of items. * @inheritDoc */ @Override public int getCount() { return mStreamAdPlacer.getAdjustedCount(mOriginalAdapter.getCount()); } /** * For ad items, returns an ad data object. For non ad items, calls you original adapter using * the original item position. * * @inheritDoc */ @Nullable @Override public Object getItem(final int position) { final Object ad = mStreamAdPlacer.getAdData(position); if (ad != null) { return ad; } return mOriginalAdapter.getItem(mStreamAdPlacer.getOriginalPosition(position)); } /** * For ad items, returns an ID representing the ad. For non ad items, calls your original * adapter using the original item position. * * For ads, this ID will be a negative integer. If you feel that this ID might collide with your * original adapter's IDs, you should return {@code false} from {@code #hasStableIds()}. * * @inheritDoc */ @Override public long getItemId(final int position) { final Object adData = mStreamAdPlacer.getAdData(position); if (adData != null) { return ~System.identityHashCode(adData) + 1; } return mOriginalAdapter.getItemId(mStreamAdPlacer.getOriginalPosition(position)); } /** * Returns the value returned by {@code hasStableIds()} on your original adapter. * * @inheritDoc */ @Override public boolean hasStableIds() { return mOriginalAdapter.hasStableIds(); } /** * For ad items, returns an ad View for the underlying position. For non-ad items, calls your * original adapter using the original ad position. * * @inheritDoc */ @Nullable @Override public View getView(final int position, final View view, final ViewGroup viewGroup) { final View resultView; final View adView = mStreamAdPlacer.getAdView(position, view, viewGroup); if (adView != null) { resultView = adView; } else { resultView = mOriginalAdapter.getView( mStreamAdPlacer.getOriginalPosition(position), view, viewGroup); } mViewPositionMap.put(resultView, position); mVisibilityTracker.addView(resultView, 0); return resultView; } /** * For ad items, returns a number greater than or equal to the view type count for your * underlying adapter. For non-ad items, calls your original adapter using the original ad * position. * * @inheritDoc */ @Override public int getItemViewType(final int position) { final int viewType = mStreamAdPlacer.getAdViewType(position); if (viewType != MoPubStreamAdPlacer.CONTENT_VIEW_TYPE) { return viewType + mOriginalAdapter.getViewTypeCount() - 1; } return mOriginalAdapter.getItemViewType(mStreamAdPlacer.getOriginalPosition(position)); } /** * Returns the view type count of your original adapter, plus the the number of possible view * types for ads. The number of possible ad view types is currently 1, but this is subject to * change in future SDK versions. * * @inheritDoc */ @Override public int getViewTypeCount() { return mOriginalAdapter.getViewTypeCount() + mStreamAdPlacer.getAdViewTypeCount(); } /** * Returns whether the adapter is empty, calling through to your original adapter. * * @inheritDoc */ @Override public boolean isEmpty() { return mOriginalAdapter.isEmpty() && mStreamAdPlacer.getAdjustedCount(0) == 0; } private void handleVisibilityChange(@NonNull final List<View> visibleViews) { // Loop through all visible positions in order to build a max and min range, and then // place ads into that range. int min = Integer.MAX_VALUE; int max = 0; for (final View view : visibleViews) { final Integer pos = mViewPositionMap.get(view); if (pos == null) { continue; } min = Math.min(pos, min); max = Math.max(pos, max); } mStreamAdPlacer.placeAdsInRange(min, max + 1); } /** * Returns the original position of an item considering ads in the stream. * * @see {@link MoPubStreamAdPlacer#getOriginalPosition(int)} * @param position The adjusted position. * @return The original position before placing ads. */ public int getOriginalPosition(final int position) { return mStreamAdPlacer.getOriginalPosition(position); } /** * Returns the position of an item considering ads in the stream. * * @see {@link MoPubStreamAdPlacer#getAdjustedPosition(int)} * @param originalPosition The original position. * @return The position adjusted by placing ads. */ public int getAdjustedPosition(final int originalPosition) { return mStreamAdPlacer.getAdjustedPosition(originalPosition); } /** * Inserts a content row at the given position, adjusting ad positions accordingly. * * Use this method if you are inserting an item into your stream and want to increment ad * positions based on that new item. * * If you do not want to increment your ad positions when inserting items, you can simply call * notifyDataSetChanged on the adapter and let it reload items normally. This is typically the * case when inserting items at the end of your stream. * * @see {@link MoPubStreamAdPlacer#insertItem(int)} * @param originalPosition The original content position at which to add an item. If you have an * adjusted position, you will need to call {@link #getOriginalPosition} to get this value. */ public void insertItem(final int originalPosition) { mStreamAdPlacer.insertItem(originalPosition); } /** * Removes the content row at the given position, adjusting ad positions accordingly. * * Use this method if you are removing an item from your stream and want to decrement ad * positions based on that removed item. * * If you do not want to decrement your ad positions when inserting items, you can simply call * notifyDataSet changed on the adapter and let it reload items normally. This is typically the * case when removing items from the end of your stream. * * @see {@link MoPubStreamAdPlacer#removeItem(int)} * @param originalPosition The original content position at which to add an item. If you have an * adjusted position, you will need to call {@link #getOriginalPosition} to get this value. */ public void removeItem(final int originalPosition) { mStreamAdPlacer.removeItem(originalPosition); } /** * Sets an on click listener for the given ListView, automatically adjusting the listener * callback positions based on ads in the adapter. * * This listener will not be called when ads are clicked. * * @param listView The ListView for this adapter. * @param listener An on click listener. */ public void setOnClickListener(@NonNull final ListView listView, @Nullable final OnItemClickListener listener) { if (!NoThrow.checkNotNull(listView, "You called MoPubAdAdapter.setOnClickListener with a" + " null ListView")) { return; } if (listener == null) { listView.setOnItemClickListener(null); return; } listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(final AdapterView<?> adapterView, final View view, final int position, final long id) { if (!mStreamAdPlacer.isAd(position)) { listener.onItemClick( adapterView, view, mStreamAdPlacer.getOriginalPosition(position), id); } } }); } /** * Sets an on long click listener for the given ListView, automatically adjusting the listener * callback positions based on ads in the adapter. * * This listener will not be called when ads are long clicked. * * @param listView The ListView for this adapter. * @param listener An an long click listener. */ public void setOnItemLongClickListener(@NonNull final ListView listView, @Nullable final OnItemLongClickListener listener) { if (!NoThrow.checkNotNull(listView, "You called MoPubAdAdapter." + "setOnItemLongClickListener with a null ListView")) { return; } if (listener == null) { listView.setOnItemLongClickListener(null); return; } listView.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(final AdapterView<?> adapterView, final View view, final int position, final long id) { return isAd(position) || listener.onItemLongClick( adapterView, view, mStreamAdPlacer.getOriginalPosition(position), id); } }); } /** * Sets an on item selected listener for the given ListView, automatically adjusting the * listener callback positions based on ads in the adapter. * * @param listView The ListView for this adapter. * @param listener An an item selected listener. */ public void setOnItemSelectedListener(@NonNull final ListView listView, @Nullable final OnItemSelectedListener listener) { if (!NoThrow.checkNotNull(listView, "You called MoPubAdAdapter.setOnItemSelectedListener" + " with a null ListView")) { return; } if (listener == null) { listView.setOnItemSelectedListener(null); return; } listView.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(final AdapterView<?> adapterView, final View view, final int position, final long id) { if (!isAd(position)) { listener.onItemSelected(adapterView, view, mStreamAdPlacer.getOriginalPosition(position), id); } } @Override public void onNothingSelected(final AdapterView<?> adapterView) { listener.onNothingSelected(adapterView); } }); } /** * Sets the currently selected item in the ListView, automatically adjusting the position based * on ads in the adapter. * * @param listView The ListView for this adapter. * @param originalPosition The original content position before loading ads. */ public void setSelection(@NonNull final ListView listView, final int originalPosition) { if (!NoThrow.checkNotNull(listView, "You called MoPubAdAdapter.setSelection with a null " + "ListView")) { return; } listView.setSelection(mStreamAdPlacer.getAdjustedPosition(originalPosition)); } /** * Scrolls an item in the ListView, automatically adjusting the position based on ads in the * adapter. * * @param listView The ListView for this adapter. * @param originalPosition The original content position before loading ads. */ public void smoothScrollToPosition(@NonNull final ListView listView, final int originalPosition) { if (!NoThrow.checkNotNull(listView, "You called MoPubAdAdapter.smoothScrollToPosition " + "with a null ListView")) { return; } listView.smoothScrollToPosition(mStreamAdPlacer.getAdjustedPosition(originalPosition)); } /** * Refreshes ads in the given ListView while preserving the scroll position. * * Call this instead of {@link #loadAds(String)} in order to preserve the scroll position in * your list. * * @param adUnitId The ad unit ID to use when loading ads. */ public void refreshAds(@NonNull final ListView listView, @NonNull String adUnitId) { refreshAds(listView, adUnitId, null); } /** * Refreshes ads in the given ListView while preserving the scroll position. * * Call this instead of {@link #loadAds(String, RequestParameters)} in order to preserve the * scroll position in your list. * * @param adUnitId The ad unit ID to use when loading ads. * @param requestParameters Targeting information to pass to the ad server. */ public void refreshAds(@NonNull final ListView listView, @NonNull String adUnitId, @Nullable RequestParameters requestParameters) { if (!NoThrow.checkNotNull(listView, "You called MoPubAdAdapter.refreshAds with a null " + "ListView")) { return; } // Get scroll offset of the first view, if it exists. View firstView = listView.getChildAt(0); int offsetY = (firstView == null) ? 0 : firstView.getTop(); // Find the range of positions where we should not clear ads. int firstPosition = listView.getFirstVisiblePosition(); int startRange = Math.max(firstPosition - 1, 0); while (mStreamAdPlacer.isAd(startRange) && startRange > 0) { startRange--; } int lastPosition = listView.getLastVisiblePosition(); while (mStreamAdPlacer.isAd(lastPosition) && lastPosition < getCount() - 1) { lastPosition++; } int originalStartRange = mStreamAdPlacer.getOriginalPosition(startRange); int originalEndRange = mStreamAdPlacer.getOriginalCount(lastPosition + 1); // Remove ads before and after the range. int originalCount = mStreamAdPlacer.getOriginalCount(getCount()); mStreamAdPlacer.removeAdsInRange(originalEndRange, originalCount); int numAdsRemoved = mStreamAdPlacer.removeAdsInRange(0, originalStartRange); // Reset the scroll position, and reload ads. if (numAdsRemoved > 0) { listView.setSelectionFromTop(firstPosition - numAdsRemoved, offsetY); } loadAds(adUnitId, requestParameters); } }