This file was deleted.

Oops, something went wrong.
@@ -0,0 +1,122 @@
package com.mopub.nativeads;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;

import com.mopub.common.Preconditions;

import java.util.HashSet;
import java.util.Set;

/**
* This is the base class for implementations of all native ad formats. When implementing a new
* native ad format, be sure to also implement and register an appropriate {@link MoPubAdRenderer}
* that supports the format.
*/
public abstract class BaseNativeAd {

public interface NativeEventListener {
void onAdImpressed();
void onAdClicked();
}

@NonNull final private Set<String> mImpressionTrackers;
@NonNull final private Set<String> mClickTrackers;
@Nullable private NativeEventListener mNativeEventListener;

protected BaseNativeAd() {
mImpressionTrackers = new HashSet<String>();
mClickTrackers = new HashSet<String>();
}

// Lifecycle Handlers
/**
* Your {@link BaseNativeAd} 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.
*/
public abstract void prepare(@NonNull final View view);

/**
* Your {@link BaseNativeAd} 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.
*/
public abstract void clear(@NonNull final View view);

/**
* Your {@link BaseNativeAd} subclass should implement this method if the network requires the developer
* to destroy or cleanup their native ad when they are permanently finished with it.
*
* This method is optional.
*/
public abstract void destroy();

public void setNativeEventListener(
@Nullable final NativeEventListener nativeEventListener) {
mNativeEventListener = nativeEventListener;
}

// Event Notifiers
/**
* Notifies the SDK that the ad has been shown. This will cause the SDK to record an impression
* for the ad. This method must be called when the native ad is impressed in order for the
* MoPub impression trackers to fire correctly.
*/
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 method must be called when the native ad is clicked in order for the
* MoPub click trackers to fire correctly.
*/
protected final void notifyAdClicked() {
if (mNativeEventListener != null) {
mNativeEventListener.onAdClicked();
}
}

final public void addImpressionTracker(@NonNull final String url) {
if (!Preconditions.NoThrow.checkNotNull(url, "impressionTracker url is not allowed to be null")) {
return;
}
mImpressionTrackers.add(url);
}

final public void addClickTracker(@NonNull final String url) {
if (!Preconditions.NoThrow.checkNotNull(url, "clickTracker url is not allowed to be null")) {
return;
}
mClickTrackers.add(url);
}

/**
* 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 StaticNativeAd#getImpressionMinPercentageViewed}
* and {@link StaticNativeAd#getImpressionMinTimeViewed()} for relevant
* impression-tracking parameters.
*/
@NonNull
Set<String> getImpressionTrackers() {
return new HashSet<String>(mImpressionTrackers);
}

/**
* Returns a Set<String> of all click trackers associated with this native ad. Note that
* network requests will automatically be made to each of these click trackers when the
* native ad is clicked.
*/
@NonNull
Set<String> getClickTrackers() {
return new HashSet<String>(mClickTrackers);
}
}
@@ -0,0 +1,11 @@
package com.mopub.nativeads;

import android.view.View;

/**
* This interface should be implemented by native ad formats that want to make use of the
* {@link NativeClickHandler} to track clicks and open click destinations.
*/
public interface ClickInterface {
void handleClick(View view);
}
@@ -36,31 +36,15 @@ protected abstract void loadNativeAd(@NonNull final Context context,
@NonNull final Map<String, Object> localExtras,
@NonNull final Map<String, String> serverExtras);

public interface ImageListener {
/**
* Called when images are successfully cached. If you haven't already called {@link
* CustomEventNativeListener#onNativeAdLoaded}, you should typically do so now.
*/
void onImagesCached();

/**
* Called when images failed to cache. You should typically call {@link
* CustomEventNativeListener#onNativeAdFailed} from this callback.
*
* @param errorCode An enum value with the relevant error message.
*/
void onImagesFailedToCache(NativeErrorCode errorCode);
}

