package com.mopub.nativeads; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.View; import com.mopub.common.Preconditions.NoThrow; import com.mopub.common.logging.MoPubLog; import com.mopub.network.Networking; import com.mopub.volley.VolleyError; import com.mopub.volley.toolbox.ImageLoader; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static com.mopub.nativeads.CustomEventNative.CustomEventNativeListener; import static com.mopub.nativeads.CustomEventNative.ImageListener; abstract class BaseForwardingNativeAd implements NativeAdInterface { private static final int IMPRESSION_MIN_PERCENTAGE_VIEWED = 50; static interface NativeEventListener { public void onAdImpressed(); public void onAdClicked(); } @Nullable private NativeEventListener mNativeEventListener; static final double MIN_STAR_RATING = 0; static final double MAX_STAR_RATING = 5; // Basic fields @Nullable private String mMainImageUrl; @Nullable private String mIconImageUrl; @Nullable private String mClickDestinationUrl; @Nullable private String mCallToAction; @Nullable private String mTitle; @Nullable private String mText; @Nullable private Double mStarRating; // Impression logistics @NonNull private final Set<String> mImpressionTrackers; private int mImpressionMinTimeViewed; // Extras @NonNull private final Map<String, Object> mExtras; // Event Logistics private boolean mIsOverridingClickTracker; private boolean mIsOverridingImpressionTracker; BaseForwardingNativeAd() { mImpressionMinTimeViewed = 1000; mImpressionTrackers = new HashSet<String>(); mExtras = new HashMap<String, Object>(); } // Getters /** * Returns the String url corresponding to the ad's main image. */ @Nullable @Override final public String getMainImageUrl() { return mMainImageUrl; } /** * Returns the String url corresponding to the ad's icon image. */ @Nullable @Override final public String getIconImageUrl() { return mIconImageUrl; } /** * Returns a Set<String> of all impression trackers associated with this native ad. Note that * network requests will automatically be made to each of these impression trackers when the * native ad is display on screen. See {@link BaseForwardingNativeAd#getImpressionMinPercentageViewed} * and {@link BaseForwardingNativeAd#getImpressionMinTimeViewed()} for relevant * impression-tracking parameters. */ @NonNull @Override final public Set<String> getImpressionTrackers() { return new HashSet<String>(mImpressionTrackers); } /** * Returns the String url that the device will attempt to resolve when the ad is clicked. */ @Nullable @Override final public String getClickDestinationUrl() { return mClickDestinationUrl; } /** * Returns the Call To Action String (i.e. "Download" or "Learn More") associated with this ad. */ @Nullable @Override final public String getCallToAction() { return mCallToAction; } /** * Returns the String corresponding to the ad's title. */ @Nullable @Override final public String getTitle() { return mTitle; } /** * Returns the String corresponding to the ad's body text. */ @Nullable @Override final public String getText() { return mText; } /** * For app install ads, this returns the associated star rating (on a 5 star scale) for the * advertised app. Note that this method may return null if the star rating was either never set * or invalid. */ @Nullable @Override final public Double getStarRating() { return mStarRating; } /** * Returns the minimum viewable percentage of the ad that must be onscreen for it to be * considered visible. See {@link BaseForwardingNativeAd#getImpressionMinTimeViewed()} for * additional impression tracking considerations. */ @Override final public int getImpressionMinPercentageViewed() { return IMPRESSION_MIN_PERCENTAGE_VIEWED; } /** * Returns the minimum amount of time (in milliseconds) the ad that must be onscreen before an * impression is recorded. See {@link BaseForwardingNativeAd#getImpressionMinPercentageViewed()} * for additional impression tracking considerations. */ @Override final public int getImpressionMinTimeViewed() { return mImpressionMinTimeViewed; } // Extras Getters /** * Given a particular String key, return the associated Object value from the ad's extras map. * See {@link BaseForwardingNativeAd#getExtras()} for more information. */ @Nullable @Override final public Object getExtra(@NonNull final String key) { if (!NoThrow.checkNotNull(key, "getExtra key is not allowed to be null")) { return null; } return mExtras.get(key); } /** * Returns a copy of the extras map, reflecting additional ad content not reflected in any * of the above hardcoded setters. This is particularly useful for passing down custom fields * with MoPub's direct-sold native ads or from mediated networks that pass back additional * fields. */ @NonNull @Override final public Map<String, Object> getExtras() { return new HashMap<String, Object>(mExtras); } /** * Returns {@code true} if the native ad is using a network impression tracker. If set to * true, the network must expose a callback that calls into * {@link BaseForwardingNativeAd#notifyAdImpressed()} in order for MoPub to fire its impression * tracker at the appropriate time. */ @Override final public boolean isOverridingImpressionTracker() { return mIsOverridingImpressionTracker; } /** * Returns {@code true} if the native ad is using a network click tracker. If set to true, the * network must expose a callback that calls into * {@link BaseForwardingNativeAd#notifyAdClicked()} in order for MoPub to fire its click tracker * at the appropriate time. */ @Override final public boolean isOverridingClickTracker() { return mIsOverridingClickTracker; } // Setters @Override public final void setNativeEventListener( @Nullable final NativeEventListener nativeEventListener) { mNativeEventListener = nativeEventListener; } final void setMainImageUrl(@Nullable final String mainImageUrl) { mMainImageUrl = mainImageUrl; } final void setIconImageUrl(@Nullable final String iconImageUrl) { mIconImageUrl = iconImageUrl; } final void setClickDestinationUrl(@Nullable final String clickDestinationUrl) { mClickDestinationUrl = clickDestinationUrl; } final void setCallToAction(@Nullable final String callToAction) { mCallToAction = callToAction; } final void setTitle(@Nullable final String title) { mTitle = title; } final void setText(@Nullable final String text) { mText = text; } final void setStarRating(@Nullable final Double starRating) { if (starRating == null) { mStarRating = null; } else if (starRating >= MIN_STAR_RATING && starRating <= MAX_STAR_RATING) { mStarRating = starRating; } else { MoPubLog.d("Ignoring attempt to set invalid star rating (" + starRating + "). Must be " + "between " + MIN_STAR_RATING + " and " + MAX_STAR_RATING + "."); } } final void addExtra(@NonNull final String key, @Nullable final Object value) { if (!NoThrow.checkNotNull(key, "addExtra key is not allowed to be null")) { return; } mExtras.put(key, value); } final void addImpressionTracker(@NonNull final String url) { if (!NoThrow.checkNotNull(url, "impressionTracker url is not allowed to be null")) { return; } mImpressionTrackers.add(url); } final void setImpressionMinTimeViewed(final int impressionMinTimeViewed) { if (impressionMinTimeViewed >= 0) { mImpressionMinTimeViewed = impressionMinTimeViewed; } } final void setOverridingImpressionTracker(final boolean isOverridingImpressionTracker) { mIsOverridingImpressionTracker = isOverridingImpressionTracker; } final void setOverridingClickTracker(final boolean isOverridingClickTracker) { mIsOverridingClickTracker = isOverridingClickTracker; } // Event Handlers /** * Your base native ad subclass should implement this method if the network requires the developer * to prepare state for recording an impression or click before a view is rendered to screen. * * This method is optional. */ @Override public void prepare(@Nullable final View view) { } /** * Your base native ad subclass should implement this method if the network requires the developer * to explicitly record an impression of a view rendered to screen. * * This method is optional. */ @Override public void recordImpression() { } /** * Your base native ad subclass should implement this method if the network requires the developer * to explicitly handle click events of views rendered to screen. * * This method is optional. */ @Override public void handleClick(@Nullable final View view) { } /** * Your base native ad subclass should implement this method if the network requires the developer * to reset or clear state of the native ad after it goes off screen and before it is rendered * again. * * This method is optional. */ @Override public void clear(@Nullable final View view) { } /** * Your base native ad subclass should implement this method if the network requires the developer * to destroy or cleanup their native ad when they are finished with it. * * This method is optional. */ @Override public void destroy() { } // Event Notifiers /** * Notifies the SDK that the ad has been shown. This will cause the SDK to record an impression * for the ad. This is meant for network SDKs that expose their own impression tracking * callbacks, and requires that you call * {@link BaseForwardingNativeAd#setOverridingImpressionTracker} from your implementation of * {@link BaseForwardingNativeAd#prepare}. */ protected final void notifyAdImpressed() { if (mNativeEventListener != null) { mNativeEventListener.onAdImpressed(); } } /** * Notifies the SDK that the user has clicked the ad. This will cause the SDK to record an * click for the ad. This is meant for network SDKs that expose their own click * tracking callbacks, and requires that you call * {@link BaseForwardingNativeAd#setOverridingClickTracker} from your implementation of * {@link BaseForwardingNativeAd#prepare}. */ protected final void notifyAdClicked() { if (mNativeEventListener != null) { mNativeEventListener.onAdClicked(); } } /** * Pre caches the given set of image urls. We recommend using this method to warm the image * cache before calling {@link CustomEventNativeListener#onNativeAdLoaded}. Doing so will * force images to cache before displaying the ad. */ static void preCacheImages(@NonNull final Context context, @NonNull final List<String> imageUrls, @NonNull final ImageListener imageListener) { final ImageLoader imageLoader = Networking.getImageLoader(context); // These Atomics are only accessed on the main thread. // We use Atomics here so we can change their values while keeping a reference for the inner class. final AtomicInteger imageCounter = new AtomicInteger(imageUrls.size()); final AtomicBoolean anyFailures = new AtomicBoolean(false); ImageLoader.ImageListener volleyImageListener = new ImageLoader.ImageListener() { @Override public void onResponse(final ImageLoader.ImageContainer imageContainer, final boolean isImmediate) { // Image Loader returns a "default" response immediately. We want to ignore this // unless the image is already cached. if (imageContainer.getBitmap() != null) { final int count = imageCounter.decrementAndGet(); if (count == 0 && !anyFailures.get()) { imageListener.onImagesCached(); } } } @Override public void onErrorResponse(final VolleyError volleyError) { MoPubLog.d("Failed to download a native ads image:", volleyError); boolean anyPreviousErrors = anyFailures.getAndSet(true); imageCounter.decrementAndGet(); if (!anyPreviousErrors) { imageListener.onImagesFailedToCache(NativeErrorCode.IMAGE_DOWNLOAD_FAILURE); } } }; for (String url : imageUrls) { if (TextUtils.isEmpty(url)) { anyFailures.set(true); imageListener.onImagesFailedToCache(NativeErrorCode.IMAGE_DOWNLOAD_FAILURE); return; } imageLoader.get(url, volleyImageListener); } } }