| @@ -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); | |||
| } | |||
| @@ -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(); | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||