public interface CustomEventNativeListener {
/**
* Your custom event subclass must call this method when it successfully loads a native ad.
* Failure to do so will disrupt the mediation waterfall and cause future ad requests to
* stall.
*
* @param nativeAd The ad that was succesfully loaded.
* @param nativeAd The ad that was successfully loaded.
*/
void onNativeAdLoaded(NativeAdInterface nativeAd);
void onNativeAdLoaded(BaseNativeAd nativeAd);

/**
* Your custom event subclass must call this method when it fails to load a native ad.
@@ -4,6 +4,7 @@
import android.support.annotation.NonNull;

import com.mopub.common.DataKeys;
import com.mopub.common.event.EventDetails;
import com.mopub.common.logging.MoPubLog;
import com.mopub.nativeads.factories.CustomEventNativeFactory;
import com.mopub.network.AdResponse;
@@ -31,6 +32,13 @@ public static void loadNativeAd(@NonNull final Context context,
localExtras.put(DataKeys.JSON_BODY_KEY, adResponse.getJsonBody());
}

final EventDetails eventDetails = adResponse.getEventDetails();
if (eventDetails != null) {
localExtras.put(DataKeys.EVENT_DETAILS, eventDetails);
}

localExtras.put(DataKeys.CLICK_TRACKING_URL_KEY, adResponse.getClickTrackingUrl());

// Custom event classes can be developed by any third party and may not be tested.
// We catch all exceptions here to prevent crashes from untested code.
try {

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -0,0 +1,15 @@
package com.mopub.nativeads;

import android.view.View;

/**
* This interface should be implemented by native ad formats that want to make use of the
* {@link ImpressionTracker} to track impressions.
*/
public interface ImpressionInterface {
int getImpressionMinPercentageViewed();
int getImpressionMinTimeViewed();
void recordImpression(View view);
boolean isImpressionRecorded();
void setImpressionRecorded();
}
@@ -2,6 +2,7 @@

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
@@ -16,18 +17,22 @@
import static com.mopub.nativeads.VisibilityTracker.VisibilityChecker;
import static com.mopub.nativeads.VisibilityTracker.VisibilityTrackerListener;

class ImpressionTracker {
/**
* Impression tracker used to call {@link ImpressionInterface#recordImpression(View)} when a
* percentage of a native ad has been on screen for a duration of time.
*/
public class ImpressionTracker {

private static final int PERIOD = 250;

// Object tracking visibility of added views
@NonNull private final VisibilityTracker mVisibilityTracker;

// All views and responses being tracked for impressions
@NonNull private final Map<View, NativeResponse> mTrackedViews;
// All views and ads being tracked for impressions
@NonNull private final Map<View, ImpressionInterface> mTrackedViews;

// Visible views being polled for time on screen before tracking impression
@NonNull private final Map<View, TimestampWrapper<NativeResponse>> mPollingViews;
@NonNull private final Map<View, TimestampWrapper<ImpressionInterface>> mPollingViews;

// Handler for polling visible views
@NonNull private final Handler mPollHandler;
@@ -41,17 +46,17 @@
// Listener for when a view becomes visible or non visible
@Nullable private VisibilityTrackerListener mVisibilityTrackerListener;

ImpressionTracker(@NonNull final Context context) {
this(new WeakHashMap<View, NativeResponse>(),
new WeakHashMap<View, TimestampWrapper<NativeResponse>>(),
public ImpressionTracker(@NonNull final Context context) {
this(new WeakHashMap<View, ImpressionInterface>(),
new WeakHashMap<View, TimestampWrapper<ImpressionInterface>>(),
new VisibilityChecker(),
new VisibilityTracker(context),
new Handler());
new Handler(Looper.getMainLooper()));
}

@VisibleForTesting
ImpressionTracker(@NonNull final Map<View, NativeResponse> trackedViews,
@NonNull final Map<View, TimestampWrapper<NativeResponse>> pollingViews,
ImpressionTracker(@NonNull final Map<View, ImpressionInterface> trackedViews,
@NonNull final Map<View, TimestampWrapper<ImpressionInterface>> pollingViews,
@NonNull final VisibilityChecker visibilityChecker,
@NonNull final VisibilityTracker visibilityTracker,
@NonNull final Handler handler) {
@@ -64,23 +69,23 @@
@Override
public void onVisibilityChanged(@NonNull final List<View> visibleViews, @NonNull final List<View> invisibleViews) {
for (final View view : visibleViews) {
// It's possible for native response to be null if the view was GC'd from this class
// It's possible for native ad to be null if the view was GC'd from this class
// but not from VisibilityTracker
// If it's null then clean up the view from this class
final NativeResponse nativeResponse = mTrackedViews.get(view);
if (nativeResponse == null) {
final ImpressionInterface impressionInterface = mTrackedViews.get(view);
if (impressionInterface == null) {
removeView(view);
continue;
}

// If the native response is already polling, don't recreate it
final TimestampWrapper<NativeResponse> polling = mPollingViews.get(view);
if (polling != null && nativeResponse.equals(polling.mInstance)) {
// If the native ad is already polling, don't recreate it
final TimestampWrapper<ImpressionInterface> polling = mPollingViews.get(view);
if (polling != null && impressionInterface.equals(polling.mInstance)) {
continue;
}

// Add a new polling view
mPollingViews.put(view, new TimestampWrapper<NativeResponse>(nativeResponse));
mPollingViews.put(view, new TimestampWrapper<ImpressionInterface>(impressionInterface));
}

for (final View view : invisibleViews) {
@@ -98,24 +103,24 @@ public void onVisibilityChanged(@NonNull final List<View> visibleViews, @NonNull
/**
* Tracks the given view for impressions.
*/
void addView(final View view, @NonNull final NativeResponse nativeResponse) {
// View is already associated with same native response
if (mTrackedViews.get(view) == nativeResponse) {
public void addView(final View view, @NonNull final ImpressionInterface impressionInterface) {
// View is already associated with the same native ad
if (mTrackedViews.get(view) == impressionInterface) {
return;
}

// Clean up state if view is being recycled and associated with a different response
// Clean up state if view is being recycled and associated with a different ad
removeView(view);

if (nativeResponse.getRecordedImpression() || nativeResponse.isDestroyed()) {
if (impressionInterface.isImpressionRecorded()) {
return;
}

mTrackedViews.put(view, nativeResponse);
mVisibilityTracker.addView(view, nativeResponse.getImpressionMinPercentageViewed());
mTrackedViews.put(view, impressionInterface);
mVisibilityTracker.addView(view, impressionInterface.getImpressionMinPercentageViewed());
}

void removeView(final View view) {
public void removeView(final View view) {
mTrackedViews.remove(view);
removePollingView(view);
mVisibilityTracker.removeView(view);
@@ -124,14 +129,14 @@ void removeView(final View view) {
/**
* Immediately clear all views. Useful for when we re-request ads for an ad placer
*/
void clear() {
public void clear() {
mTrackedViews.clear();
mPollingViews.clear();
mVisibilityTracker.clear();
mPollHandler.removeMessages(0);
}

void destroy() {
public void destroy() {
clear();
mVisibilityTracker.destroy();
mVisibilityTrackerListener = null;
@@ -163,9 +168,9 @@ private void removePollingView(final View view) {

@Override
public void run() {
for (final Map.Entry<View, TimestampWrapper<NativeResponse>> entry : mPollingViews.entrySet()) {
for (final Map.Entry<View, TimestampWrapper<ImpressionInterface>> entry : mPollingViews.entrySet()) {
final View view = entry.getKey();
final TimestampWrapper<NativeResponse> timestampWrapper = entry.getValue();
final TimestampWrapper<ImpressionInterface> timestampWrapper = entry.getValue();

// If it's been visible for the min impression time, trigger the callback
if (!mVisibilityChecker.hasRequiredTimeElapsed(
@@ -175,6 +180,7 @@ public void run() {
}

timestampWrapper.mInstance.recordImpression(view);
timestampWrapper.mInstance.setImpressionRecorded();

// Removed in a separate loop to avoid a ConcurrentModification exception.
mRemovedViews.add(view);
@@ -0,0 +1,66 @@
package com.mopub.nativeads;

/**
* Stores an integer interval in the form of a start and a length.
*/
public class IntInterval {
private int start;
private int length;

public IntInterval(int start, int length) {
this.start = start;
this.length = length;
}

public int getStart() {
return start;
}

public int getLength() {
return length;
}

public void setStart(int start) {
this.start = start;
}

public void setLength(int length) {
this.length = length;
}

/**
* For comparing intervals directly to this object.
* @param start The start of the interval
* @param length The length of the interval
* @return Whether or not the given interval and this object have the same values.
*/
public boolean equals(int start, int length) {
return this.start == start && this.length == length;
}

@Override
public String toString() {
return "{start : " + start + ", length : " + length + "}";
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof IntInterval)) {
return false;
}

final IntInterval other = (IntInterval) o;
return this.start == other.start && this.length == other.length;
}

@Override
public int hashCode() {
int result = 29;
result = 31 * result + start;
result = 31 * result + length;
return result;
}
}
@@ -0,0 +1,395 @@
package com.mopub.nativeads;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Color;
import android.graphics.SurfaceTexture;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.TextureView;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;

import com.mopub.common.Preconditions;
import com.mopub.common.logging.MoPubLog;
import com.mopub.common.util.Dips;
import com.mopub.common.util.Drawables;
import com.mopub.common.util.Utils;
import com.mopub.mobileads.VastVideoProgressBarWidget;
import com.mopub.mobileads.resource.DrawableConstants.GradientStrip;

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class MediaLayout extends RelativeLayout {
public enum Mode { IMAGE, PLAYING, LOADING, BUFFERING, PAUSED, FINISHED }
public enum MuteState { MUTED, UNMUTED }

private static final int GRADIENT_STRIP_HEIGHT_DIPS = 35;
private static final int MUTE_SIZE_DIPS = 36;
private static final int CONTROL_SIZE_DIPS = 40;
private static final int PINNER_PADDING_DIPS = 10;

private static final float ASPECT_MULTIPLIER_WIDTH_TO_HEIGHT = 9f / 16;
private static final float ASPECT_MULTIPLIER_HEIGHT_TO_WIDTH = 16f / 9;

@NonNull private volatile Mode mMode = Mode.IMAGE;
@NonNull private MuteState mMuteState;

@NonNull private ImageView mMainImageView;

// These views are video-only, ordered by their z index. Don't create them if they aren't needed.
@Nullable private TextureView mVideoTextureView;
@Nullable private ProgressBar mLoadingSpinner;
@Nullable private ImageView mPlayButton;
@Nullable private ImageView mBottomGradient;
@Nullable private ImageView mTopGradient;
@Nullable private VastVideoProgressBarWidget mVideoProgress;
@Nullable private ImageView mMuteControl;
@Nullable private View mOverlay;
@Nullable private Drawable mMutedDrawable;
@Nullable private Drawable mUnmutedDrawable;

private boolean mIsInitialized;

// Measurements
private final int mControlSizePx;
private final int mGradientStripHeightPx;
private final int mMuteSizePx;
private final int mPaddingPx;

// Constructors
public MediaLayout(@NonNull final Context context) {
this(context, null);
}

public MediaLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) {
this(context, attrs, 0);
}

public MediaLayout(@NonNull final Context context, @Nullable final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
Preconditions.checkNotNull(context);

mMuteState = MuteState.MUTED;

// Create and layout the main imageView and set its modes.
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
params.addRule(RelativeLayout.CENTER_IN_PARENT);
mMainImageView = new ImageView(context);
mMainImageView.setLayoutParams(params);
mMainImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
addView(mMainImageView);

mControlSizePx = Dips.asIntPixels(CONTROL_SIZE_DIPS, context);
mGradientStripHeightPx = Dips.asIntPixels(GRADIENT_STRIP_HEIGHT_DIPS, context);
mMuteSizePx = Dips.asIntPixels(MUTE_SIZE_DIPS, context);
mPaddingPx = Dips.asIntPixels(PINNER_PADDING_DIPS, context);
}

public void setSurfaceTextureListener(@Nullable final TextureView.SurfaceTextureListener stl) {
if (mVideoTextureView != null) {
mVideoTextureView.setSurfaceTextureListener(stl);

SurfaceTexture st = mVideoTextureView.getSurfaceTexture();
if (st != null && stl != null) {
stl.onSurfaceTextureAvailable(st, mVideoTextureView.getWidth(), mVideoTextureView.getHeight());
}
}
}

/**
* Users should call this method when the view will be used for video. Video views are not
* instantiated in the image-only case in order to save time and memory.
*/
public void initForVideo() {
if (mIsInitialized) {
return;
}

// Init and set up all the video view items.
final LayoutParams videoTextureLayoutParams =
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
videoTextureLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
mVideoTextureView = new TextureView(getContext());
mVideoTextureView.setLayoutParams(videoTextureLayoutParams);
mVideoTextureView.setId((int) Utils.generateUniqueId());
addView(mVideoTextureView);

// Place texture beneath image.
mMainImageView.bringToFront();

final LayoutParams loadingSpinnerParams = new LayoutParams(mControlSizePx, mControlSizePx);
loadingSpinnerParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
loadingSpinnerParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
mLoadingSpinner = new ProgressBar(getContext());
mLoadingSpinner.setLayoutParams(loadingSpinnerParams);
mLoadingSpinner.setPadding(0, mPaddingPx, mPaddingPx, 0);
mLoadingSpinner.setIndeterminate(true);
addView(mLoadingSpinner);

final LayoutParams bottomGradientParams =
new LayoutParams(LayoutParams.MATCH_PARENT, mGradientStripHeightPx);
bottomGradientParams.addRule(RelativeLayout.ALIGN_BOTTOM, mVideoTextureView.getId());
mBottomGradient = new ImageView(getContext());
mBottomGradient.setLayoutParams(bottomGradientParams);
final GradientDrawable bottomGradientDrawable =
new GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP,
new int[] {GradientStrip.START_COLOR, GradientStrip.END_COLOR});
mBottomGradient.setImageDrawable(bottomGradientDrawable);
addView(mBottomGradient);

final LayoutParams topGradientParams =
new LayoutParams(LayoutParams.MATCH_PARENT, mGradientStripHeightPx);
topGradientParams.addRule(RelativeLayout.ALIGN_TOP, mVideoTextureView.getId());
mTopGradient = new ImageView(getContext());
mTopGradient.setLayoutParams(topGradientParams);
final GradientDrawable topGradientDrawable =
new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
new int[] {GradientStrip.START_COLOR, GradientStrip.END_COLOR});
mTopGradient.setImageDrawable(topGradientDrawable);
addView(mTopGradient);

mVideoProgress = new VastVideoProgressBarWidget(getContext());
mVideoProgress.setAnchorId(mVideoTextureView.getId());
mVideoProgress.calibrateAndMakeVisible(1000, 0);
addView(mVideoProgress);

mMutedDrawable = Drawables.NATIVE_MUTED.createDrawable(getContext());
mUnmutedDrawable = Drawables.NATIVE_UNMUTED.createDrawable(getContext());
final LayoutParams muteControlParams = new LayoutParams(mMuteSizePx, mMuteSizePx);
muteControlParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
muteControlParams.addRule(RelativeLayout.ABOVE, mVideoProgress.getId());
mMuteControl = new ImageView(getContext());
mMuteControl.setLayoutParams(muteControlParams);
mMuteControl.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
mMuteControl.setPadding(mPaddingPx, mPaddingPx, mPaddingPx, mPaddingPx);
mMuteControl.setImageDrawable(mMutedDrawable);
addView(mMuteControl);

final LayoutParams overlayParams =
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
overlayParams.addRule(RelativeLayout.CENTER_IN_PARENT);
mOverlay = new View(getContext());
mOverlay.setLayoutParams(overlayParams);
mOverlay.setBackgroundColor(Color.TRANSPARENT);
addView(mOverlay);

final LayoutParams playButtonParams = new LayoutParams(mControlSizePx, mControlSizePx);
playButtonParams.addRule(RelativeLayout.CENTER_IN_PARENT);
mPlayButton = new ImageView(getContext());
mPlayButton.setLayoutParams(playButtonParams);
mPlayButton.setImageDrawable(Drawables.NATIVE_PLAY.createDrawable(getContext()));
addView(mPlayButton);

mIsInitialized = true;
updateViewState();
}

/**
* Resets the view, removing all the OnClickListeners and setting the view to hide
*/
public void reset() {
setMode(Mode.IMAGE);
setPlayButtonClickListener(null);
setMuteControlClickListener(null);
setVideoClickListener(null);
}

@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

final int measWidth = MeasureSpec.getSize(widthMeasureSpec);
final int measHeight = MeasureSpec.getSize(heightMeasureSpec);

final int curWidth = getMeasuredWidth();
final int curHeight = getMeasuredHeight();

int finalWidth;
if (widthMode == MeasureSpec.EXACTLY) {
finalWidth = measWidth;
} else if (widthMode == MeasureSpec.AT_MOST) {
// Cap width at max width.
finalWidth = Math.min(measWidth, curWidth);
} else {
// MeasWidth is meaningless. Stay with current width.
finalWidth = curWidth;
}

// Set height based on width + height constraints.
int finalHeight = (int) (ASPECT_MULTIPLIER_WIDTH_TO_HEIGHT * finalWidth);

// Check if the layout is giving us bounds smaller than we want, conform to those if needed.
if (heightMode == MeasureSpec.EXACTLY && measHeight < finalHeight) {
finalHeight = measHeight;
finalWidth = (int) (ASPECT_MULTIPLIER_HEIGHT_TO_WIDTH * finalHeight);
}

if (Math.abs(finalHeight - curHeight) >= 2
|| Math.abs(finalWidth - curWidth) >= 2) {
MoPubLog.v(String.format("Resetting mediaLayout size to w: %d h: %d", finalWidth, finalHeight));
getLayoutParams().width = finalWidth;
getLayoutParams().height = finalHeight;
}

super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

public void setMainImageDrawable(@NonNull Drawable drawable) {
Preconditions.checkNotNull(drawable);
mMainImageView.setImageDrawable(drawable);
}

public void resetProgress() {
if (mVideoProgress != null) {
mVideoProgress.reset();
}
}

public void updateProgress(final int progressTenthPercentage) {
if (mVideoProgress != null) {
mVideoProgress.updateProgress(progressTenthPercentage);
}
}

public TextureView getTextureView() {
return mVideoTextureView;
}

public void setMode(@NonNull final Mode mode) {
Preconditions.checkNotNull(mode);
mMode = mode;
post(new Runnable() {
@Override
public void run() {
updateViewState();
}
});
}

@Nullable
public ImageView getMainImageView() {
return mMainImageView;
}

public void setMuteControlClickListener(@Nullable OnClickListener muteControlListener) {
if (mMuteControl != null) {
mMuteControl.setOnClickListener(muteControlListener);
}
}

public void setPlayButtonClickListener(@Nullable OnClickListener playButtonListener) {
if (mPlayButton != null && mOverlay != null) {
mOverlay.setOnClickListener(playButtonListener);
mPlayButton.setOnClickListener(playButtonListener);
}
}

public void setVideoClickListener(@Nullable OnClickListener videoClickListener) {
if (mVideoTextureView != null) {
mVideoTextureView.setOnClickListener(videoClickListener);
}
}

public void setMuteState(@NonNull final MuteState muteState) {
Preconditions.checkNotNull(muteState);
if (muteState == mMuteState) {
return;
}
mMuteState = muteState;
if (mMuteControl != null) {
switch (mMuteState) {
case MUTED:
mMuteControl.setImageDrawable(mMutedDrawable);
break;
case UNMUTED:
default:
mMuteControl.setImageDrawable(mUnmutedDrawable);
}
}
}

private void updateViewState() {
switch (mMode) {
case IMAGE:
setMainImageVisibility(VISIBLE);
setLoadingSpinnerVisibility(INVISIBLE);
setVideoControlVisibility(INVISIBLE);
setPlayButtonVisibility(INVISIBLE);
break;
case LOADING:
setMainImageVisibility(VISIBLE);
setLoadingSpinnerVisibility(VISIBLE);
setVideoControlVisibility(INVISIBLE);
setPlayButtonVisibility(INVISIBLE);
break;
case BUFFERING:
setMainImageVisibility(INVISIBLE);
setLoadingSpinnerVisibility(VISIBLE);
setVideoControlVisibility(VISIBLE);
setPlayButtonVisibility(INVISIBLE);
case PLAYING:
setMainImageVisibility(INVISIBLE);
setLoadingSpinnerVisibility(INVISIBLE);
setVideoControlVisibility(VISIBLE);
setPlayButtonVisibility(INVISIBLE);
break;
case PAUSED:
setMainImageVisibility(INVISIBLE);
setLoadingSpinnerVisibility(INVISIBLE);
setVideoControlVisibility(VISIBLE);
setPlayButtonVisibility(VISIBLE);
break;
case FINISHED:
setMainImageVisibility(VISIBLE);
setLoadingSpinnerVisibility(INVISIBLE);
setVideoControlVisibility(INVISIBLE);
setPlayButtonVisibility(VISIBLE);
break;
default:
break;
}
}

private void setMainImageVisibility(final int visibility) {
mMainImageView.setVisibility(visibility);
}

private void setLoadingSpinnerVisibility(final int visibility) {
if (mLoadingSpinner != null) {
mLoadingSpinner.setVisibility(visibility);
}

if (mTopGradient != null) {
mTopGradient.setVisibility(visibility);
}
}

private void setVideoControlVisibility(final int visibility) {
if (mBottomGradient != null) {
mBottomGradient.setVisibility(visibility);
}

if (mVideoProgress != null) {
mVideoProgress.setVisibility(visibility);
}

if (mMuteControl != null) {
mMuteControl.setVisibility(visibility);
}
}

private void setPlayButtonVisibility(final int visibility) {
if (mPlayButton != null && mOverlay != null) {
mPlayButton.setVisibility(visibility);
mOverlay.setVisibility(visibility);
}
}
}
@@ -0,0 +1,99 @@
package com.mopub.nativeads;

import android.support.annotation.NonNull;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class MediaViewBinder {
public final static class Builder {
private final int layoutId;
private int mediaLayoutId;
private int titleId;
private int textId;
private int iconImageId;
private int callToActionId;
private int privacyInformationIconImageId;

@NonNull private Map<String, Integer> extras = Collections.emptyMap();

public Builder(final int layoutId) {
this.layoutId = layoutId;
this.extras = new HashMap<String, Integer>();
}

@NonNull
public final Builder mediaLayoutId(final int mediaLayoutId) {
this.mediaLayoutId = mediaLayoutId;
return this;
}

@NonNull
public final Builder titleId(final int titlteId) {
this.titleId = titlteId;
return this;
}

@NonNull
public final Builder textId(final int textId) {
this.textId = textId;
return this;
}

@NonNull
public final Builder iconImageId(final int iconImageId) {
this.iconImageId = iconImageId;
return this;
}

@NonNull
public final Builder callToActionId(final int callToActionId) {
this.callToActionId = callToActionId;
return this;
}

@NonNull
public final Builder privacyInformationIconImageId(final int privacyInformationIconImageId) {
this.privacyInformationIconImageId = privacyInformationIconImageId;
return this;
}

@NonNull
public final Builder addExtras(final Map<String, Integer> resourceIds) {
this.extras = new HashMap<String, Integer>(resourceIds);
return this;
}

@NonNull
public final Builder addExtra(final String key, final int resourceId) {
this.extras.put(key, resourceId);
return this;
}

@NonNull
public final MediaViewBinder build() {
return new MediaViewBinder(this);
}
}

final int layoutId;
final int mediaLayoutId;
final int titleId;
final int textId;
final int callToActionId;
final int iconImageId;
final int privacyInformationIconImageId;
@NonNull final Map<String, Integer> extras;

private MediaViewBinder(@NonNull final Builder builder) {
this.layoutId = builder.layoutId;
this.mediaLayoutId = builder.mediaLayoutId;
this.titleId = builder.titleId;
this.textId = builder.textId;
this.callToActionId = builder.callToActionId;
this.iconImageId = builder.iconImageId;
this.privacyInformationIconImageId = builder.privacyInformationIconImageId;
this.extras = builder.extras;
}
}
@@ -0,0 +1,49 @@
package com.mopub.nativeads;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import com.mopub.common.VisibleForTesting;
import com.mopub.common.logging.MoPubLog;

class MediaViewHolder {
@Nullable View mainView;
@Nullable MediaLayout mediaLayout;
@Nullable TextView titleView;
@Nullable TextView textView;
@Nullable ImageView iconImageView;
@Nullable TextView callToActionView;
@Nullable ImageView privacyInformationIconImageView;

@VisibleForTesting
static final MediaViewHolder EMPTY_MEDIA_VIEW_HOLDER = new MediaViewHolder();

// Use fromViewBinder instead of a constructor
private MediaViewHolder() {}

@NonNull
static MediaViewHolder fromViewBinder(@NonNull final View view,
@NonNull final MediaViewBinder mediaViewBinder) {
final MediaViewHolder mediaViewHolder = new MediaViewHolder();
mediaViewHolder.mainView = view;
try {
mediaViewHolder.titleView = (TextView) view.findViewById(mediaViewBinder.titleId);
mediaViewHolder.textView = (TextView) view.findViewById(mediaViewBinder.textId);
mediaViewHolder.callToActionView =
(TextView) view.findViewById(mediaViewBinder.callToActionId);
mediaViewHolder.mediaLayout = (MediaLayout) view.findViewById(mediaViewBinder.mediaLayoutId);
mediaViewHolder.iconImageView =
(ImageView) view.findViewById(mediaViewBinder.iconImageId);
mediaViewHolder.privacyInformationIconImageView =
(ImageView) view.findViewById(mediaViewBinder.privacyInformationIconImageId);
return mediaViewHolder;
} catch (ClassCastException exception) {
MoPubLog.w("Could not cast from id in MediaViewBinder to expected View type",
exception);
return EMPTY_MEDIA_VIEW_HOLDER;
}
}
}
@@ -150,17 +150,14 @@ void handleAdRemoved(final int position) {
}

/**
* Registers a {@link MoPubNativeAdRenderer} to use when displaying ads in your stream.
* Registers a {@link MoPubStaticNativeAdRenderer}, supporting a specific native ad format,
* to use when displaying ads in your stream. Note that if multiple ad renderers support
* a specific native ad format, the first one registered will be used.
*
* 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.
*
* @deprecated in version 3.9.0, use {@link #registerViewBinder(ViewBinder)} instead.
*/
@Deprecated
public final void registerAdRenderer(@NonNull final MoPubAdRenderer adRenderer) {
if (!Preconditions.NoThrow.checkNotNull(
adRenderer, "Tried to set a null ad renderer on the placer.")) {
@@ -169,23 +166,6 @@ public final void registerAdRenderer(@NonNull final MoPubAdRenderer adRenderer)
mStreamAdPlacer.registerAdRenderer(adRenderer);
}

/**
* Registers a {@link ViewBinder} to use when displaying ads in your stream.
*
* This binder will be used automatically to create and render your view when you call
* {@link #getView}. If you register a second {@link ViewBinder}, it will replace the first,
* although this behavior is subject to change in a future SDK version.
*
* @param viewBinder The view binder
*/
public final void registerViewBinder(@NonNull final ViewBinder viewBinder) {
if (!Preconditions.NoThrow.checkNotNull(
viewBinder, "Tried to set a null view binder on the placer.")) {
return;
}
mStreamAdPlacer.registerAdRenderer(new MoPubNativeAdRenderer(viewBinder));
}

/**
* Sets a listener that will be called after the SDK loads new ads from the server and places
* them into your stream.
@@ -9,14 +9,14 @@
/**
* An interface for creating ad views and rendering them using ad data.
*
* Normally you will use the subclass {@link com.mopub.nativeads.MoPubNativeAdRenderer} with {@link
* Normally you will use the subclass {@link MoPubStaticNativeAdRenderer} with {@link
* com.mopub.nativeads.ViewBinder} to customize your ad view with your own layout. However, if you
* wish to programmatically create or manage your ad view, you can implement {@code MoPubAdRenderer}
* directly.
*
* @param <T> The ad payload type.
*/
public interface MoPubAdRenderer<T> {
public interface MoPubAdRenderer<T extends BaseNativeAd> {
/**
* Creates a new view to be used as an ad.
*
@@ -39,4 +39,12 @@
* @param ad The ad data that should be bound to the view.
*/
void renderAdView(@NonNull View view, @NonNull T ad);

/**
* Determines if this renderer supports the type of native ad passed in.
*
* @param nativeAd The native ad to render.
* @return True if the renderer can render the native ad and false if it cannot.
*/
boolean supports(@NonNull BaseNativeAd nativeAd);
}
@@ -3,9 +3,11 @@
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;

import com.mopub.common.VisibleForTesting;
import com.mopub.common.logging.MoPubLog;
import com.mopub.nativeads.NativeImageHelper.ImageListener;

import org.json.JSONArray;
import org.json.JSONException;
@@ -21,7 +23,7 @@

import static com.mopub.common.DataKeys.JSON_BODY_KEY;
import static com.mopub.common.util.Numbers.parseDouble;
import static com.mopub.nativeads.NativeResponse.Parameter;
import static com.mopub.nativeads.NativeImageHelper.preCacheImages;

public class MoPubCustomEventNative extends CustomEventNative {

@@ -34,36 +36,88 @@ protected void loadNativeAd(@NonNull final Context context,
Object json = localExtras.get(JSON_BODY_KEY);
// null or non-JSONObjects should not be passed in localExtras as JSON_BODY_KEY
if (!(json instanceof JSONObject)) {
customEventNativeListener.onNativeAdFailed(NativeErrorCode.INVALID_JSON);
customEventNativeListener.onNativeAdFailed(NativeErrorCode.INVALID_RESPONSE);
return;
}

final MoPubForwardingNativeAd moPubForwardingNativeAd =
new MoPubForwardingNativeAd(context.getApplicationContext(),
final MoPubStaticNativeAd moPubStaticNativeAd =
new MoPubStaticNativeAd(context,
(JSONObject) json,
new ImpressionTracker(context),
new NativeClickHandler(context),
customEventNativeListener);

try {
moPubForwardingNativeAd.loadAd();
moPubStaticNativeAd.loadAd();
} catch (IllegalArgumentException e) {
customEventNativeListener.onNativeAdFailed(NativeErrorCode.UNSPECIFIED);
}
}

static class MoPubForwardingNativeAd extends BaseForwardingNativeAd {
static class MoPubStaticNativeAd extends StaticNativeAd {
enum Parameter {
IMPRESSION_TRACKER("imptracker", true),
CLICK_TRACKER("clktracker", true),

TITLE("title", false),
TEXT("text", false),
MAIN_IMAGE("mainimage", false),
ICON_IMAGE("iconimage", false),

CLICK_DESTINATION("clk", false),
FALLBACK("fallback", false),
CALL_TO_ACTION("ctatext", false),
STAR_RATING("starrating", false);

@NonNull final String name;
final boolean required;

Parameter(@NonNull final String name, final boolean required) {
this.name = name;
this.required = required;
}

@Nullable
static Parameter from(@NonNull final String name) {
for (final Parameter parameter : values()) {
if (parameter.name.equals(name)) {
return parameter;
}
}

return null;
}

@NonNull
@VisibleForTesting
static final Set<String> requiredKeys = new HashSet<String>();
static {
for (final Parameter parameter : values()) {
if (parameter.required) {
requiredKeys.add(parameter.name);
}
}
}
}

@VisibleForTesting
static final String DAA_CLICKTHROUGH_URL = "https://www.mopub.com/optout";
static final String PRIVACY_INFORMATION_CLICKTHROUGH_URL = "https://www.mopub.com/optout";

@NonNull private final Context mContext;
@NonNull private final CustomEventNativeListener mCustomEventNativeListener;
@NonNull private final JSONObject mJsonObject;
@NonNull private final ImpressionTracker mImpressionTracker;
@NonNull private final NativeClickHandler mNativeClickHandler;

MoPubForwardingNativeAd(@NonNull final Context context,
MoPubStaticNativeAd(@NonNull final Context context,
@NonNull final JSONObject jsonBody,
@NonNull final ImpressionTracker impressionTracker,
@NonNull final NativeClickHandler nativeClickHandler,
@NonNull final CustomEventNativeListener customEventNativeListener) {
mJsonObject = jsonBody;
mContext = context;
mContext = context.getApplicationContext();
mImpressionTracker = impressionTracker;
mNativeClickHandler = nativeClickHandler;
mCustomEventNativeListener = customEventNativeListener;
}

@@ -87,11 +141,12 @@ void loadAd() throws IllegalArgumentException {
addExtra(key, mJsonObject.opt(key));
}
}
setPrivacyInformationIconClickThroughUrl(PRIVACY_INFORMATION_CLICKTHROUGH_URL);

preCacheImages(mContext, getAllImageUrls(), new ImageListener() {
@Override
public void onImagesCached() {
mCustomEventNativeListener.onNativeAdLoaded(MoPubForwardingNativeAd.this);
mCustomEventNativeListener.onNativeAdLoaded(MoPubStaticNativeAd.this);
}

@Override
@@ -124,8 +179,6 @@ private void addInstanceVariable(@NonNull final Parameter key,
case IMPRESSION_TRACKER:
addImpressionTrackers(value);
break;
case CLICK_TRACKER:
break;
case CLICK_DESTINATION:
setClickDestinationUrl((String) value);
break;
@@ -200,9 +253,34 @@ private boolean isImageKey(@Nullable final String name) {
return imageUrls;
}

// Lifecycle Handlers
@Override
public void prepare(@NonNull final View view) {
mImpressionTracker.addView(view, this);
mNativeClickHandler.setOnClickListener(view, this);
}

@Override
public void clear(@NonNull final View view) {
mImpressionTracker.removeView(view);
mNativeClickHandler.clearOnClickListener(view);
}

@Override
public void destroy() {
mImpressionTracker.destroy();
}

// Event Handlers
@Override
public void recordImpression(@NonNull final View view) {
notifyAdImpressed();
}

@Override
public String getDaaIconClickthroughUrl() {
return DAA_CLICKTHROUGH_URL;
public void handleClick(@Nullable final View view) {
notifyAdClicked();
mNativeClickHandler.openClickDestinationUrl(getClickDestinationUrl(), view);
}
}
}

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -3,10 +3,10 @@
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;

import com.mopub.common.AdFormat;
import com.mopub.common.Constants;
import com.mopub.common.DataKeys;
import com.mopub.common.Preconditions;
import com.mopub.common.VisibleForTesting;
import com.mopub.common.logging.MoPubLog;
@@ -29,91 +29,65 @@
import static com.mopub.nativeads.CustomEventNative.CustomEventNativeListener;
import static com.mopub.nativeads.NativeErrorCode.CONNECTION_ERROR;
import static com.mopub.nativeads.NativeErrorCode.EMPTY_AD_RESPONSE;
import static com.mopub.nativeads.NativeErrorCode.INVALID_JSON;
import static com.mopub.nativeads.NativeErrorCode.INVALID_RESPONSE;
import static com.mopub.nativeads.NativeErrorCode.INVALID_REQUEST_URL;
import static com.mopub.nativeads.NativeErrorCode.NATIVE_RENDERER_CONFIGURATION_ERROR;
import static com.mopub.nativeads.NativeErrorCode.SERVER_ERROR_RESPONSE_CODE;
import static com.mopub.nativeads.NativeErrorCode.UNSPECIFIED;

public class MoPubNative {

public interface MoPubNativeNetworkListener {
public void onNativeLoad(final NativeResponse nativeResponse);
public void onNativeFail(final NativeErrorCode errorCode);
void onNativeLoad(final NativeAd nativeAd);
void onNativeFail(final NativeErrorCode errorCode);
}

static final MoPubNativeNetworkListener EMPTY_NETWORK_LISTENER = new
MoPubNativeNetworkListener() {
static final MoPubNativeNetworkListener EMPTY_NETWORK_LISTENER =
new MoPubNativeNetworkListener() {
@Override
public void onNativeLoad(@NonNull final NativeResponse nativeResponse) {
public void onNativeLoad(@NonNull final NativeAd nativeAd) {
// If this listener is invoked, it means that MoPubNative instance has been destroyed
// so destroy any leftover incoming NativeResponses
nativeResponse.destroy();
// so destroy any leftover incoming NativeAds
nativeAd.destroy();
}
@Override
public void onNativeFail(final NativeErrorCode errorCode) {
}
};

static final MoPubNativeEventListener EMPTY_EVENT_LISTENER = new
MoPubNativeEventListener() {
@Override
public void onNativeImpression(@Nullable final View view) {
}
@Override
public void onNativeClick(@Nullable final View view) {
}
};

public interface MoPubNativeEventListener {
public void onNativeImpression(final View view);
public void onNativeClick(final View view);
}

/**
* @deprecated As of release 2.4, use {@link MoPubNativeEventListener} and
* {@link MoPubNativeNetworkListener} instead.
*/
@Deprecated
public interface MoPubNativeListener extends MoPubNativeNetworkListener, MoPubNativeEventListener {
}

// must be an activity context since 3rd party networks need it
@NonNull private final WeakReference<Context> mContext;
@NonNull private final String mAdUnitId;
@NonNull private MoPubNativeNetworkListener mMoPubNativeNetworkListener;
@NonNull private MoPubNativeEventListener mMoPubNativeEventListener;

// For small sets TreeMap, takes up less memory than HashMap
@NonNull private Map<String, Object> mLocalExtras = new TreeMap<String, Object>();
@NonNull private final AdRequest.Listener mVolleyListener;
@Nullable private AdRequest mNativeRequest;
@NonNull AdRendererRegistry mAdRendererRegistry;

/**
* @deprecated As of release 2.4, use {@link MoPubNative(Context, String,
* MoPubNativeNetworkListener)} and {@link #setNativeEventListener(MoPubNativeEventListener)}
* instead.
*/
@Deprecated
public MoPubNative(@NonNull final Context context,
@NonNull final String adUnitId,
@NonNull final MoPubNativeListener moPubNativeListener) {
this(context, adUnitId, (MoPubNativeNetworkListener) moPubNativeListener);
setNativeEventListener(moPubNativeListener);
@NonNull final MoPubNativeNetworkListener moPubNativeNetworkListener) {
this(context, adUnitId, new AdRendererRegistry(), moPubNativeNetworkListener);
}

@VisibleForTesting
public MoPubNative(@NonNull final Context context,
@NonNull final String adUnitId,
@NonNull final MoPubNativeNetworkListener moPubNativeNetworkListener) {
@NonNull final String adUnitId,
@NonNull AdRendererRegistry adRendererRegistry,
@NonNull final MoPubNativeNetworkListener moPubNativeNetworkListener) {
Preconditions.checkNotNull(context, "Context may not be null.");
Preconditions.checkNotNull(adUnitId, "AdUnitId may not be null.");
Preconditions.checkNotNull(adRendererRegistry, "AdRendererRegistry may not be null.");
Preconditions.checkNotNull(moPubNativeNetworkListener, "MoPubNativeNetworkListener may not be null.");

ManifestUtils.checkNativeActivitiesDeclared(context);

mContext = new WeakReference<Context>(context);
mAdUnitId = adUnitId;
mMoPubNativeNetworkListener = moPubNativeNetworkListener;
mMoPubNativeEventListener = EMPTY_EVENT_LISTENER;
mAdRendererRegistry = adRendererRegistry;
mVolleyListener = new AdRequest.Listener() {
@Override
public void onSuccess(@NonNull final AdResponse response) {
@@ -130,9 +104,13 @@ public void onErrorResponse(@NonNull final VolleyError volleyError) {
fetchAdvertisingInfoAsync(context, null);
}

public void setNativeEventListener(@Nullable final MoPubNativeEventListener nativeEventListener) {
mMoPubNativeEventListener = (nativeEventListener == null)
? EMPTY_EVENT_LISTENER : nativeEventListener;
/**
* Registers an ad renderer for rendering a specific native ad format.
* Note that if multiple ad renderers support a specific native ad format, the first
* one registered will be used.
*/
public void registerAdRenderer(MoPubAdRenderer moPubAdRenderer) {
mAdRendererRegistry.registerAdRenderer(moPubAdRenderer);
}

public void destroy() {
@@ -142,7 +120,6 @@ public void destroy() {
mNativeRequest = null;
}
mMoPubNativeNetworkListener = EMPTY_NETWORK_LISTENER;
mMoPubNativeEventListener = EMPTY_EVENT_LISTENER;
}

public void setLocalExtras(@Nullable final Map<String, Object> localExtras) {
@@ -225,17 +202,25 @@ private void onAdLoad(@NonNull final AdResponse response) {
final CustomEventNativeListener customEventNativeListener =
new CustomEventNativeListener() {
@Override
public void onNativeAdLoaded(@NonNull final NativeAdInterface nativeAd) {
public void onNativeAdLoaded(@NonNull final BaseNativeAd nativeAd) {
final Context context = getContextOrDestroy();
if (context == null) {
return;
}
mMoPubNativeNetworkListener.onNativeLoad(new NativeResponse(context,
response.getImpressionTrackingUrl(),
response.getClickTrackingUrl(),
mAdUnitId,
nativeAd,
mMoPubNativeEventListener));

MoPubAdRenderer renderer = mAdRendererRegistry.getRendererForAd(nativeAd);
if (renderer == null) {
onNativeAdFailed(NATIVE_RENDERER_CONFIGURATION_ERROR);
return;
}

mMoPubNativeNetworkListener.onNativeLoad(new NativeAd(context,
response.getImpressionTrackingUrl(),
response.getClickTrackingUrl(),
mAdUnitId,
nativeAd,
renderer)
);
}

@Override
@@ -244,8 +229,6 @@ public void onNativeAdFailed(final NativeErrorCode errorCode) {
}
};



CustomEventNativeAdapter.loadNativeAd(
context,
mLocalExtras,
@@ -261,10 +244,10 @@ void onAdError(@NonNull final VolleyError volleyError) {
MoPubNetworkError error = (MoPubNetworkError) volleyError;
switch (error.getReason()) {
case BAD_BODY:
mMoPubNativeNetworkListener.onNativeFail(INVALID_JSON);
mMoPubNativeNetworkListener.onNativeFail(INVALID_RESPONSE);
return;
case BAD_HEADER_DATA:
mMoPubNativeNetworkListener.onNativeFail(INVALID_JSON);
mMoPubNativeNetworkListener.onNativeFail(INVALID_RESPONSE);
return;
case WARMING_UP:
// Used for the sample app to signal a toast.
@@ -304,17 +287,10 @@ Context getContextOrDestroy() {
return context;
}

@NonNull
@VisibleForTesting
@Deprecated
@NonNull
MoPubNativeNetworkListener getMoPubNativeNetworkListener() {
return mMoPubNativeNetworkListener;
}

@NonNull
@VisibleForTesting
@Deprecated
MoPubNativeEventListener getMoPubNativeEventListener() {
return mMoPubNativeEventListener;
}
}
@@ -134,49 +134,4 @@ public static MoPubClientPositioning clientPositioning() {
public static MoPubServerPositioning serverPositioning() {
return new MoPubServerPositioning();
}

/**
* Creates and returns a {@link MoPubNativeAdPositioning.Builder}.
*
* @return A new builder.
* @deprecated We recommend using {@link #serverPositioning()} and specifying positioning in
* the MoPub UI. If you still want to hard-code positioning information in your app,
* use {@link #clientPositioning} instead of this builder.
*/
@NonNull
@Deprecated
public static Builder newBuilder() {
return new Builder();
}

/**
* A Builder class for the ad positioning.
*/
@Deprecated
public static final class Builder extends MoPubClientPositioning {
@NonNull
@Override
public Builder addFixedPosition(final int position) {
super.addFixedPosition(position);
return this;
}

@NonNull
@Override
public Builder enableRepeatingPositions(final int interval) {
super.enableRepeatingPositions(interval);
return this;
}

/**
* Creates and returns a new immutable positioning object.
*
* @return A new positioning object.
*/
@NonNull
@Deprecated
public MoPubClientPositioning build() {
return this;
}
}
}

This file was deleted.

Oops, something went wrong.
@@ -186,10 +186,14 @@ public void setAdLoadedListener(@Nullable final MoPubNativeAdLoadedListener list
}

/**
* Registers a view binder for rendering "static" native ads with images.
* Registers an ad renderer for rendering a specific native ad format. Note that if multiple ad
* renderers support a specific native ad format, the first one registered will be used.
*/
public void registerViewBinder(@NonNull ViewBinder viewBinder) {
mStreamAdPlacer.registerAdRenderer(new MoPubNativeAdRenderer(viewBinder), NATIVE_AD_VIEW_TYPE_BASE + 1);
public void registerAdRenderer(@NonNull MoPubAdRenderer adRenderer) {
if (!Preconditions.NoThrow.checkNotNull(adRenderer, "Cannot register a null adRenderer")) {
return;
}
mStreamAdPlacer.registerAdRenderer(adRenderer);
}

/**
@@ -394,7 +398,7 @@ public int getItemCount() {
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
if (viewType >= NATIVE_AD_VIEW_TYPE_BASE && viewType <= NATIVE_AD_VIEW_TYPE_BASE + mStreamAdPlacer.getAdViewTypeCount()) {
// Create the view and a view holder.
final MoPubAdRenderer adRenderer = mStreamAdPlacer.getAdRendererForViewType(viewType);
final MoPubAdRenderer adRenderer = mStreamAdPlacer.getAdRendererForViewType(viewType - NATIVE_AD_VIEW_TYPE_BASE);
if (adRenderer == null) {
MoPubLog.w("No view binder was registered for ads in MoPubRecyclerAdapter.");
// This will cause a null pointer exception.
@@ -408,9 +412,9 @@ public int getItemCount() {

@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
Object adData = mStreamAdPlacer.getAdData(position);
if (adData != null) {
mStreamAdPlacer.bindAdView((NativeAdData) adData, holder.itemView);
Object adResponse = mStreamAdPlacer.getAdData(position);
if (adResponse != null) {
mStreamAdPlacer.bindAdView((NativeAd) adResponse, holder.itemView);
return;
}

@@ -0,0 +1,89 @@
package com.mopub.nativeads;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.mopub.common.Preconditions;
import com.mopub.common.VisibleForTesting;

import java.util.WeakHashMap;

import static android.view.View.VISIBLE;

/**
* An implementation of {@link com.mopub.nativeads.MoPubAdRenderer} for rendering native ads.
*/
public class MoPubStaticNativeAdRenderer implements MoPubAdRenderer<StaticNativeAd> {
@NonNull private final ViewBinder mViewBinder;

// This is used instead of View.setTag, which causes a memory leak in 2.3
// and earlier: https://code.google.com/p/android/issues/detail?id=18273
@VisibleForTesting @NonNull final WeakHashMap<View, StaticNativeViewHolder> mViewHolderMap;

/**
* Constructs a native ad renderer with a view binder.
*
* @param viewBinder The view binder to use when inflating and rendering an ad.
*/
public MoPubStaticNativeAdRenderer(@NonNull final ViewBinder viewBinder) {
mViewBinder = viewBinder;
mViewHolderMap = new WeakHashMap<View, StaticNativeViewHolder>();
}

@Override
@NonNull
public View createAdView(@NonNull final Context context, @Nullable final ViewGroup parent) {
return LayoutInflater
.from(context)
.inflate(mViewBinder.layoutId, parent, false);
}

@Override
public void renderAdView(@NonNull final View view,
@NonNull final StaticNativeAd staticNativeAd) {
StaticNativeViewHolder staticNativeViewHolder = mViewHolderMap.get(view);
if (staticNativeViewHolder == null) {
staticNativeViewHolder = StaticNativeViewHolder.fromViewBinder(view, mViewBinder);
mViewHolderMap.put(view, staticNativeViewHolder);
}

update(staticNativeViewHolder, staticNativeAd);
NativeRendererHelper.updateExtras(staticNativeViewHolder.mainView,
mViewBinder.extras,
staticNativeAd.getExtras());
setViewVisibility(staticNativeViewHolder, VISIBLE);
}

@Override
public boolean supports(@NonNull final BaseNativeAd nativeAd) {
Preconditions.checkNotNull(nativeAd);
return nativeAd instanceof StaticNativeAd;
}

private void update(@NonNull final StaticNativeViewHolder staticNativeViewHolder,
@NonNull final StaticNativeAd staticNativeAd) {
NativeRendererHelper.addTextView(staticNativeViewHolder.titleView,
staticNativeAd.getTitle());
NativeRendererHelper.addTextView(staticNativeViewHolder.textView, staticNativeAd.getText());
NativeRendererHelper.addTextView(staticNativeViewHolder.callToActionView,
staticNativeAd.getCallToAction());
NativeImageHelper.loadImageView(staticNativeAd.getMainImageUrl(),
staticNativeViewHolder.mainImageView);
NativeImageHelper.loadImageView(staticNativeAd.getIconImageUrl(),
staticNativeViewHolder.iconImageView);
NativeRendererHelper.addPrivacyInformationIcon(
staticNativeViewHolder.privacyInformationIconImageView,
staticNativeAd.getPrivacyInformationIconClickThroughUrl());
}

private void setViewVisibility(@NonNull final StaticNativeViewHolder staticNativeViewHolder,
final int visibility) {
if (staticNativeViewHolder.mainView != null) {
staticNativeViewHolder.mainView.setVisibility(visibility);
}
}
}
@@ -55,19 +55,16 @@ public void onAdRemoved(final int position) {
@NonNull private final Runnable mPlacementRunnable;
@NonNull private final PositioningSource mPositioningSource;
@NonNull private final NativeAdSource mAdSource;
@NonNull private final ImpressionTracker mImpressionTracker;

@NonNull private final HashMap<NativeResponse, WeakReference<View>> mViewMap;
@NonNull private final WeakHashMap<View, NativeResponse> mNativeResponseMap;
@NonNull private final HashMap<NativeAd, WeakReference<View>> mViewMap;
@NonNull private final WeakHashMap<View, NativeAd> mNativeAdMap;

private boolean mHasReceivedPositions;
@Nullable private PlacementData mPendingPlacementData;
private boolean mHasReceivedAds;
private boolean mHasPlacedAds;
@NonNull private PlacementData mPlacementData;

private int adViewType = DEFAULT_AD_VIEW_TYPE;
@Nullable private MoPubAdRenderer mAdRenderer;
@Nullable private String mAdUnitId;

@NonNull private MoPubNativeAdLoadedListener mAdLoadedListener =
@@ -82,7 +79,7 @@ public void onAdRemoved(final int position) {

private int mItemCount;
// A buffer around the visible range where we'll place ads if possible.
private static final int RANGE_BUFFER = 10;
private static final int RANGE_BUFFER = 6;
private boolean mNeedsPlacement;

/**
@@ -111,7 +108,6 @@ public MoPubStreamAdPlacer(@NonNull final Context context,
@NonNull final MoPubServerPositioning adPositioning) {
this(context,
new NativeAdSource(),
new ImpressionTracker(context),
new ServerPositioningSource(context));
}

@@ -128,29 +124,24 @@ public MoPubStreamAdPlacer(@NonNull final Context context,
// reference to it that might be subsequently modified by the caller.
this(context,
new NativeAdSource(),
new ImpressionTracker(context),
new ClientPositioningSource(adPositioning));
}

@VisibleForTesting
MoPubStreamAdPlacer(@NonNull final Context context,
@NonNull final NativeAdSource adSource,
@NonNull final ImpressionTracker impressionTracker,
@NonNull final PositioningSource positioningSource) {
Preconditions.checkNotNull(context, "context is not allowed to be null");
Preconditions.checkNotNull(adSource, "adSource is not allowed to be null");
Preconditions.checkNotNull(impressionTracker, "impressionTracker is not allowed to be " +
"null");
Preconditions.checkNotNull(positioningSource, "positioningSource is not allowed to be " +
"null");

mContext = context;
mImpressionTracker = impressionTracker;
mPositioningSource = positioningSource;
mAdSource = adSource;
mPlacementData = PlacementData.empty();

mNativeResponseMap = new WeakHashMap<>();
mNativeAdMap = new WeakHashMap<>();
mViewMap = new HashMap<>();

mPlacementHandler = new Handler();
@@ -170,33 +161,26 @@ public void run() {
}

/**
* Registers an ad renderer to use when displaying ads in your stream.
* Registers an ad renderer for rendering a specific native ad format in your stream.
* Note that if multiple ad renderers support a specific native ad format, the first
* one registered will be used.
*
* This renderer will automatically create and render your view when you call {@link
* #getAdView}. If you register a second renderer, it will replace the first, although this
* behavior is subject to change in a future SDK version.
* #getAdView}.
*
* @param adRenderer The ad renderer.
*/
public void registerAdRenderer(@NonNull final MoPubAdRenderer adRenderer) {
registerAdRenderer(adRenderer, -1);
}

public void registerAdRenderer(@NonNull final MoPubAdRenderer adRenderer, int viewType) {
if (!NoThrow.checkNotNull(adRenderer, "Cannot register a null adRenderer")) {
return;
}
adViewType = viewType;
mAdRenderer = adRenderer;

mAdSource.registerAdRenderer(adRenderer);
}

@Nullable
public MoPubAdRenderer getAdRendererForViewType(int viewType) {
if (viewType == adViewType) {
return mAdRenderer;
}

return null;
return mAdSource.getAdRendererForViewType(viewType);
}

/**
@@ -247,8 +231,9 @@ public void loadAds(@NonNull final String adUnitId,
return;
}

if (mAdRenderer == null) {
MoPubLog.w("You must call registerAdRenderer before loading ads");
if (mAdSource.getAdRendererCount() == 0) {
MoPubLog.w("You must register at least 1 ad renderer by calling registerAdRenderer " +
"before loading ads");
return;
}

@@ -384,7 +369,6 @@ public void clearAds() {
public void destroy() {
mPlacementHandler.removeMessages(0);
mAdSource.clear();
mImpressionTracker.destroy();
mPlacementData.clearAds();
}

@@ -417,35 +401,33 @@ public Object getAdData(final int position) {
@Nullable
public View getAdView(final int position, @Nullable final View convertView,
@Nullable final ViewGroup parent) {
final NativeAdData adData = mPlacementData.getPlacedAd(position);
if (adData == null) {
final NativeAd nativeAd = mPlacementData.getPlacedAd(position);
if (nativeAd == null) {
return null;
}

final View view = (convertView != null) ?
convertView : adData.getAdRenderer().createAdView(mContext, parent);
bindAdView(adData, view);
convertView : nativeAd.createAdView(parent);
bindAdView(nativeAd, view);
return view;
}

/**
* Given an ad and a view, attaches the ad data to the view and prepares the ad for display.
* @param adData the ad to bind.
* @param nativeAd the ad to bind.
* @param adView the view to bind it to.
*/
public void bindAdView(@NonNull NativeAdData adData, @NonNull View adView) {
NativeResponse nativeResponse = adData.getAd();
WeakReference<View> mappedViewRef = mViewMap.get(nativeResponse);
public void bindAdView(@NonNull NativeAd nativeAd, @NonNull View adView) {
WeakReference<View> mappedViewRef = mViewMap.get(nativeAd);
View mappedView = null;
if (mappedViewRef != null) {
mappedView = mappedViewRef.get();
}
if (!adView.equals(mappedView)) {
clearNativeResponse(mappedView);
clearNativeResponse(adView);
prepareNativeResponse(nativeResponse, adView);
//noinspection unchecked
adData.getAdRenderer().renderAdView(adView, nativeResponse);
clearNativeAd(mappedView);
clearNativeAd(adView);
prepareNativeAd(nativeAd, adView);
nativeAd.renderAdView(adView);
}
}

@@ -498,7 +480,7 @@ public int removeAdsInRange(int originalStartPosition, int originalEndPosition)
* @see #getAdViewType
*/
public int getAdViewTypeCount() {
return 1;
return mAdSource.getAdRendererCount();
}

/**
@@ -515,7 +497,12 @@ public int getAdViewTypeCount() {
* @return The ad view type.
*/
public int getAdViewType(final int position) {
return isAd(position) ? 1 : CONTENT_VIEW_TYPE;
NativeAd nativeAd = mPlacementData.getPlacedAd(position);
if (nativeAd == null) {
return CONTENT_VIEW_TYPE;
}

return mAdSource.getViewTypeForAd(nativeAd);
}

/**
@@ -706,54 +693,40 @@ private boolean tryPlaceAdsInRange(final int start, final int end) {
* @return false if there is no ad available to be placed.
*/
private boolean tryPlaceAd(final int position) {
final NativeResponse adResponse = mAdSource.dequeueAd();
if (adResponse == null) {
final NativeAd nativeAd = mAdSource.dequeueAd();
if (nativeAd == null) {
return false;
}

final NativeAdData adData = createAdData(adResponse);
mPlacementData.placeAd(position, adData);
mPlacementData.placeAd(position, nativeAd);
mItemCount++;

mAdLoadedListener.onAdLoaded(position);
return true;
}

@NonNull
private NativeAdData createAdData(@NonNull final NativeResponse adResponse) {
Preconditions.checkNotNull(mAdUnitId);
Preconditions.checkNotNull(mAdRenderer);

//noinspection ConstantConditions
return new NativeAdData(mAdUnitId, mAdRenderer, adResponse);
}

/**
* Clears any native response click trackers and impression tracking are set up for this view.
* Clears any {@link NativeAd} click trackers and impression tracking are set up for this view.
*/
private void clearNativeResponse(@Nullable final View view) {
private void clearNativeAd(@Nullable final View view) {
if (view == null) {
return;
}
mImpressionTracker.removeView(view);
final NativeResponse lastNativeResponse = mNativeResponseMap.get(view);
if (lastNativeResponse != null) {
lastNativeResponse.clear(view);
mNativeResponseMap.remove(view);
mViewMap.remove(lastNativeResponse);
final NativeAd lastNativeAd = mNativeAdMap.get(view);
if (lastNativeAd != null) {
lastNativeAd.clear(view);
mNativeAdMap.remove(view);
mViewMap.remove(lastNativeAd);
}
}

/**
* Prepares a view and nativeresponse for display by attaching click handlers
* Prepares a view and {@link NativeAd} for display by attaching click handlers
* and setting up impression tracking.
*/
private void prepareNativeResponse(@NonNull final NativeResponse nativeResponse, @NonNull final View view) {
mViewMap.put(nativeResponse, new WeakReference<View>(view));
mNativeResponseMap.put(view, nativeResponse);
if (!nativeResponse.isOverridingImpressionTracker()) {
mImpressionTracker.addView(view, nativeResponse);
}
nativeResponse.prepare(view);
private void prepareNativeAd(@NonNull final NativeAd nativeAd, @NonNull final View view) {
mViewMap.put(nativeAd, new WeakReference<View>(view));
mNativeAdMap.put(view, nativeAd);
nativeAd.prepare(view);
}
}
@@ -0,0 +1,90 @@
package com.mopub.nativeads;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.mopub.common.Preconditions;
import com.mopub.common.VisibleForTesting;

import java.util.WeakHashMap;

import static android.view.View.VISIBLE;

public class MoPubVideoNativeAdRenderer implements MoPubAdRenderer<VideoNativeAd> {
@NonNull private final MediaViewBinder mMediaViewBinder;

// This is used instead of View.setTag, which causes a memory leak in 2.3
// and earlier: https://code.google.com/p/android/issues/detail?id=18273
@VisibleForTesting
@NonNull final WeakHashMap<View, MediaViewHolder> mMediaViewHolderMap;

/**
* Constructs a native ad renderer with a view binder.
*
* @param mediaViewBinder The view binder to use when inflating and rendering an ad.
*/
public MoPubVideoNativeAdRenderer(@NonNull final MediaViewBinder mediaViewBinder) {
mMediaViewBinder = mediaViewBinder;
mMediaViewHolderMap = new WeakHashMap<View, MediaViewHolder>();
}

@Override
@NonNull
public View createAdView(@NonNull final Context context, @Nullable final ViewGroup parent) {
return LayoutInflater
.from(context)
.inflate(mMediaViewBinder.layoutId, parent, false);
}

@Override
public void renderAdView(@NonNull final View view,
@NonNull final VideoNativeAd videoNativeAd) {
MediaViewHolder mediaViewHolder = mMediaViewHolderMap.get(view);
if (mediaViewHolder == null) {
mediaViewHolder = MediaViewHolder.fromViewBinder(view, mMediaViewBinder);
mMediaViewHolderMap.put(view, mediaViewHolder);
}

update(mediaViewHolder, videoNativeAd);
NativeRendererHelper.updateExtras(mediaViewHolder.mainView, mMediaViewBinder.extras, videoNativeAd.getExtras());
setViewVisibility(mediaViewHolder, VISIBLE);

MediaLayout mediaLayout = (MediaLayout) view.findViewById(mMediaViewBinder.mediaLayoutId);
videoNativeAd.render(mediaLayout);
}

@Override
public boolean supports(@NonNull final BaseNativeAd nativeAd) {
Preconditions.checkNotNull(nativeAd);
return nativeAd instanceof VideoNativeAd;
}

private void update(@NonNull final MediaViewHolder mediaViewHolder,
@NonNull final VideoNativeAd videoNativeAd) {
NativeRendererHelper.addTextView(mediaViewHolder.titleView,
videoNativeAd.getTitle());
NativeRendererHelper.addTextView(mediaViewHolder.textView, videoNativeAd.getText());
NativeRendererHelper.addCtaButton(mediaViewHolder.callToActionView,
mediaViewHolder.mainView, videoNativeAd.getCallToAction()
);
NativeImageHelper.loadImageView(videoNativeAd.getMainImageUrl(),
mediaViewHolder.mediaLayout.getMainImageView());
NativeImageHelper.loadImageView(videoNativeAd.getIconImageUrl(),
mediaViewHolder.iconImageView);
NativeRendererHelper.addPrivacyInformationIcon(
mediaViewHolder.privacyInformationIconImageView,
videoNativeAd.getPrivacyInformationIconClickThroughUrl());
}

private void setViewVisibility(@NonNull final MediaViewHolder mediaViewHolder,
final int visibility) {
if (mediaViewHolder.mainView != null) {
mediaViewHolder.mainView.setVisibility(visibility);
}
}
}

@@ -0,0 +1,165 @@
package com.mopub.nativeads;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;

import com.mopub.common.VisibleForTesting;
import com.mopub.network.TrackingRequest;

import java.util.HashSet;
import java.util.Set;

import static com.mopub.nativeads.BaseNativeAd.NativeEventListener;

public class NativeAd {

/**
* Listen for MoPub specific click and impression events
*/
public interface MoPubNativeEventListener {
void onImpression(final View view);
void onClick(final View view);
}

@NonNull private final Context mContext;
@NonNull private final BaseNativeAd mBaseNativeAd;
@NonNull private final MoPubAdRenderer mMoPubAdRenderer;
@NonNull private final Set<String> mImpressionTrackers;
@NonNull private final Set<String> mClickTrackers;
@NonNull private final String mAdUnitId;
@Nullable private MoPubNativeEventListener mMoPubNativeEventListener;

private boolean mRecordedImpression;
private boolean mIsClicked;
private boolean mIsDestroyed;

public NativeAd(@NonNull final Context context,
@NonNull final String moPubImpressionTrackerUrl,
@NonNull final String moPubClickTrackerUrl,
@NonNull final String adUnitId,
@NonNull final BaseNativeAd baseNativeAd,
@NonNull final MoPubAdRenderer moPubAdRenderer) {
mContext = context.getApplicationContext();
mAdUnitId = adUnitId;

mImpressionTrackers = new HashSet<String>();
mImpressionTrackers.add(moPubImpressionTrackerUrl);
mImpressionTrackers.addAll(baseNativeAd.getImpressionTrackers());

mClickTrackers = new HashSet<String>();
mClickTrackers.add(moPubClickTrackerUrl);
mClickTrackers.addAll(baseNativeAd.getClickTrackers());

mBaseNativeAd = baseNativeAd;
mBaseNativeAd.setNativeEventListener(new NativeEventListener() {
@Override
public void onAdImpressed() {
recordImpression(null);
}

@Override
public void onAdClicked() {
handleClick(null);
}
});

mMoPubAdRenderer = moPubAdRenderer;
}

@Override
public String toString() {
final StringBuilder stringBuilder = new StringBuilder("\n");
stringBuilder.append("impressionTrackers").append(":").append(mImpressionTrackers).append("\n");
stringBuilder.append("clickTrackers").append(":").append(mClickTrackers).append("\n");
stringBuilder.append("recordedImpression").append(":").append(mRecordedImpression).append("\n");
stringBuilder.append("isClicked").append(":").append(mIsClicked).append("\n");
stringBuilder.append("isDestroyed").append(":").append(mIsDestroyed).append("\n");
return stringBuilder.toString();
}

public void setMoPubNativeEventListener(@Nullable final MoPubNativeEventListener moPubNativeEventListener) {
mMoPubNativeEventListener = moPubNativeEventListener;
}

@NonNull
public String getAdUnitId() {
return mAdUnitId;
}

public boolean isDestroyed() {
return mIsDestroyed;
}

@NonNull
public View createAdView(ViewGroup parent) {
return mMoPubAdRenderer.createAdView(mContext, parent);
}

public void renderAdView(View view) {
//noinspection unchecked
mMoPubAdRenderer.renderAdView(view, mBaseNativeAd);
}

@NonNull
public MoPubAdRenderer getMoPubAdRenderer() {
return mMoPubAdRenderer;
}

// Lifecycle Handlers
public void prepare(@NonNull final View view) {
if (mIsDestroyed) {
return;
}

mBaseNativeAd.prepare(view);
}

public void clear(@NonNull final View view) {
if (mIsDestroyed) {
return;
}

mBaseNativeAd.clear(view);
}

public void destroy() {
if (mIsDestroyed) {
return;
}

mBaseNativeAd.destroy();
mIsDestroyed = true;
}

// Event Handlers
@VisibleForTesting
void recordImpression(@Nullable final View view) {
if (mRecordedImpression || mIsDestroyed) {
return;
}

TrackingRequest.makeTrackingHttpRequest(mImpressionTrackers, mContext);
if (mMoPubNativeEventListener != null) {
mMoPubNativeEventListener.onImpression(view);
}

mRecordedImpression = true;
}

@VisibleForTesting
void handleClick(@Nullable final View view) {
if (mIsClicked || mIsDestroyed) {
return;
}

TrackingRequest.makeTrackingHttpRequest(mClickTrackers, mContext);
if (mMoPubNativeEventListener != null) {
mMoPubNativeEventListener.onClick(view);
}

mIsClicked = true;
}
}
@@ -8,11 +8,11 @@
class NativeAdData {
@NonNull private final String adUnitId;
@NonNull private final MoPubAdRenderer adRenderer;
@NonNull private final NativeResponse adResponse;
@NonNull private final NativeAd adResponse;

NativeAdData(@NonNull final String adUnitId,
@NonNull final MoPubAdRenderer adRenderer,
@NonNull final NativeResponse adResponse) {
@NonNull final NativeAd adResponse) {
this.adUnitId = adUnitId;
this.adRenderer = adRenderer;
this.adResponse = adResponse;
@@ -29,7 +29,7 @@ MoPubAdRenderer getAdRenderer() {
}

@NonNull
NativeResponse getAd() {
NativeAd getAd() {
return adResponse;
}
}

This file was deleted.

Oops, something went wrong.
@@ -30,28 +30,33 @@
* This class is not thread safe and should only be called from the UI thread.
*/
class NativeAdSource {
private static final int CACHE_LIMIT = 3;
/**
* Number of ads to cache
*/
private static final int CACHE_LIMIT = 1;

private static final int EXPIRATION_TIME_MILLISECONDS = 15 * 60 * 1000; // 15 minutes
private static final int DEFAULT_RETRY_TIME_MILLISECONDS = 1000; // 1 second
private static final int MAXIMUM_RETRY_TIME_MILLISECONDS = 5 * 60 * 1000; // 5 minutes.
private static final double EXPONENTIAL_BACKOFF_FACTOR = 2.0;
@VisibleForTesting static final int[] RETRY_TIME_ARRAY_MILLISECONDS = new int[]{1000, 3000, 5000, 25000, 60000, MAXIMUM_RETRY_TIME_MILLISECONDS};

@NonNull private final List<TimestampWrapper<NativeResponse>> mNativeAdCache;
@NonNull private final List<TimestampWrapper<NativeAd>> mNativeAdCache;
@NonNull private final Handler mReplenishCacheHandler;
@NonNull private final Runnable mReplenishCacheRunnable;
@NonNull private final MoPubNativeNetworkListener mMoPubNativeNetworkListener;

@VisibleForTesting boolean mRequestInFlight;
@VisibleForTesting boolean mRetryInFlight;
@VisibleForTesting int mSequenceNumber;
@VisibleForTesting int mRetryTimeMilliseconds;
@VisibleForTesting int mCurrentRetries;

@Nullable private AdSourceListener mAdSourceListener;

// We will need collections of these when we support multiple ad units.
@Nullable private RequestParameters mRequestParameters;
@Nullable private MoPubNative mMoPubNative;

@NonNull private final AdRendererRegistry mAdRendererRegistry;

/**
* A listener for when ads are available for dequeueing.
*/
@@ -63,12 +68,15 @@
}

NativeAdSource() {
this(new ArrayList<TimestampWrapper<NativeResponse>>(CACHE_LIMIT), new Handler());
this(new ArrayList<TimestampWrapper<NativeAd>>(CACHE_LIMIT),
new Handler(),
new AdRendererRegistry());
}

@VisibleForTesting
NativeAdSource(@NonNull final List<TimestampWrapper<NativeResponse>> nativeAdCache,
@NonNull final Handler replenishCacheHandler) {
NativeAdSource(@NonNull final List<TimestampWrapper<NativeAd>> nativeAdCache,
@NonNull final Handler replenishCacheHandler,
@NonNull AdRendererRegistry adRendererRegistry) {
mNativeAdCache = nativeAdCache;
mReplenishCacheHandler = replenishCacheHandler;
mReplenishCacheRunnable = new Runnable() {
@@ -79,10 +87,12 @@ public void run() {
}
};

mAdRendererRegistry = adRendererRegistry;

// Construct native URL and start filling the cache
mMoPubNativeNetworkListener = new MoPubNativeNetworkListener() {
@Override
public void onNativeLoad(@NonNull final NativeResponse nativeResponse) {
public void onNativeLoad(@NonNull final NativeAd nativeAd) {
// This can be null if the ad source was cleared as the AsyncTask is posting
// back to the UI handler. Drop this response.
if (mMoPubNative == null) {
@@ -93,7 +103,7 @@ public void onNativeLoad(@NonNull final NativeResponse nativeResponse) {
mSequenceNumber++;
resetRetryTime();

mNativeAdCache.add(new TimestampWrapper<NativeResponse>(nativeResponse));
mNativeAdCache.add(new TimestampWrapper<NativeAd>(nativeAd));
if (mNativeAdCache.size() == 1 && mAdSourceListener != null) {
mAdSourceListener.onAdsAvailable();
}
@@ -106,21 +116,46 @@ public void onNativeFail(final NativeErrorCode errorCode) {
// Reset the retry time for the next time we dequeue.
mRequestInFlight = false;

// Stopping requests after the max retry time prevents us from using battery when
// Stopping requests after the max retry count prevents us from using battery when
// the user is not interacting with the stream, eg. the app is backgrounded.
if (mRetryTimeMilliseconds >= MAXIMUM_RETRY_TIME_MILLISECONDS) {
if (mCurrentRetries >= RETRY_TIME_ARRAY_MILLISECONDS.length - 1) {
resetRetryTime();
return;
}

updateRetryTime();
mRetryInFlight = true;
mReplenishCacheHandler.postDelayed(mReplenishCacheRunnable, mRetryTimeMilliseconds);
mReplenishCacheHandler.postDelayed(mReplenishCacheRunnable, getRetryTime());
}
};

mSequenceNumber = 0;
mRetryTimeMilliseconds = DEFAULT_RETRY_TIME_MILLISECONDS;
resetRetryTime();
}

int getAdRendererCount() {
return mAdRendererRegistry.getAdRendererCount();
}

public int getViewTypeForAd(@NonNull final NativeAd nativeAd) {
return mAdRendererRegistry.getViewTypeForAd(nativeAd);
}

/**
* Registers an ad renderer for rendering a specific native ad format.
* Note that if multiple ad renderers support a specific native ad format, the first
* one registered will be used.
*/
void registerAdRenderer(@NonNull final MoPubAdRenderer moPubNativeAdRenderer) {
mAdRendererRegistry.registerAdRenderer(moPubNativeAdRenderer);
if (mMoPubNative != null) {
mMoPubNative.registerAdRenderer(moPubNativeAdRenderer);
}
}

@Nullable
public MoPubAdRenderer getAdRendererForViewType(final int viewType) {
return mAdRendererRegistry.getRendererForViewType(viewType);
}

/**
@@ -142,6 +177,10 @@ void loadAds(final RequestParameters requestParameters,
final MoPubNative moPubNative) {
clear();

for (MoPubAdRenderer renderer : mAdRendererRegistry.getRendererIterable()) {
moPubNative.registerAdRenderer(renderer);
}

mRequestParameters = requestParameters;
mMoPubNative = moPubNative;

@@ -160,7 +199,7 @@ void clear() {

mRequestParameters = null;

for (final TimestampWrapper<NativeResponse> timestampWrapper : mNativeAdCache) {
for (final TimestampWrapper<NativeAd> timestampWrapper : mNativeAdCache) {
timestampWrapper.mInstance.destroy();
}
mNativeAdCache.clear();
@@ -182,7 +221,7 @@ void clear() {
* @return Ad ad item that should be rendered into a view.
*/
@Nullable
NativeResponse dequeueAd() {
NativeAd dequeueAd() {
final long now = SystemClock.uptimeMillis();

// Starting an ad request takes several millis. Post for performance reasons.
@@ -192,7 +231,7 @@ NativeResponse dequeueAd() {

// Dequeue the first ad that hasn't expired.
while (!mNativeAdCache.isEmpty()) {
TimestampWrapper<NativeResponse> responseWrapper = mNativeAdCache.remove(0);
TimestampWrapper<NativeAd> responseWrapper = mNativeAdCache.remove(0);

if (now - responseWrapper.mCreatedTimestamp < EXPIRATION_TIME_MILLISECONDS) {
return responseWrapper.mInstance;
@@ -203,16 +242,22 @@ NativeResponse dequeueAd() {

@VisibleForTesting
void updateRetryTime() {
// Backoff time calculations
mRetryTimeMilliseconds = (int) (mRetryTimeMilliseconds * EXPONENTIAL_BACKOFF_FACTOR);
if (mRetryTimeMilliseconds > MAXIMUM_RETRY_TIME_MILLISECONDS) {
mRetryTimeMilliseconds = MAXIMUM_RETRY_TIME_MILLISECONDS;
if (mCurrentRetries < RETRY_TIME_ARRAY_MILLISECONDS.length - 1) {
mCurrentRetries++;
}
}

@VisibleForTesting
void resetRetryTime() {
mRetryTimeMilliseconds = DEFAULT_RETRY_TIME_MILLISECONDS;
mCurrentRetries = 0;
}

@VisibleForTesting
int getRetryTime() {
if (mCurrentRetries >= RETRY_TIME_ARRAY_MILLISECONDS.length) {
mCurrentRetries = RETRY_TIME_ARRAY_MILLISECONDS.length - 1;
}
return RETRY_TIME_ARRAY_MILLISECONDS[mCurrentRetries];
}

/**
Oops, something went wrong.