diff --git a/CHANGELOG.md b/CHANGELOG.md index 63873ff7f..5add0cbcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Version 4.19.0 (December 11, 2017) +- Updated Facebook Audience Network adapters to 4.26.1. +- Updated Flurry adapters to 8.1.0. +- Updated Millennial rewarded ads adapters to 6.6.1. +- Fixed a potential crash for native video ads when attempting to blur the last video frame. +- Fixed a duplicate on loaded callback for some rewarded ads. + ## Version 4.18.0 (November 1, 2017) - Updated the SDK compile version to 26. Android API 26 artifacts live in the new Google maven repository `maven { url 'https://maven.google.com' }`. See [this article](https://developer.android.com/about/versions/oreo/android-8.0-migration.html) for more information about using Android API 26. - Fixed MoPub in-app browser's back and forward button icons. diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..0f179eab2 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,22 @@ +#!/usr/bin/env groovy +pipeline { + agent any + environment { + ANDROID_HOME = '/Users/jenkins/Library/Android/sdk' + } + stages { + stage('Build') { + steps { + sh './gradlew clean build' + } + } + } + post { + success { + hipchatSend message: "${env.JOB_NAME} #${env.BUILD_NUMBER} has succeeded.", color: 'GREEN' + } + failure { + hipchatSend message: "Attention @here ${env.JOB_NAME} #${env.BUILD_NUMBER} has failed.", color: 'RED' + } + } +} diff --git a/README.md b/README.md index c98dd9bc3..a262d86f7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ The MoPub SDK is available via: } dependencies { - compile('com.mopub:mopub-sdk:4.18.0@aar') { + compile('com.mopub:mopub-sdk:4.19.0@aar') { transitive = true } } @@ -61,27 +61,27 @@ The MoPub SDK is available via: // ... other project dependencies // For banners - compile('com.mopub:mopub-sdk-banner:4.18.0@aar') { + compile('com.mopub:mopub-sdk-banner:4.19.0@aar') { transitive = true } // For interstitials - compile('com.mopub:mopub-sdk-interstitial:4.18.0@aar') { + compile('com.mopub:mopub-sdk-interstitial:4.19.0@aar') { transitive = true } // For rewarded videos. This will automatically also include interstitials - compile('com.mopub:mopub-sdk-rewardedvideo:4.18.0@aar') { + compile('com.mopub:mopub-sdk-rewardedvideo:4.19.0@aar') { transitive = true } // For native static (images). - compile('com.mopub:mopub-sdk-native-static:4.18.0@aar') { + compile('com.mopub:mopub-sdk-native-static:4.19.0@aar') { transitive = true } // For native video. This will automatically also include native static - compile('com.mopub:mopub-sdk-native-video:4.18.0@aar') { + compile('com.mopub:mopub-sdk-native-video:4.19.0@aar') { transitive = true } } @@ -109,24 +109,18 @@ The MoPub SDK is available via: ## New in this Version Please view the [changelog](https://github.com/mopub/mopub-android-sdk/blob/master/CHANGELOG.md) for a complete list of additions, fixes, and enhancements in the latest release. -- Updated the SDK compile version to 26. Android API 26 artifacts live in the new Google maven repository `maven { url 'https://maven.google.com' }`. See [this article](https://developer.android.com/about/versions/oreo/android-8.0-migration.html) for more information about using Android API 26. -- Fixed MoPub in-app browser's back and forward button icons. -- Updated AdMob adapters to 11.4.0. -- Updated Chartboost adapters to 7.0.1. -- Updated Facebook Audience Network adapters to 4.26.0. -- Updated Millennial to 6.6.1. -- Updated TapJoy adapters to 11.11.0. -- Updated Unity Ads adapters to 2.1.1. -- Updated Vungle adapters to 5.3.0. +- Updated Facebook Audience Network adapters to 4.26.1. +- Updated Flurry adapters to 8.1.0. +- Updated Millennial rewarded ads adapters to 6.6.1. ## Requirements - Android 4.1 (API Version 16) and up (**Updated in 4.12.0**) -- android-support-v4.jar, r23 (**Updated in 4.4.0**) -- android-support-annotations.jar, r23 (**Updated in 4.4.0**) -- android-support-v7-recyclerview.jar, r23 (**Updated in 4.4.0**) +- android-support-v4.jar, r26 (**Updated in 4.18.0**) +- android-support-annotations.jar, r26 (**Updated in 4.18.0**) +- android-support-v7-recyclerview.jar, r26 (**Updated in 4.18.0**) - MoPub Volley Library (mopub-volley-1.1.0.jar - available on JCenter) (**Updated in 3.6.0**) -- **Recommended** Google Play Services 9.4.0 +- **Recommended** Google Play Services 11.4.0 ## Upgrading from 4.15.0 and Prior In 4.16.0, dependencies were added to viewability libraries provided by AVID and Moat. Apps upgrading from previous versions must add @@ -149,7 +143,7 @@ Update to the following to exclude one or both viewability vendors: ``` dependencies { - compile('com.mopub:mopub-sdk:4.18.0@aar') { + compile('com.mopub:mopub-sdk:4.19.0@aar') { transitive = true exclude module: 'libAvid-mopub' // To exclude AVID exclude module: 'moat-mobile-app-kit' // To exclude Moat diff --git a/extras/src/com/mopub/mobileads/MillennialRewardedVideo.java b/extras/src/com/mopub/mobileads/MillennialRewardedVideo.java new file mode 100644 index 000000000..ca906ba1b --- /dev/null +++ b/extras/src/com/mopub/mobileads/MillennialRewardedVideo.java @@ -0,0 +1,361 @@ +package com.mopub.mobileads; + +import android.app.Activity; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.millennialmedia.AppInfo; +import com.millennialmedia.CreativeInfo; +import com.millennialmedia.InterstitialAd; +import com.millennialmedia.InterstitialAd.InterstitialErrorStatus; +import com.millennialmedia.InterstitialAd.InterstitialListener; +import com.millennialmedia.MMException; +import com.millennialmedia.MMLog; +import com.millennialmedia.MMSDK; +import com.millennialmedia.XIncentivizedEventListener; +import com.mopub.common.BaseLifecycleListener; +import com.mopub.common.LifecycleListener; +import com.mopub.common.MoPub; +import com.mopub.common.MoPubReward; + +import java.util.Map; + + +/** + * Compatible with version 6.6 of the Millennial Media SDK. + */ + +@SuppressWarnings("unused") +final class MillennialRewardedVideo extends CustomEventRewardedVideo { + + private static final String TAG = MillennialRewardedVideo.class.getSimpleName(); + public static final String DCN_KEY = "dcn"; + public static final String APID_KEY = "adUnitID"; + + private InterstitialAd millennialInterstitial; + private MillennialRewardedVideoListener millennialRewardedVideoListener = new MillennialRewardedVideoListener(); + private Context context; + private String apid = null; + + static { + Log.i(TAG, "Millennial Media Adapter Version: " + MillennialUtils.VERSION); + } + + + public CreativeInfo getCreativeInfo() { + + if (millennialInterstitial == null) { + return null; + } + + return millennialInterstitial.getCreativeInfo(); + } + + + @Nullable + @Override + protected CustomEventRewardedVideoListener getVideoListenerForSdk() { + + return millennialRewardedVideoListener; + } + + + @Nullable + @Override + protected LifecycleListener getLifecycleListener() { + + return new BaseLifecycleListener(); + } + + + @NonNull + @Override + protected String getAdNetworkId() { + + return (apid == null) ? "" : apid; + } + + + @Override + protected void onInvalidate() { + + if (millennialInterstitial != null) { + millennialInterstitial.destroy(); + millennialInterstitial = null; + apid = null; + } + } + + + @Override + protected boolean checkAndInitializeSdk(@NonNull Activity launcherActivity, + @NonNull Map localExtras, @NonNull Map serverExtras) throws Exception { + + if (!MillennialUtils.initSdk(launcherActivity)) { + Log.e(TAG, "MM SDK must be initialized with an Activity or Application context."); + + return false; + } + + return true; + } + + + @Override + protected void loadWithSdkInitialized(@NonNull Activity activity, @NonNull Map localExtras, + @NonNull Map serverExtras) throws Exception { + + this.context = activity.getApplicationContext(); + apid = serverExtras.get(APID_KEY); + String dcn = serverExtras.get(DCN_KEY); + + if (MillennialUtils.isEmpty(apid)) { + Log.e(TAG, "Invalid extras-- Be sure you have a placement ID specified."); + MoPubRewardedVideoManager.onRewardedVideoLoadFailure(MillennialRewardedVideo.class, "", + MoPubErrorCode.ADAPTER_CONFIGURATION_ERROR); + + return; + } + + // Add DCN support + AppInfo ai = new AppInfo().setMediator("mopubsdk").setSiteId(dcn); + try { + MMSDK.setAppInfo(ai); + /* If MoPub gets location, so do we. */ + MMSDK.setLocationEnabled(MoPub.getLocationAwareness() != MoPub.LocationAwareness.DISABLED); + + millennialInterstitial = InterstitialAd.createInstance(apid); + millennialInterstitial.setListener(millennialRewardedVideoListener); + millennialInterstitial.xSetIncentivizedListener(millennialRewardedVideoListener); + millennialInterstitial.load(activity, null); + + } catch (MMException e) { + Log.e(TAG, "An exception occurred loading an InterstitialAd", e); + MoPubRewardedVideoManager + .onRewardedVideoLoadFailure(MillennialRewardedVideo.class, apid, MoPubErrorCode.INTERNAL_ERROR); + } + } + + + @Override + protected boolean hasVideoAvailable() { + + return ((millennialInterstitial != null) && millennialInterstitial.isReady()); + } + + + @Override + protected void showVideo() { + + if ((millennialInterstitial != null) && millennialInterstitial.isReady()) { + try { + millennialInterstitial.show(context); + } catch (MMException e) { + Log.e(TAG, "An exception occurred showing the MM SDK interstitial.", e); + MoPubRewardedVideoManager + .onRewardedVideoPlaybackError(MillennialRewardedVideo.class, millennialInterstitial.placementId, + MoPubErrorCode.INTERNAL_ERROR); + } + } else { + Log.w(TAG, "showVideo called before MillennialInterstitial ad was loaded."); + } + } + + + class MillennialRewardedVideoListener + implements InterstitialListener, XIncentivizedEventListener, CustomEventRewardedVideoListener { + + @Override + public void onAdLeftApplication(InterstitialAd interstitialAd) { + // onLeaveApplication is an alias to on clicked. We are not required to call this. + + // @formatter:off + // https://github.com/mopub/mopub-android-sdk/blob/940eee70fe1980b4869d61cb5d668ccbab75c0ee/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/CustomEventInterstitial.java + // @formatter:on + Log.d(TAG, "Millennial Rewarded Video Ad - Leaving application"); + } + + + @Override + public void onClicked(final InterstitialAd interstitialAd) { + + Log.d(TAG, "Millennial Rewarded Video Ad - Ad was clicked"); + MillennialUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + + MoPubRewardedVideoManager + .onRewardedVideoClicked(MillennialRewardedVideo.class, interstitialAd.placementId); + } + }); + } + + + @Override + public void onClosed(final InterstitialAd interstitialAd) { + + Log.d(TAG, "Millennial Rewarded Video Ad - Ad was closed"); + MillennialUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + + MoPubRewardedVideoManager + .onRewardedVideoClosed(MillennialRewardedVideo.class, interstitialAd.placementId); + } + }); + } + + + @Override + public void onExpired(final InterstitialAd interstitialAd) { + + Log.d(TAG, "Millennial Rewarded Video Ad - Ad expired"); + MillennialUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + + MoPubRewardedVideoManager + .onRewardedVideoLoadFailure(MillennialRewardedVideo.class, interstitialAd.placementId, + MoPubErrorCode.VIDEO_NOT_AVAILABLE); + } + }); + } + + + @Override + public void onLoadFailed(final InterstitialAd interstitialAd, InterstitialErrorStatus + interstitialErrorStatus) { + + Log.d(TAG, "Millennial Rewarded Video Ad - load failed (" + interstitialErrorStatus.getErrorCode() + "): " + + interstitialErrorStatus.getDescription()); + + final MoPubErrorCode moPubErrorCode; + + switch (interstitialErrorStatus.getErrorCode()) { + case InterstitialErrorStatus.ALREADY_LOADED: + // This will generate discrepancies, as requests will NOT be sent to Millennial. + MillennialUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + + MoPubRewardedVideoManager + .onRewardedVideoLoadSuccess(MillennialRewardedVideo.class, interstitialAd.placementId); + } + }); + Log.w(TAG, "Millennial Rewarded Video Ad - Attempted to load ads when ads are already loaded."); + return; + case InterstitialErrorStatus.EXPIRED: + case InterstitialErrorStatus.DISPLAY_FAILED: + case InterstitialErrorStatus.INIT_FAILED: + case InterstitialErrorStatus.ADAPTER_NOT_FOUND: + moPubErrorCode = MoPubErrorCode.INTERNAL_ERROR; + break; + case InterstitialErrorStatus.NO_NETWORK: + moPubErrorCode = MoPubErrorCode.NO_CONNECTION; + break; + case InterstitialErrorStatus.UNKNOWN: + moPubErrorCode = MoPubErrorCode.UNSPECIFIED; + break; + case InterstitialErrorStatus.NOT_LOADED: + case InterstitialErrorStatus.LOAD_FAILED: + default: + moPubErrorCode = MoPubErrorCode.NETWORK_NO_FILL; + } + + MillennialUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + + MoPubRewardedVideoManager + .onRewardedVideoLoadFailure(MillennialRewardedVideo.class, interstitialAd.placementId, + moPubErrorCode); + } + }); + } + + + @Override + public void onLoaded(final InterstitialAd interstitialAd) { + + Log.d(TAG, "Millennial Rewarded Video Ad - Ad loaded splendidly"); + + CreativeInfo creativeInfo = getCreativeInfo(); + + if ((creativeInfo != null) && MMLog.isDebugEnabled()) { + MMLog.d(TAG, "Rewarded Video Creative Info: " + creativeInfo); + } + + MillennialUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + + MoPubRewardedVideoManager + .onRewardedVideoLoadSuccess(MillennialRewardedVideo.class, interstitialAd.placementId); + } + }); + } + + + @Override + public void onShowFailed(final InterstitialAd interstitialAd, InterstitialErrorStatus + interstitialErrorStatus) { + + Log.e(TAG, "Millennial Rewarded Video Ad - Show failed (" + interstitialErrorStatus.getErrorCode() + "): " + + interstitialErrorStatus.getDescription()); + + MillennialUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + + MoPubRewardedVideoManager + .onRewardedVideoPlaybackError(MillennialRewardedVideo.class, interstitialAd.placementId, + MoPubErrorCode.VIDEO_PLAYBACK_ERROR); + } + }); + } + + + @Override + public void onShown(final InterstitialAd interstitialAd) { + + Log.d(TAG, "Millennial Rewarded Video Ad - Ad shown"); + MillennialUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + + MoPubRewardedVideoManager + .onRewardedVideoStarted(MillennialRewardedVideo.class, interstitialAd.placementId); + } + }); + } + + + @Override + public boolean onVideoComplete() { + + Log.d(TAG, "Millennial Rewarded Video Ad - Video completed"); + MillennialUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + + MoPubRewardedVideoManager + .onRewardedVideoCompleted(MillennialRewardedVideo.class, millennialInterstitial.placementId, + MoPubReward.success(MoPubReward.NO_REWARD_LABEL, MoPubReward.DEFAULT_REWARD_AMOUNT)); + } + }); + return false; + } + + + @Override + public boolean onCustomEvent(XIncentiveEvent xIncentiveEvent) { + + Log.d(TAG, "Millennial Rewarded Video Ad - Custom event received: " + xIncentiveEvent.eventId + ", " + + xIncentiveEvent.args); + + return false; + } + } +} diff --git a/mopub-sample/AndroidManifest.xml b/mopub-sample/AndroidManifest.xml index 963334ad1..814b58ea3 100644 --- a/mopub-sample/AndroidManifest.xml +++ b/mopub-sample/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="57" + android:versionName="4.19.0"> @@ -17,11 +17,19 @@ android:networkSecurityConfig="@xml/network_security_config"> + + + + + + diff --git a/mopub-sample/build.gradle b/mopub-sample/build.gradle index b9101ad42..a54d0f60c 100644 --- a/mopub-sample/build.gradle +++ b/mopub-sample/build.gradle @@ -11,7 +11,7 @@ apply plugin: 'com.android.application' project.group = 'com.mopub' project.description = '''MoPub Sample App''' -project.version = '4.18.0' +project.version = '4.19.0' android { compileSdkVersion 26 @@ -19,7 +19,7 @@ android { lintOptions { abortOnError false } defaultConfig { - versionCode 56 + versionCode 57 versionName version minSdkVersion 16 targetSdkVersion 26 @@ -58,7 +58,8 @@ android { dependencies { compile 'com.android.support:support-v4:26.1.0' - compile 'com.google.android.gms:play-services-ads:9.4.0' + compile 'com.google.android.gms:play-services-ads:11.4.0' + compile 'com.google.android.gms:play-services-base:11.4.0' compile 'com.android.support:recyclerview-v7:26.1.0' compile project(':mopub-sdk') } diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/AbstractBannerDetailFragment.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/AbstractBannerDetailFragment.java index f4ee63c90..c92575b3a 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/AbstractBannerDetailFragment.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/AbstractBannerDetailFragment.java @@ -44,6 +44,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, layoutParams.height = getHeight(); mMoPubView.setLayoutParams(layoutParams); + views.mKeywordsField.setText(getArguments().getString(MoPubListFragment.KEYWORDS_KEY, "")); hideSoftKeyboard(views.mKeywordsField); final String adUnitId = mMoPubSampleAdUnit.getAdUnitId(); diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/AdUnitDataSource.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/AdUnitDataSource.java index dbe77011d..3a1b987b9 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/AdUnitDataSource.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/AdUnitDataSource.java @@ -4,7 +4,9 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.NonNull; +import com.mopub.common.Preconditions; import com.mopub.common.logging.MoPubLog; import java.util.ArrayList; @@ -53,7 +55,10 @@ MoPubSampleAdUnit createSampleAdUnit(final MoPubSampleAdUnit sampleAdUnit) { } private MoPubSampleAdUnit createSampleAdUnit(final MoPubSampleAdUnit sampleAdUnit, - final boolean isUserGenerated) { + final boolean isUserGenerated) { + deleteAllAdUnitsWithAdUnitIdAndAdType(sampleAdUnit.getAdUnitId(), + sampleAdUnit.getFragmentClassName()); + final ContentValues values = new ContentValues(); final int userGenerated = isUserGenerated ? 1 : 0; values.put(COLUMN_AD_UNIT_ID, sampleAdUnit.getAdUnitId()); @@ -85,6 +90,20 @@ void deleteSampleAdUnit(final MoPubSampleAdUnit adConfiguration) { database.close(); } + private void deleteAllAdUnitsWithAdUnitIdAndAdType(@NonNull final String adUnitId, + @NonNull final String adType) { + Preconditions.checkNotNull(adUnitId); + Preconditions.checkNotNull(adType); + + final SQLiteDatabase database = mDatabaseHelper.getWritableDatabase(); + final int numDeletedRows = database.delete(TABLE_AD_CONFIGURATIONS, + COLUMN_AD_UNIT_ID + " = '" + adUnitId + + "' AND " + COLUMN_USER_GENERATED + " = 1 AND " + + COLUMN_AD_TYPE + " = '" + adType + "'", null); + MoPubLog.d(numDeletedRows + " rows deleted with adUnitId: " + adUnitId); + database.close(); + } + List getAllAdUnits() { final List adConfigurations = new ArrayList<>(); SQLiteDatabase database = mDatabaseHelper.getReadableDatabase(); @@ -94,7 +113,9 @@ List getAllAdUnits() { while (!cursor.isAfterLast()) { final MoPubSampleAdUnit adConfiguration = cursorToAdConfiguration(cursor); - adConfigurations.add(adConfiguration); + if (adConfiguration != null) { + adConfigurations.add(adConfiguration); + } cursor.moveToNext(); } diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/InterstitialDetailFragment.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/InterstitialDetailFragment.java index f27ab6bb7..fec98dd95 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/InterstitialDetailFragment.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/InterstitialDetailFragment.java @@ -25,6 +25,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa MoPubSampleAdUnit.fromBundle(getArguments()); final View view = inflater.inflate(R.layout.interstitial_detail_fragment, container, false); final DetailFragmentViewHolder views = DetailFragmentViewHolder.fromView(view); + views.mKeywordsField.setText(getArguments().getString(MoPubListFragment.KEYWORDS_KEY, "")); hideSoftKeyboard(views.mKeywordsField); final String adUnitId = adConfiguration.getAdUnitId(); diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubListFragment.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubListFragment.java index d655656c2..ce6d1e79c 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubListFragment.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubListFragment.java @@ -3,11 +3,15 @@ import android.app.AlertDialog; import android.app.Dialog; import android.content.DialogInterface; +import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.ListFragment; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -20,12 +24,14 @@ import android.widget.Toast; import com.mopub.common.MoPub; +import com.mopub.common.Preconditions; import com.mopub.common.logging.MoPubLog; import java.util.ArrayList; import java.util.List; import static com.mopub.simpleadsdemo.MoPubSampleAdUnit.AdType; +import static com.mopub.simpleadsdemo.Utils.logToast; interface TrashCanClickListener { @@ -33,6 +39,12 @@ interface TrashCanClickListener { } public class MoPubListFragment extends ListFragment implements TrashCanClickListener { + + private static final String AD_UNIT_ID_KEY = "adUnitId"; + private static final String FORMAT_KEY = "format"; + static final String KEYWORDS_KEY = "keywords"; + private static final String NAME_KEY = "name"; + private MoPubSampleListAdapter mAdapter; private AdUnitDataSource mAdUnitDataSource; @@ -44,6 +56,33 @@ public void onCreate(Bundle savedInstanceState) { initializeAdapter(); } + void addAdUnitViaDeeplink(@Nullable final Uri deeplinkData) { + if (deeplinkData == null) { + return; + } + + final String adUnitId = deeplinkData.getQueryParameter(AD_UNIT_ID_KEY); + try { + Utils.validateAdUnitId(adUnitId); + } catch (IllegalArgumentException e) { + logToast(getContext(), "Ignoring invalid ad unit: " + adUnitId); + return; + } + + final String format = deeplinkData.getQueryParameter(FORMAT_KEY); + final AdType adType = AdType.fromDeeplinkString(format); + if (adType == null) { + logToast(getContext(), "Ignoring invalid ad format: " + format); + return; + } + + final String name = deeplinkData.getQueryParameter(NAME_KEY); + final MoPubSampleAdUnit adUnit = new MoPubSampleAdUnit.Builder(adUnitId, + adType).description(name == null ? "" : name).build(); + final MoPubSampleAdUnit newAdUnit = addAdUnit(adUnit); + enterAdFragment(newAdUnit, deeplinkData.getQueryParameter(KEYWORDS_KEY)); + } + @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.ad_unit_list_fragment, container, false); @@ -67,6 +106,15 @@ public void onListItemClick(ListView listView, View view, int position, long id) final MoPubSampleAdUnit adConfiguration = mAdapter.getItem(position); + if (adConfiguration != null) { + enterAdFragment(adConfiguration, null); + } + } + + private void enterAdFragment(@NonNull final MoPubSampleAdUnit adConfiguration, + @Nullable final String keywords) { + Preconditions.checkNotNull(adConfiguration); + final FragmentTransaction fragmentTransaction = getActivity().getSupportFragmentManager().beginTransaction(); @@ -83,7 +131,15 @@ public void onListItemClick(ListView listView, View view, int position, long id) return; } - fragment.setArguments(adConfiguration.toBundle()); + final Bundle bundle = adConfiguration.toBundle(); + if (!TextUtils.isEmpty(keywords)) { + bundle.putString(KEYWORDS_KEY, keywords); + } + fragment.setArguments(bundle); + + if (getFragmentManager().getBackStackEntryCount() > 0) { + getFragmentManager().popBackStack(); + } fragmentTransaction .replace(R.id.fragment_container, fragment) @@ -130,10 +186,28 @@ public void onPause() { super.onPause(); } - void addAdUnit(final MoPubSampleAdUnit moPubSampleAdUnit) { - MoPubSampleAdUnit createdAdUnit = mAdUnitDataSource.createSampleAdUnit(moPubSampleAdUnit); + @NonNull + MoPubSampleAdUnit addAdUnit(@NonNull final MoPubSampleAdUnit moPubSampleAdUnit) { + Preconditions.checkNotNull(moPubSampleAdUnit); + + final MoPubSampleAdUnit createdAdUnit = + mAdUnitDataSource.createSampleAdUnit(moPubSampleAdUnit); + + for (int i = 0; i < mAdapter.getCount(); i++) { + final MoPubSampleAdUnit currentAdUnit = mAdapter.getItem(i); + if (currentAdUnit != null && + moPubSampleAdUnit.getAdUnitId().equals(currentAdUnit.getAdUnitId()) && + moPubSampleAdUnit.getFragmentClassName().equals( + currentAdUnit.getFragmentClassName()) && + currentAdUnit.isUserDefined()) { + mAdapter.remove(currentAdUnit); + logToast(getContext(), moPubSampleAdUnit.getAdUnitId() + " replaced."); + break; + } + } mAdapter.add(createdAdUnit); mAdapter.sort(MoPubSampleAdUnit.COMPARATOR); + return createdAdUnit; } void deleteAdUnit(final MoPubSampleAdUnit moPubSampleAdUnit) { diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubSampleActivity.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubSampleActivity.java index fc175ea0b..a40e9dfba 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubSampleActivity.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubSampleActivity.java @@ -1,11 +1,12 @@ package com.mopub.simpleadsdemo; import android.annotation.TargetApi; +import android.content.Intent; import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.app.FragmentActivity; -import android.support.v4.app.FragmentManager; import android.webkit.WebView; import com.mopub.common.MoPub; @@ -37,6 +38,9 @@ private static void setWebDebugging() { } } + private MoPubListFragment mMoPubListFragment; + private Intent mDeeplinkIntent; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -63,17 +67,35 @@ protected void onCreate(Bundle savedInstanceState) { MoPub.setLocationAwareness(MoPub.LocationAwareness.TRUNCATED); MoPub.setLocationPrecision(4); - if (findViewById(R.id.fragment_container) != null) { - final MoPubListFragment listFragment = new MoPubListFragment(); - listFragment.setArguments(getIntent().getExtras()); - FragmentManager fragmentManager = getSupportFragmentManager(); - fragmentManager.beginTransaction() - .add(R.id.fragment_container, listFragment) - .commit(); - } + createMoPubListFragment(getIntent()); // Intercepts all logs including Level.FINEST so we can show a toast // that is not normally user-facing. This is only used for native ads. LoggingUtils.enableCanaryLogging(this); } + + private void createMoPubListFragment(@NonNull final Intent intent) { + if (findViewById(R.id.fragment_container) != null) { + mMoPubListFragment = new MoPubListFragment(); + mMoPubListFragment.setArguments(intent.getExtras()); + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragment_container, mMoPubListFragment).commit(); + + mDeeplinkIntent = intent; + } + } + + @Override + public void onNewIntent(@NonNull final Intent intent) { + mDeeplinkIntent = intent; + } + + @Override + public void onPostResume() { + super.onPostResume(); + if (mMoPubListFragment != null && mDeeplinkIntent != null) { + mMoPubListFragment.addAdUnitViaDeeplink(mDeeplinkIntent.getData()); + mDeeplinkIntent = null; + } + } } diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubSampleAdUnit.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubSampleAdUnit.java index f6573175d..ea3bb0d3c 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubSampleAdUnit.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/MoPubSampleAdUnit.java @@ -2,9 +2,11 @@ import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import java.util.Comparator; +import java.util.Locale; class MoPubSampleAdUnit implements Comparable { @@ -51,6 +53,35 @@ static AdType fromFragmentClassName(final String fragmentClassName) { return null; } + + @Nullable + static AdType fromDeeplinkString(@Nullable final String adType) { + if (adType == null) { + return null; + } + switch (adType.toLowerCase(Locale.US)) { + case "banner": + return BANNER; + case "interstitial": + return INTERSTITIAL; + case "mrect": + return MRECT; + case "leaderboard": + return LEADERBOARD; + case "skyscraper": + return SKYSCRAPER; + case "rewarded": + return REWARDED_VIDEO; + case "native": + return LIST_VIEW; + case "nativetableplacer": + return RECYCLER_VIEW; + case "nativecollectionplacer": + return CUSTOM_NATIVE; + default: + return null; + } + } } static final Comparator COMPARATOR = diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeGalleryFragment.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeGalleryFragment.java index b7141689d..b62f18c56 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeGalleryFragment.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeGalleryFragment.java @@ -69,6 +69,7 @@ public void onClick(View view) { final String adUnitId = mAdConfiguration.getAdUnitId(); views.mDescriptionView.setText(mAdConfiguration.getDescription()); views.mAdUnitIdView.setText(adUnitId); + views.mKeywordsField.setText(getArguments().getString(MoPubListFragment.KEYWORDS_KEY, "")); mViewPager = (ViewPager) view.findViewById(R.id.gallery_pager); // Set up a renderer for a static native ad. diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeListViewFragment.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeListViewFragment.java index b0e6cc5ed..6616d4e3c 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeListViewFragment.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeListViewFragment.java @@ -64,6 +64,7 @@ public void onClick(View view) { final String adUnitId = mAdConfiguration.getAdUnitId(); views.mDescriptionView.setText(mAdConfiguration.getDescription()); views.mAdUnitIdView.setText(adUnitId); + views.mKeywordsField.setText(getArguments().getString(MoPubListFragment.KEYWORDS_KEY, "")); final ArrayAdapter adapter = new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_1); diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeRecyclerViewFragment.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeRecyclerViewFragment.java index 472a5648e..f9b9d0951 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeRecyclerViewFragment.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/NativeRecyclerViewFragment.java @@ -80,6 +80,7 @@ public void onClick(final View v) { final String adUnitId = mAdConfiguration.getAdUnitId(); viewHolder.mDescriptionView.setText(mAdConfiguration.getDescription()); viewHolder.mAdUnitIdView.setText(adUnitId); + viewHolder.mKeywordsField.setText(getArguments().getString(MoPubListFragment.KEYWORDS_KEY, "")); final RecyclerView.Adapter originalAdapter = new DemoRecyclerAdapter(); diff --git a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/RewardedVideoDetailFragment.java b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/RewardedVideoDetailFragment.java index 09dd38189..8f66f3d2e 100644 --- a/mopub-sample/src/main/java/com/mopub/simpleadsdemo/RewardedVideoDetailFragment.java +++ b/mopub-sample/src/main/java/com/mopub/simpleadsdemo/RewardedVideoDetailFragment.java @@ -50,6 +50,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa MoPubSampleAdUnit.fromBundle(getArguments()); final View view = inflater.inflate(R.layout.interstitial_detail_fragment, container, false); final DetailFragmentViewHolder views = DetailFragmentViewHolder.fromView(view); + views.mKeywordsField.setText(getArguments().getString(MoPubListFragment.KEYWORDS_KEY, "")); hideSoftKeyboard(views.mKeywordsField); if (!sRewardedVideoInitialized) { diff --git a/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/BannerVisibilityTracker.java b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/BannerVisibilityTracker.java new file mode 100644 index 000000000..7649a5068 --- /dev/null +++ b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/BannerVisibilityTracker.java @@ -0,0 +1,294 @@ +package com.mopub.mobileads; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Handler; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import android.view.ViewTreeObserver; + +import com.mopub.common.Preconditions; +import com.mopub.common.VisibleForTesting; +import com.mopub.common.logging.MoPubLog; +import com.mopub.common.util.Dips; +import com.mopub.common.util.Views; + +import java.lang.ref.WeakReference; + +import static android.view.ViewTreeObserver.OnPreDrawListener; + +/** + * Tracks banner views to determine when they become visible, where visibility is determined by + * whether a minimum number of dips have been visible for a minimum duration, where both values are + * configured by the AdServer via headers. + */ +class BannerVisibilityTracker { + // Time interval to use for throttling visibility checks. + private static final int VISIBILITY_THROTTLE_MILLIS = 100; + + /** + * Callback when visibility conditions are satisfied. + */ + interface BannerVisibilityTrackerListener { + void onVisibilityChanged(); + } + + @NonNull @VisibleForTesting final OnPreDrawListener mOnPreDrawListener; + @NonNull @VisibleForTesting WeakReference mWeakViewTreeObserver; + + /** + * Banner view that is being tracked. + */ + @NonNull private final View mTrackedView; + + /** + * Root view of banner view being tracked. + */ + @NonNull private final View mRootView; + + /** + * Object to check actual visibility. + */ + @NonNull private final BannerVisibilityChecker mVisibilityChecker; + + /** + * Callback listener. + */ + @Nullable private BannerVisibilityTrackerListener mBannerVisibilityTrackerListener; + + /** + * Runnable to run on each visibility loop. + */ + @NonNull private final BannerVisibilityRunnable mVisibilityRunnable; + + /** + * Handler for visibility. + */ + @NonNull private final Handler mVisibilityHandler; + + /** + * Whether the visibility runnable is scheduled. + */ + private boolean mIsVisibilityScheduled; + + /** + * Whether the imp tracker has been fired already. + */ + private boolean mIsImpTrackerFired; + + @VisibleForTesting + public BannerVisibilityTracker(@NonNull final Context context, + @NonNull final View rootView, + @NonNull final View trackedView, + final int minVisibleDips, + final int minVisibleMillis) { + Preconditions.checkNotNull(rootView); + Preconditions.checkNotNull(trackedView); + + mRootView = rootView; + mTrackedView = trackedView; + + mVisibilityChecker = new BannerVisibilityChecker(minVisibleDips, minVisibleMillis); + mVisibilityHandler = new Handler(); + mVisibilityRunnable = new BannerVisibilityRunnable(); + + mOnPreDrawListener = new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + scheduleVisibilityCheck(); + return true; + } + }; + + mWeakViewTreeObserver = new WeakReference(null); + setViewTreeObserver(context, mTrackedView); + } + + private void setViewTreeObserver(@Nullable final Context context, @Nullable final View view) { + final ViewTreeObserver originalViewTreeObserver = mWeakViewTreeObserver.get(); + if (originalViewTreeObserver != null && originalViewTreeObserver.isAlive()) { + return; + } + + final View rootView = Views.getTopmostView(context, view); + if (rootView == null) { + MoPubLog.d("Unable to set Visibility Tracker due to no available root view."); + return; + } + + final ViewTreeObserver viewTreeObserver = rootView.getViewTreeObserver(); + if (!viewTreeObserver.isAlive()) { + MoPubLog.w("Visibility Tracker was unable to track views because the" + + " root view tree observer was not alive"); + return; + } + + mWeakViewTreeObserver = new WeakReference<>(viewTreeObserver); + viewTreeObserver.addOnPreDrawListener(mOnPreDrawListener); + } + + @Nullable + @Deprecated + @VisibleForTesting + BannerVisibilityTrackerListener getBannerVisibilityTrackerListener() { + return mBannerVisibilityTrackerListener; + } + + void setBannerVisibilityTrackerListener( + @Nullable final BannerVisibilityTrackerListener bannerVisibilityTrackerListener) { + mBannerVisibilityTrackerListener = bannerVisibilityTrackerListener; + } + + /** + * Destroy the visibility tracker, preventing it from future use. + */ + void destroy() { + mVisibilityHandler.removeMessages(0); + mIsVisibilityScheduled = false; + final ViewTreeObserver viewTreeObserver = mWeakViewTreeObserver.get(); + if (viewTreeObserver != null && viewTreeObserver.isAlive()) { + viewTreeObserver.removeOnPreDrawListener(mOnPreDrawListener); + } + mWeakViewTreeObserver.clear(); + mBannerVisibilityTrackerListener = null; + } + + void scheduleVisibilityCheck() { + // Tracking this directly instead of calling hasMessages directly because we measured that + // this led to slightly better performance. + if (mIsVisibilityScheduled) { + return; + } + + mIsVisibilityScheduled = true; + mVisibilityHandler.postDelayed(mVisibilityRunnable, VISIBILITY_THROTTLE_MILLIS); + } + + @NonNull + @Deprecated + @VisibleForTesting + BannerVisibilityChecker getBannerVisibilityChecker() { + return mVisibilityChecker; + } + + @NonNull + @Deprecated + @VisibleForTesting + Handler getVisibilityHandler() { + return mVisibilityHandler; + } + + @Deprecated + @VisibleForTesting + boolean isVisibilityScheduled() { + return mIsVisibilityScheduled; + } + + @Deprecated + @VisibleForTesting + boolean isImpTrackerFired() { + return mIsImpTrackerFired; + } + + class BannerVisibilityRunnable implements Runnable { + @Override + public void run() { + if (mIsImpTrackerFired) { + return; + } + + mIsVisibilityScheduled = false; + + // If the view meets the dips count requirement for visibility, then also check the + // duration requirement for visibility. + if (mVisibilityChecker.isVisible(mRootView, mTrackedView)) { + // Start the timer for duration requirement if it hasn't already. + if (!mVisibilityChecker.hasBeenVisibleYet()) { + mVisibilityChecker.setStartTimeMillis(); + } + + if (mVisibilityChecker.hasRequiredTimeElapsed()) { + if (mBannerVisibilityTrackerListener != null) { + mBannerVisibilityTrackerListener.onVisibilityChanged(); + mIsImpTrackerFired = true; + } + } + } + + // If visibility requirements are not met, check again later. + if (!mIsImpTrackerFired) { + scheduleVisibilityCheck(); + } + } + } + + static class BannerVisibilityChecker { + private int mMinVisibleDips; + private int mMinVisibleMillis; + private long mStartTimeMillis = Long.MIN_VALUE; + + // A rect to use for hit testing. Create this once to avoid excess garbage collection + private final Rect mClipRect = new Rect(); + + BannerVisibilityChecker(final int minVisibleDips, final int minVisibleMillis) { + mMinVisibleDips = minVisibleDips; + mMinVisibleMillis = minVisibleMillis; + } + + boolean hasBeenVisibleYet() { + return mStartTimeMillis != Long.MIN_VALUE; + } + + void setStartTimeMillis() { + mStartTimeMillis = SystemClock.uptimeMillis(); + } + + /** + * Whether the visible time has elapsed from the start time. + */ + boolean hasRequiredTimeElapsed() { + if (!hasBeenVisibleYet()) { + return false; + } + + return SystemClock.uptimeMillis() - mStartTimeMillis >= mMinVisibleMillis; + } + + /** + * Whether the visible dips count requirement is met. + */ + boolean isVisible(@Nullable final View rootView, @Nullable final View view) { + // ListView & GridView both call detachFromParent() for views that can be recycled for + // new data. This is one of the rare instances where a view will have a null parent for + // an extended period of time and will not be the main window. + // view.getGlobalVisibleRect() doesn't check that case, so if the view has visibility + // of View.VISIBLE but its group has no parent it is likely in the recycle bin of a + // ListView / GridView and not on screen. + if (view == null || view.getVisibility() != View.VISIBLE || rootView.getParent() == null) { + return false; + } + + // If either width or height is non-positive, the view cannot be visible. + if (view.getWidth() <= 0 || view.getHeight() <= 0) { + return false; + } + + // View completely clipped by its parents + if (!view.getGlobalVisibleRect(mClipRect)) { + return false; + } + + // Calculate area of view not clipped by any of its parents + final int widthInDips = Dips.pixelsToIntDips((float) mClipRect.width(), + view.getContext()); + final int heightInDips = Dips.pixelsToIntDips((float) mClipRect.height(), + view.getContext()); + final long visibleViewAreaInDips = (long) (widthInDips * heightInDips); + + return visibleViewAreaInDips >= mMinVisibleDips; + } + } +} + diff --git a/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/CustomEventBanner.java b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/CustomEventBanner.java index 0db1f4088..be3c00627 100644 --- a/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/CustomEventBanner.java +++ b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/CustomEventBanner.java @@ -37,7 +37,13 @@ protected abstract void loadBanner(Context context, * Called when a Custom Event is being invalidated or destroyed. Perform any final cleanup here. */ protected abstract void onInvalidate(); - + + /* + * Fire MPX impression trackers and 3rd-party impression trackers from JS. + */ + protected void trackMpxAndThirdPartyImpressions() { + } + public interface CustomEventBannerListener { /* * Your custom event subclass must call this method when it successfully loads an ad and diff --git a/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/CustomEventBannerAdapter.java b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/CustomEventBannerAdapter.java index ef72d1bae..9ae515fdc 100644 --- a/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/CustomEventBannerAdapter.java +++ b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/CustomEventBannerAdapter.java @@ -4,11 +4,14 @@ import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.TextUtils; import android.view.View; import com.mopub.common.AdReport; 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; import com.mopub.common.util.ReflectionTarget; import com.mopub.mobileads.CustomEventBanner.CustomEventBannerListener; @@ -38,6 +41,11 @@ public class CustomEventBannerAdapter implements CustomEventBannerListener { private final Runnable mTimeout; private boolean mStoredAutorefresh; + private int mImpressionMinVisibleDips = Integer.MIN_VALUE; + private int mImpressionMinVisibleMs = Integer.MIN_VALUE; + private boolean mIsVisibilityImpressionTrackingEnabled = false; + @Nullable private BannerVisibilityTracker mVisibilityTracker; + public CustomEventBannerAdapter(@NonNull MoPubView moPubView, @NonNull String className, @NonNull Map serverExtras, @@ -68,6 +76,9 @@ public void run() { // Attempt to load the JSON extras into mServerExtras. mServerExtras = new TreeMap(serverExtras); + // Parse banner impression tracking headers to determine if we are in visibility experiment + parseBannerImpressionTrackingHeaders(); + mLocalExtras = mMoPubView.getLocalExtras(); if (mMoPubView.getLocation() != null) { mLocalExtras.put("location", mMoPubView.getLocation()); @@ -107,6 +118,13 @@ void invalidate() { MoPubLog.d("Invalidating a custom event banner threw an exception", e); } } + if (mVisibilityTracker != null) { + try { + mVisibilityTracker.destroy(); + } catch (Exception e) { + MoPubLog.d("Destroying a banner visibility tracker threw an exception", e); + } + } mContext = null; mCustomEventBanner = null; mLocalExtras = null; @@ -118,6 +136,31 @@ boolean isInvalidated() { return mInvalidated; } + @Deprecated + @VisibleForTesting + int getImpressionMinVisibleDips() { + return mImpressionMinVisibleDips; + } + + @Deprecated + @VisibleForTesting + int getImpressionMinVisibleMs() { + return mImpressionMinVisibleMs; + } + + @Deprecated + @VisibleForTesting + boolean isVisibilityImpressionTrackingEnabled() { + return mIsVisibilityImpressionTrackingEnabled; + } + + @Nullable + @Deprecated + @VisibleForTesting + BannerVisibilityTracker getVisibilityTracker() { + return mVisibilityTracker; + } + private void cancelTimeout() { mHandler.removeCallbacks(mTimeout); } @@ -132,6 +175,34 @@ private int getTimeoutDelayMilliseconds() { return mMoPubView.getAdTimeoutDelay() * 1000; } + private void parseBannerImpressionTrackingHeaders() { + final String impressionMinVisibleDipsString = + mServerExtras.get(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS); + final String impressionMinVisibleMsString = + mServerExtras.get(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS); + + if (!TextUtils.isEmpty(impressionMinVisibleDipsString) + && !TextUtils.isEmpty(impressionMinVisibleMsString)) { + try { + mImpressionMinVisibleDips = Integer.parseInt(impressionMinVisibleDipsString); + } catch (NumberFormatException e) { + MoPubLog.d("Cannot parse integer from header " + + DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS); + } + + try { + mImpressionMinVisibleMs = Integer.parseInt(impressionMinVisibleMsString); + } catch (NumberFormatException e) { + MoPubLog.d("Cannot parse integer from header " + + DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS); + } + + if (mImpressionMinVisibleDips > 0 && mImpressionMinVisibleMs >= 0) { + mIsVisibilityImpressionTrackingEnabled = true; + } + } + } + /* * CustomEventBanner.Listener implementation */ @@ -145,9 +216,36 @@ public void onBannerLoaded(View bannerView) { if (mMoPubView != null) { mMoPubView.nativeAdLoaded(); + + // If visibility impression tracking is enabled for banners, fire all impression + // tracking URLs (AdServer, MPX, 3rd-party) for both HTML and MRAID banner types when + // visibility conditions are met. + // + // Else, retain old behavior of firing AdServer impression tracking URL if and only if + // banner is not HTML. + if (mIsVisibilityImpressionTrackingEnabled) { + // Set up visibility tracker and listener if in experiment + mVisibilityTracker = new BannerVisibilityTracker(mContext, mMoPubView, bannerView, + mImpressionMinVisibleDips, mImpressionMinVisibleMs); + mVisibilityTracker.setBannerVisibilityTrackerListener( + new BannerVisibilityTracker.BannerVisibilityTrackerListener() { + @Override + public void onVisibilityChanged() { + mMoPubView.trackNativeImpression(); + if (mCustomEventBanner != null) { + mCustomEventBanner.trackMpxAndThirdPartyImpressions(); + } + } + }); + } + mMoPubView.setAdContentView(bannerView); - if (!(bannerView instanceof HtmlBannerWebView)) { - mMoPubView.trackNativeImpression(); + + // Old behavior + if (!mIsVisibilityImpressionTrackingEnabled) { + if (!(bannerView instanceof HtmlBannerWebView)) { + mMoPubView.trackNativeImpression(); + } } } } diff --git a/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/HtmlBanner.java b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/HtmlBanner.java index 6104ff90a..1cf274fb5 100644 --- a/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/HtmlBanner.java +++ b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mobileads/HtmlBanner.java @@ -13,6 +13,7 @@ import java.util.Map; import static com.mopub.common.DataKeys.AD_REPORT_KEY; +import static com.mopub.common.util.JavaScriptWebViewCallbacks.WEB_VIEW_DID_APPEAR; import static com.mopub.mobileads.MoPubErrorCode.INTERNAL_ERROR; import static com.mopub.mobileads.MoPubErrorCode.NETWORK_INVALID_STATE; @@ -37,6 +38,7 @@ protected void loadBanner( redirectUrl = serverExtras.get(DataKeys.REDIRECT_URL_KEY); clickthroughUrl = serverExtras.get(DataKeys.CLICKTHROUGH_URL_KEY); isScrollable = Boolean.valueOf(serverExtras.get(DataKeys.SCROLLABLE_KEY)); + try { adReport = (AdReport) localExtras.get(AD_REPORT_KEY); } catch (ClassCastException e) { @@ -75,6 +77,11 @@ protected void onInvalidate() { } } + @Override + protected void trackMpxAndThirdPartyImpressions() { + mHtmlBannerWebView.loadUrl(WEB_VIEW_DID_APPEAR.getUrl()); + } + private boolean extrasAreValid(Map serverExtras) { return serverExtras.containsKey(DataKeys.HTML_RESPONSE_BODY_KEY); } diff --git a/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mraid/MraidBanner.java b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mraid/MraidBanner.java index 86526d22b..5e13128f5 100644 --- a/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mraid/MraidBanner.java +++ b/mopub-sdk/mopub-sdk-banner/src/main/java/com/mopub/mraid/MraidBanner.java @@ -18,6 +18,7 @@ import static com.mopub.common.DataKeys.AD_REPORT_KEY; import static com.mopub.common.DataKeys.HTML_RESPONSE_BODY_KEY; +import static com.mopub.common.util.JavaScriptWebViewCallbacks.WEB_VIEW_DID_APPEAR; import static com.mopub.mobileads.MoPubErrorCode.MRAID_LOAD_ERROR; class MraidBanner extends CustomEventBanner { @@ -95,15 +96,19 @@ public void onReady(final @NonNull MraidBridge.MraidWebView webView, @Override protected void onInvalidate() { + if (mExternalViewabilitySessionManager != null) { + mExternalViewabilitySessionManager.endDisplaySession(); + mExternalViewabilitySessionManager = null; + } if (mMraidController != null) { mMraidController.setMraidListener(null); mMraidController.destroy(); } + } - if (mExternalViewabilitySessionManager != null) { - mExternalViewabilitySessionManager.endDisplaySession(); - mExternalViewabilitySessionManager = null; - } + @Override + protected void trackMpxAndThirdPartyImpressions() { + mMraidController.loadJavascript(WEB_VIEW_DID_APPEAR.getJavascript()); } private boolean extrasAreValid(@NonNull final Map serverExtras) { diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/BrowserWebViewClient.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/BrowserWebViewClient.java index e7fc2d0cb..66de74134 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/BrowserWebViewClient.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/BrowserWebViewClient.java @@ -8,7 +8,6 @@ import android.webkit.WebViewClient; import com.mopub.common.logging.MoPubLog; -import com.mopub.exceptions.IntentNotResolvableException; import java.util.EnumSet; diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/DataKeys.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/DataKeys.java index be2193a93..1de41b771 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/DataKeys.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/DataKeys.java @@ -17,9 +17,14 @@ public class DataKeys { public static final String AD_WIDTH = "com_mopub_ad_width"; public static final String AD_HEIGHT = "com_mopub_ad_height"; + // Banner imp tracking fields + public static final String BANNER_IMPRESSION_MIN_VISIBLE_DIPS = "Banner-Impression-Min-Pixels"; + public static final String BANNER_IMPRESSION_MIN_VISIBLE_MS = "Banner-Impression-Min-Ms"; + // Native fields public static final String IMPRESSION_MIN_VISIBLE_PERCENT = "Impression-Min-Visible-Percent"; public static final String IMPRESSION_VISIBLE_MS = "Impression-Visible-Ms"; + public static final String IMPRESSION_MIN_VISIBLE_PX = "Impression-Min-Visible-Px"; // Native Video fields public static final String PLAY_VISIBLE_PERCENT = "Play-Visible-Percent"; diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java index c154a387a..e4b96b6e3 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java @@ -13,7 +13,7 @@ import static com.mopub.common.ExternalViewabilitySessionManager.ViewabilityVendor; public class MoPub { - public static final String SDK_VERSION = "4.18.0"; + public static final String SDK_VERSION = "4.19.0"; public enum LocationAwareness { NORMAL, TRUNCATED, DISABLED } diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPubBrowser.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPubBrowser.java index 59865d448..c60702bf8 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPubBrowser.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPubBrowser.java @@ -24,12 +24,12 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static com.mopub.common.event.BaseEvent.*; +import static com.mopub.common.event.BaseEvent.Category; +import static com.mopub.common.event.BaseEvent.Name; +import static com.mopub.common.event.BaseEvent.SamplingRate; import static com.mopub.common.util.Drawables.BACKGROUND; import static com.mopub.common.util.Drawables.CLOSE; -import static com.mopub.common.util.Drawables.LEFT_ARROW; import static com.mopub.common.util.Drawables.REFRESH; -import static com.mopub.common.util.Drawables.RIGHT_ARROW; import static com.mopub.common.util.Drawables.UNLEFT_ARROW; import static com.mopub.common.util.Drawables.UNRIGHT_ARROW; diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/logging/MoPubLog.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/logging/MoPubLog.java index 0eac63140..fb527feff 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/logging/MoPubLog.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/logging/MoPubLog.java @@ -1,6 +1,5 @@ package com.mopub.common.logging; -import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.util.Log; diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/ImageUtils.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/ImageUtils.java index deb8928b6..15177acd6 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/ImageUtils.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/ImageUtils.java @@ -1,10 +1,7 @@ package com.mopub.common.util; import android.graphics.Bitmap; -import android.os.Build; import android.support.annotation.NonNull; -import android.widget.ImageView; - public class ImageUtils { diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/JavaScriptWebViewCallbacks.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/JavaScriptWebViewCallbacks.java new file mode 100644 index 000000000..143dc2e7c --- /dev/null +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/JavaScriptWebViewCallbacks.java @@ -0,0 +1,22 @@ +package com.mopub.common.util; + +public enum JavaScriptWebViewCallbacks { + // The ad server appends these functions to the MRAID javascript to help with third party + // impression tracking. + WEB_VIEW_DID_APPEAR("webviewDidAppear();"), + WEB_VIEW_DID_CLOSE("webviewDidClose();"); + + private String mJavascript; + + JavaScriptWebViewCallbacks(String javascript) { + mJavascript = javascript; + } + + public String getJavascript() { + return mJavascript; + } + + public String getUrl() { + return "javascript:" + mJavascript; + } +} diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/Reflection.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/Reflection.java index e59d522a6..cf94ec5b7 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/Reflection.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/Reflection.java @@ -4,7 +4,6 @@ import android.support.annotation.Nullable; import com.mopub.common.Preconditions; -import com.mopub.common.logging.MoPubLog; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/ResponseHeader.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/ResponseHeader.java index 9c45e5154..ad909dbed 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/ResponseHeader.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/ResponseHeader.java @@ -27,9 +27,14 @@ public enum ResponseHeader { ACCEPT_LANGUAGE("Accept-Language"), BROWSER_AGENT("X-Browser-Agent"), + // Banner impression tracking fields + BANNER_IMPRESSION_MIN_VISIBLE_DIPS("X-Banner-Impression-Min-Pixels"), + BANNER_IMPRESSION_MIN_VISIBLE_MS("X-Banner-Impression-Min-Ms"), + // Native fields IMPRESSION_MIN_VISIBLE_PERCENT("X-Impression-Min-Visible-Percent"), IMPRESSION_VISIBLE_MS("X-Impression-Visible-Ms"), + IMPRESSION_MIN_VISIBLE_PX("X-Native-Impression-Min-Px"), // Native Video fields PLAY_VISIBLE_PERCENT("X-Play-Visible-Percent"), diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/Streams.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/Streams.java index 0572a7cfb..fabe64695 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/Streams.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/util/Streams.java @@ -1,5 +1,7 @@ package com.mopub.common.util; +import com.mopub.common.logging.MoPubLog; + import java.io.Closeable; import java.io.IOException; import java.io.InputStream; @@ -60,8 +62,9 @@ public static void closeStream(Closeable stream) { try { stream.close(); - } catch (IOException e) { + } catch (Exception e) { // Unable to close the stream + MoPubLog.d("Unable to close stream. Ignoring."); } } } diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/BaseVideoViewController.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/BaseVideoViewController.java index 6bc577f07..9e31439a0 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/BaseVideoViewController.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/BaseVideoViewController.java @@ -13,12 +13,13 @@ import android.widget.VideoView; import com.mopub.common.IntentActions; +import com.mopub.common.Preconditions; import com.mopub.common.logging.MoPubLog; public abstract class BaseVideoViewController { private final Context mContext; private final RelativeLayout mLayout; - private final BaseVideoViewControllerListener mBaseVideoViewControllerListener; + @NonNull private final BaseVideoViewControllerListener mBaseVideoViewControllerListener; @Nullable private Long mBroadcastIdentifier; public interface BaseVideoViewControllerListener { @@ -30,7 +31,11 @@ void onStartActivityForResult(final Class clazz, final Bundle extras); } - protected BaseVideoViewController(final Context context, @Nullable final Long broadcastIdentifier, final BaseVideoViewControllerListener baseVideoViewControllerListener) { + protected BaseVideoViewController(final Context context, + @Nullable final Long broadcastIdentifier, + @NonNull final BaseVideoViewControllerListener baseVideoViewControllerListener) { + Preconditions.checkNotNull(baseVideoViewControllerListener); + mContext = context; mBroadcastIdentifier = broadcastIdentifier; mBaseVideoViewControllerListener = baseVideoViewControllerListener; @@ -61,6 +66,7 @@ void onActivityResult(final int requestCode, final int resultCode, final Intent // By default, the activity result is ignored } + @NonNull protected BaseVideoViewControllerListener getBaseVideoViewControllerListener() { return mBaseVideoViewControllerListener; } diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/HtmlWebViewClient.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/HtmlWebViewClient.java index eda4eea5a..743c4d691 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/HtmlWebViewClient.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/HtmlWebViewClient.java @@ -4,11 +4,9 @@ import android.graphics.Bitmap; import android.net.Uri; import android.support.annotation.NonNull; -import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; -import com.mopub.common.Preconditions; import com.mopub.common.UrlAction; import com.mopub.common.UrlHandler; import com.mopub.common.logging.MoPubLog; diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/MoatBuyerTagXmlManager.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/MoatBuyerTagXmlManager.java index 5472c9718..18a3a137c 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/MoatBuyerTagXmlManager.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/MoatBuyerTagXmlManager.java @@ -6,7 +6,6 @@ import com.mopub.common.Preconditions; import com.mopub.mobileads.util.XmlUtils; -import org.w3c.dom.Element; import org.w3c.dom.Node; import java.util.HashSet; diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastVideoBlurLastVideoFrameTask.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastVideoBlurLastVideoFrameTask.java index dcbcbba99..dbf37d7f4 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastVideoBlurLastVideoFrameTask.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastVideoBlurLastVideoFrameTask.java @@ -3,7 +3,6 @@ import android.graphics.Bitmap; import android.media.MediaMetadataRetriever; import android.os.AsyncTask; -import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.widget.ImageView; diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastVideoView.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastVideoView.java index d4f9949d3..a34b447bb 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastVideoView.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastVideoView.java @@ -3,7 +3,6 @@ import android.content.Context; import android.media.MediaMetadataRetriever; import android.os.AsyncTask; -import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.widget.ImageView; diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastWrapperXmlManager.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastWrapperXmlManager.java index 0bf3c15ae..a70280797 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastWrapperXmlManager.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/VastWrapperXmlManager.java @@ -8,9 +8,6 @@ import org.w3c.dom.Node; -import java.util.ArrayList; -import java.util.List; - /** * This XML manager handles Wrapper nodes. Wrappers redirect to other VAST documents (which may * in turn redirect to more wrappers). Wrappers can also contain impression trackers, diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/resource/CloseButtonDrawable.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/resource/CloseButtonDrawable.java index 0cdb4fa73..123f33e34 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/resource/CloseButtonDrawable.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/mobileads/resource/CloseButtonDrawable.java @@ -1,7 +1,6 @@ package com.mopub.mobileads.resource; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Paint; public class CloseButtonDrawable extends BaseWidgetDrawable { diff --git a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/network/AdRequest.java b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/network/AdRequest.java index 45287b987..bd4ba50e7 100644 --- a/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/network/AdRequest.java +++ b/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/network/AdRequest.java @@ -243,6 +243,8 @@ protected Response parseNetworkResponse(final NetworkResponse networ ResponseHeader.IMPRESSION_MIN_VISIBLE_PERCENT); final String impressionVisibleMS = extractHeader(headers, ResponseHeader.IMPRESSION_VISIBLE_MS); + final String impressionMinVisiblePx = extractHeader(headers, + ResponseHeader.IMPRESSION_MIN_VISIBLE_PX); if (!TextUtils.isEmpty(impressionMinVisiblePercent)) { serverExtras.put(DataKeys.IMPRESSION_MIN_VISIBLE_PERCENT, impressionMinVisiblePercent); @@ -250,6 +252,9 @@ protected Response parseNetworkResponse(final NetworkResponse networ if (!TextUtils.isEmpty(impressionVisibleMS)) { serverExtras.put(DataKeys.IMPRESSION_VISIBLE_MS, impressionVisibleMS); } + if (!TextUtils.isEmpty(impressionMinVisiblePx)) { + serverExtras.put(DataKeys.IMPRESSION_MIN_VISIBLE_PX, impressionMinVisiblePx); + } } if (AdType.VIDEO_NATIVE.equals(adTypeString)) { serverExtras.put(DataKeys.PLAY_VISIBLE_PERCENT, @@ -289,6 +294,14 @@ protected Response parseNetworkResponse(final NetworkResponse networ extractHeader(headers, ResponseHeader.VIDEO_VIEWABILITY_TRACKERS)); } + // Banner imp tracking + if (AdFormat.BANNER.equals(mAdFormat)) { + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, + extractHeader(headers, ResponseHeader.BANNER_IMPRESSION_MIN_VISIBLE_MS)); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, + extractHeader(headers, ResponseHeader.BANNER_IMPRESSION_MIN_VISIBLE_DIPS)); + } + // Disable viewability vendors, if any final String disabledViewabilityVendors = extractHeader(headers, ResponseHeader.DISABLE_VIEWABILITY); diff --git a/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/BaseInterstitialActivity.java b/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/BaseInterstitialActivity.java index f5208bb00..3e4f0c175 100644 --- a/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/BaseInterstitialActivity.java +++ b/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/BaseInterstitialActivity.java @@ -18,27 +18,6 @@ abstract class BaseInterstitialActivity extends Activity { @Nullable protected AdReport mAdReport; - - enum JavaScriptWebViewCallbacks { - // The ad server appends these functions to the MRAID javascript to help with third party - // impression tracking. - WEB_VIEW_DID_APPEAR("webviewDidAppear();"), - WEB_VIEW_DID_CLOSE("webviewDidClose();"); - - private String mJavascript; - private JavaScriptWebViewCallbacks(String javascript) { - mJavascript = javascript; - } - - protected String getJavascript() { - return mJavascript; - } - - protected String getUrl() { - return "javascript:" + mJavascript; - } - } - @Nullable private CloseableLayout mCloseableLayout; @Nullable private Long mBroadcastIdentifier; diff --git a/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/MoPubActivity.java b/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/MoPubActivity.java index 886624a9a..d5f51e1fe 100644 --- a/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/MoPubActivity.java +++ b/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/MoPubActivity.java @@ -31,8 +31,8 @@ import static com.mopub.common.IntentActions.ACTION_INTERSTITIAL_DISMISS; import static com.mopub.common.IntentActions.ACTION_INTERSTITIAL_FAIL; import static com.mopub.common.IntentActions.ACTION_INTERSTITIAL_SHOW; -import static com.mopub.mobileads.BaseInterstitialActivity.JavaScriptWebViewCallbacks.WEB_VIEW_DID_APPEAR; -import static com.mopub.mobileads.BaseInterstitialActivity.JavaScriptWebViewCallbacks.WEB_VIEW_DID_CLOSE; +import static com.mopub.common.util.JavaScriptWebViewCallbacks.WEB_VIEW_DID_APPEAR; +import static com.mopub.common.util.JavaScriptWebViewCallbacks.WEB_VIEW_DID_CLOSE; import static com.mopub.mobileads.CustomEventInterstitial.CustomEventInterstitialListener; import static com.mopub.mobileads.EventForwardingBroadcastReceiver.broadcastAction; import static com.mopub.mobileads.HtmlWebViewClient.MOPUB_FAIL_LOAD; @@ -167,14 +167,14 @@ protected void onCreate(Bundle savedInstanceState) { @Override protected void onDestroy() { - if (mHtmlInterstitialWebView != null) { - mHtmlInterstitialWebView.loadUrl(WEB_VIEW_DID_CLOSE.getUrl()); - mHtmlInterstitialWebView.destroy(); - } if (mExternalViewabilitySessionManager != null) { mExternalViewabilitySessionManager.endDisplaySession(); mExternalViewabilitySessionManager = null; } + if (mHtmlInterstitialWebView != null) { + mHtmlInterstitialWebView.loadUrl(WEB_VIEW_DID_CLOSE.getUrl()); + mHtmlInterstitialWebView.destroy(); + } broadcastAction(this, getBroadcastIdentifier(), ACTION_INTERSTITIAL_DISMISS); super.onDestroy(); } diff --git a/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/MraidActivity.java b/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/MraidActivity.java index bb9b44c49..a2bee5e71 100644 --- a/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/MraidActivity.java +++ b/mopub-sdk/mopub-sdk-interstitial/src/main/java/com/mopub/mobileads/MraidActivity.java @@ -34,8 +34,8 @@ import static com.mopub.common.IntentActions.ACTION_INTERSTITIAL_DISMISS; import static com.mopub.common.IntentActions.ACTION_INTERSTITIAL_FAIL; import static com.mopub.common.IntentActions.ACTION_INTERSTITIAL_SHOW; -import static com.mopub.mobileads.BaseInterstitialActivity.JavaScriptWebViewCallbacks.WEB_VIEW_DID_APPEAR; -import static com.mopub.mobileads.BaseInterstitialActivity.JavaScriptWebViewCallbacks.WEB_VIEW_DID_CLOSE; +import static com.mopub.common.util.JavaScriptWebViewCallbacks.WEB_VIEW_DID_APPEAR; +import static com.mopub.common.util.JavaScriptWebViewCallbacks.WEB_VIEW_DID_CLOSE; import static com.mopub.mobileads.EventForwardingBroadcastReceiver.broadcastAction; import static com.mopub.mobileads.HtmlWebViewClient.MOPUB_FAIL_LOAD; @@ -240,13 +240,14 @@ protected void onResume() { @Override protected void onDestroy() { - if (mMraidController != null) { - mMraidController.destroy(); - } if (mExternalViewabilitySessionManager != null) { mExternalViewabilitySessionManager.endDisplaySession(); mExternalViewabilitySessionManager = null; } + if (mMraidController != null) { + mMraidController.destroy(); + } + if (getBroadcastIdentifier()!= null) { broadcastAction(this, getBroadcastIdentifier(), ACTION_INTERSTITIAL_DISMISS); } diff --git a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/ImpressionInterface.java b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/ImpressionInterface.java index 6f84021f3..6931c4148 100644 --- a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/ImpressionInterface.java +++ b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/ImpressionInterface.java @@ -8,6 +8,7 @@ */ public interface ImpressionInterface { int getImpressionMinPercentageViewed(); + Integer getImpressionMinVisiblePx(); int getImpressionMinTimeViewed(); void recordImpression(View view); boolean isImpressionRecorded(); diff --git a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/ImpressionTracker.java b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/ImpressionTracker.java index d7775eab0..d49896363 100644 --- a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/ImpressionTracker.java +++ b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/ImpressionTracker.java @@ -117,7 +117,8 @@ public void addView(final View view, @NonNull final ImpressionInterface impressi } mTrackedViews.put(view, impressionInterface); - mVisibilityTracker.addView(view, impressionInterface.getImpressionMinPercentageViewed()); + mVisibilityTracker.addView(view, impressionInterface.getImpressionMinPercentageViewed(), + impressionInterface.getImpressionMinVisiblePx()); } public void removeView(final View view) { diff --git a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubAdAdapter.java b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubAdAdapter.java index 18ec136cd..c1aaa849e 100644 --- a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubAdAdapter.java +++ b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubAdAdapter.java @@ -342,7 +342,7 @@ public View getView(final int position, final View view, final ViewGroup viewGro mStreamAdPlacer.getOriginalPosition(position), view, viewGroup); } mViewPositionMap.put(resultView, position); - mVisibilityTracker.addView(resultView, 0); + mVisibilityTracker.addView(resultView, 0, null); return resultView; } diff --git a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubCustomEventNative.java b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubCustomEventNative.java index 02f935415..682097272 100644 --- a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubCustomEventNative.java +++ b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubCustomEventNative.java @@ -67,6 +67,16 @@ protected void loadNativeAd(@NonNull final Context context, } } + if (serverExtras.containsKey(DataKeys.IMPRESSION_MIN_VISIBLE_PX)) { + try { + moPubStaticNativeAd.setImpressionMinVisiblePx(Integer.parseInt( + serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PX))); + } catch (final NumberFormatException e) { + MoPubLog.d("Unable to format min visible px: " + + serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PX)); + } + } + try { moPubStaticNativeAd.loadAd(); } catch (IllegalArgumentException e) { diff --git a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubRecyclerAdapter.java b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubRecyclerAdapter.java index 21d2e3d2d..7afb0337d 100644 --- a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubRecyclerAdapter.java +++ b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/MoPubRecyclerAdapter.java @@ -316,7 +316,6 @@ public void refreshAds(@NonNull String adUnitId, loadAds(adUnitId, requestParameters); } else { MoPubLog.w("This LayoutManager can't be refreshed."); - return; } } @@ -420,7 +419,7 @@ public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int pos } mViewPositionMap.put(holder.itemView, position); - mVisibilityTracker.addView(holder.itemView, 0); + mVisibilityTracker.addView(holder.itemView, 0, null); //noinspection unchecked mOriginalAdapter.onBindViewHolder(holder, mStreamAdPlacer.getOriginalPosition(position)); diff --git a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/SpinningProgressView.java b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/SpinningProgressView.java index 9fa5d9fff..ff2204401 100644 --- a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/SpinningProgressView.java +++ b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/SpinningProgressView.java @@ -3,7 +3,6 @@ import android.content.Context; import android.graphics.Color; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; diff --git a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/StaticNativeAd.java b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/StaticNativeAd.java index d4ed19ef5..a2b90337b 100644 --- a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/StaticNativeAd.java +++ b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/StaticNativeAd.java @@ -35,6 +35,7 @@ public abstract class StaticNativeAd extends BaseNativeAd implements ImpressionI private boolean mImpressionRecorded; private int mImpressionMinTimeViewed; private int mImpressionMinPercentageViewed; + private Integer mImpressionMinVisiblePx; // Extras @NonNull private final Map mExtras; @@ -42,6 +43,7 @@ public abstract class StaticNativeAd extends BaseNativeAd implements ImpressionI public StaticNativeAd() { mImpressionMinTimeViewed = DEFAULT_IMPRESSION_MIN_TIME_VIEWED_MS; mImpressionMinPercentageViewed = DEFAULT_IMPRESSION_MIN_PERCENTAGE_VIEWED; + mImpressionMinVisiblePx = null; mExtras = new HashMap(); } @@ -213,7 +215,7 @@ final public void setImpressionMinTimeViewed(final int impressionMinTimeViewed) if (impressionMinTimeViewed > 0) { mImpressionMinTimeViewed = impressionMinTimeViewed; } else { - MoPubLog.d("Ignoring non-positive impressionMinTimeViewed"); + MoPubLog.d("Ignoring non-positive impressionMinTimeViewed: " + impressionMinTimeViewed); } } @@ -227,7 +229,23 @@ final public void setImpressionMinPercentageViewed(final int impressionMinPercen if (impressionMinPercentageViewed >= 0 && impressionMinPercentageViewed <= 100) { mImpressionMinPercentageViewed = impressionMinPercentageViewed; } else { - MoPubLog.d("Ignoring impressionMinTimeViewed that's not a percent [0, 100]"); + MoPubLog.d("Ignoring impressionMinTimeViewed that's not a percent [0, 100]: " + + impressionMinPercentageViewed); + } + } + + /** + * Sets the minimum number of pixels of the ad to be on screen before impression trackers are + * fired. This must be an Integer greater than 0. + * + * @param impressionMinVisiblePx Number of pixels of an ad (ignored if negative or 0). + */ + final public void setImpressionMinVisiblePx(@Nullable final Integer impressionMinVisiblePx) { + if (impressionMinVisiblePx != null && impressionMinVisiblePx > 0) { + mImpressionMinVisiblePx = impressionMinVisiblePx; + } else { + MoPubLog.d("Ignoring null or non-positive impressionMinVisiblePx: " + + impressionMinVisiblePx); } } @@ -271,6 +289,17 @@ final public int getImpressionMinTimeViewed() { return mImpressionMinTimeViewed; } + /** + * Returns the minimum viewable number of pixels of the ad that must be onscreen for it to be + * considered visible. This value, if present and positive will override the min percentage. + * See {@link StaticNativeAd#getImpressionMinTimeViewed()} for additional impression + * tracking considerations. + */ + @Override + final public Integer getImpressionMinVisiblePx() { + return mImpressionMinVisiblePx; + } + @Override final public boolean isImpressionRecorded() { return mImpressionRecorded; diff --git a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/TaskManager.java b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/TaskManager.java index a13163c77..4b9df9eea 100644 --- a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/TaskManager.java +++ b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/TaskManager.java @@ -1,10 +1,8 @@ package com.mopub.nativeads; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import com.mopub.common.Preconditions; -import com.mopub.common.Preconditions.NoThrow; import java.util.Collections; import java.util.HashMap; diff --git a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/VisibilityTracker.java b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/VisibilityTracker.java index 7453d2c4d..424e97d05 100644 --- a/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/VisibilityTracker.java +++ b/mopub-sdk/mopub-sdk-native-static/src/main/java/com/mopub/nativeads/VisibilityTracker.java @@ -54,6 +54,12 @@ static class TrackingInfo { int mMaxInvisiblePercent; long mAccessOrder; View mRootView; + + /** + * If this number is set, then use this as the minimum amount of the view seen before it is + * considered visible. This is in real pixels. + */ + @Nullable Integer mMinVisiblePx; } // Views that are being tracked, mapped to the min viewable percentage @@ -135,15 +141,19 @@ void setVisibilityTrackerListener( /** * Tracks the given view for visibility. */ - void addView(@NonNull final View view, final int minPercentageViewed) { - addView(view, view, minPercentageViewed); + void addView(@NonNull final View view, final int minPercentageViewed, + @Nullable final Integer minVisiblePx) { + addView(view, view, minPercentageViewed, minVisiblePx); } - void addView(@NonNull View rootView, @NonNull final View view, final int minPercentageViewed) { - addView(rootView, view, minPercentageViewed, minPercentageViewed); + void addView(@NonNull View rootView, @NonNull final View view, final int minPercentageViewed, + @Nullable final Integer minVisiblePx) { + addView(rootView, view, minPercentageViewed, minPercentageViewed, minVisiblePx); } - void addView(@NonNull View rootView, @NonNull final View view, final int minVisiblePercentageViewed, final int maxInvisiblePercentageViewed) { + void addView(@NonNull View rootView, @NonNull final View view, + final int minVisiblePercentageViewed, final int maxInvisiblePercentageViewed, + @Nullable final Integer minVisiblePx) { setViewTreeObserver(view.getContext(), view); // Find the view if already tracked @@ -160,6 +170,7 @@ void addView(@NonNull View rootView, @NonNull final View view, final int minVisi trackingInfo.mMinViewablePercent = minVisiblePercentageViewed; trackingInfo.mMaxInvisiblePercent = maxInvisiblePercent; trackingInfo.mAccessOrder = mAccessCounter; + trackingInfo.mMinVisiblePx = minVisiblePx; // Trim the number of tracked views to a reasonable number mAccessCounter++; @@ -240,11 +251,14 @@ public void run() { final View view = entry.getKey(); final int minPercentageViewed = entry.getValue().mMinViewablePercent; final int maxInvisiblePercent = entry.getValue().mMaxInvisiblePercent; + final Integer minVisiblePx = entry.getValue().mMinVisiblePx; final View rootView = entry.getValue().mRootView; - if (mVisibilityChecker.isVisible(rootView, view, minPercentageViewed)) { + if (mVisibilityChecker.isVisible(rootView, view, minPercentageViewed, + minVisiblePx)) { mVisibleViews.add(view); - } else if (!mVisibilityChecker.isVisible(rootView, view, maxInvisiblePercent)){ + } else if (!mVisibilityChecker.isVisible(rootView, view, maxInvisiblePercent, + null)) { mInvisibleViews.add(view); } } @@ -271,9 +285,11 @@ boolean hasRequiredTimeElapsed(final long startTimeMillis, final int minTimeView } /** - * Whether the view is at least certain % visible + * Whether the view is at least certain amount visible. If the min pixel amount is set, + * use that. Otherwise, use the min percentage visible. */ - boolean isVisible(@Nullable final View rootView, @Nullable final View view, final int minPercentageViewed) { + boolean isVisible(@Nullable final View rootView, @Nullable final View view, + final int minPercentageViewed, @Nullable final Integer minVisiblePx) { // ListView & GridView both call detachFromParent() for views that can be recycled for // new data. This is one of the rare instances where a view will have a null parent for // an extended period of time and will not be the main window. @@ -297,6 +313,10 @@ boolean isVisible(@Nullable final View rootView, @Nullable final View view, fina return false; } + if (minVisiblePx != null && minVisiblePx > 0) { + return visibleViewArea >= minVisiblePx; + } + return 100 * visibleViewArea >= minPercentageViewed * totalViewArea; } } diff --git a/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/MoPubCustomEventVideoNative.java b/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/MoPubCustomEventVideoNative.java index dc9cefcb6..ac405b116 100644 --- a/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/MoPubCustomEventVideoNative.java +++ b/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/MoPubCustomEventVideoNative.java @@ -42,6 +42,7 @@ import static com.mopub.common.DataKeys.EVENT_DETAILS; import static com.mopub.common.DataKeys.IMPRESSION_MIN_VISIBLE_PERCENT; +import static com.mopub.common.DataKeys.IMPRESSION_MIN_VISIBLE_PX; import static com.mopub.common.DataKeys.IMPRESSION_VISIBLE_MS; import static com.mopub.common.DataKeys.JSON_BODY_KEY; import static com.mopub.common.DataKeys.MAX_BUFFER_MS; @@ -305,6 +306,8 @@ public void onVastVideoConfigurationPrepared(@Nullable VastVideoConfig vastVideo visibilityTrackingEvent.totalRequiredPlayTimeMs = mVideoResponseHeaders.getImpressionVisibleMs(); visibilityTrackingEvents.add(visibilityTrackingEvent); + visibilityTrackingEvent.minimumVisiblePx = + mVideoResponseHeaders.getImpressionVisiblePx(); // VAST impression trackers for (final VastTracker vastTracker : vastVideoConfig.getImpressionTrackers()) { @@ -317,6 +320,8 @@ public void onVastVideoConfigurationPrepared(@Nullable VastVideoConfig vastVideo vastImpressionTrackingEvent.totalRequiredPlayTimeMs = mVideoResponseHeaders.getImpressionVisibleMs(); visibilityTrackingEvents.add(vastImpressionTrackingEvent); + vastImpressionTrackingEvent.minimumVisiblePx = + mVideoResponseHeaders.getImpressionVisiblePx(); } // Visibility tracking event from http response Vast payload @@ -435,7 +440,8 @@ public void render(@NonNull MediaLayout mediaLayout) { mVideoVisibleTracking.addView(mRootView, mediaLayout, mVideoResponseHeaders.getPlayVisiblePercent(), - mVideoResponseHeaders.getPauseVisiblePercent()); + mVideoResponseHeaders.getPauseVisiblePercent(), + mVideoResponseHeaders.getImpressionVisiblePx()); mMediaLayout = mediaLayout; mMediaLayout.initForVideo(); @@ -905,14 +911,13 @@ static class VideoResponseHeaders { private int mImpressionMinVisiblePercent; private int mImpressionVisibleMs; private int mMaxBufferMs; + private Integer mImpressionVisiblePx; private JSONObject mVideoTrackers; VideoResponseHeaders(@NonNull final Map serverExtras) { try { mPlayVisiblePercent = Integer.parseInt(serverExtras.get(PLAY_VISIBLE_PERCENT)); mPauseVisiblePercent = Integer.parseInt(serverExtras.get(PAUSE_VISIBLE_PERCENT)); - mImpressionMinVisiblePercent = - Integer.parseInt(serverExtras.get(IMPRESSION_MIN_VISIBLE_PERCENT)); mImpressionVisibleMs = Integer.parseInt(serverExtras.get(IMPRESSION_VISIBLE_MS)); mMaxBufferMs = Integer.parseInt(serverExtras.get(MAX_BUFFER_MS)); mHeadersAreValid = true; @@ -920,6 +925,25 @@ static class VideoResponseHeaders { mHeadersAreValid = false; } + final String impressionVisiblePxString = serverExtras.get(IMPRESSION_MIN_VISIBLE_PX); + if (!TextUtils.isEmpty(impressionVisiblePxString)) { + try { + mImpressionVisiblePx = Integer.parseInt(impressionVisiblePxString); + } catch (NumberFormatException e) { + MoPubLog.d("Unable to parse impression min visible px from server extras."); + } + } + try { + mImpressionMinVisiblePercent = + Integer.parseInt(serverExtras.get(IMPRESSION_MIN_VISIBLE_PERCENT)); + } catch (NumberFormatException e) { + MoPubLog.d("Unable to parse impression min visible percent from server extras."); + if (mImpressionVisiblePx == null || mImpressionVisiblePx < 0) { + mHeadersAreValid = false; + } + } + + final String videoTrackers = serverExtras.get(VIDEO_TRACKERS_KEY); if (TextUtils.isEmpty(videoTrackers)) { return; @@ -957,6 +981,11 @@ int getMaxBufferMs() { return mMaxBufferMs; } + @Nullable + Integer getImpressionVisiblePx() { + return mImpressionVisiblePx; + } + JSONObject getVideoTrackers() { return mVideoTrackers; } diff --git a/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/NativeFullScreenVideoView.java b/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/NativeFullScreenVideoView.java index 0b7c6d0b2..eb3a94fff 100644 --- a/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/NativeFullScreenVideoView.java +++ b/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/NativeFullScreenVideoView.java @@ -422,7 +422,7 @@ public void setColorFilter(ColorFilter cf) { } public int getOpacity() { return PixelFormat.UNKNOWN; } - }; + } @Deprecated @VisibleForTesting diff --git a/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/NativeVideoController.java b/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/NativeVideoController.java index 94f66ef71..d2e8f3e0d 100644 --- a/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/NativeVideoController.java +++ b/mopub-sdk/mopub-sdk-native-video/src/main/java/com/mopub/nativeads/NativeVideoController.java @@ -42,6 +42,7 @@ import com.mopub.common.event.Event; import com.mopub.common.event.EventDetails; import com.mopub.common.event.MoPubEvents; +import com.mopub.common.logging.MoPubLog; import com.mopub.mobileads.RepeatingHandlerRunnable; import com.mopub.mobileads.VastTracker; import com.mopub.mobileads.VastVideoConfig; @@ -310,6 +311,11 @@ public void onLoadingChanged(boolean isLoading) {} @Override public void onPlayerStateChanged(final boolean playWhenReady, final int newState) { if (newState == STATE_ENDED && mFinalFrame == null) { + if (mExoPlayer == null || mSurface == null || mTextureView == null) { + MoPubLog.w("onPlayerStateChanged called afer view has been recycled."); + return; + } + mFinalFrame = new BitmapDrawable(mContext.getResources(), mTextureView.getBitmap()); mNativeVideoProgressRunnable.requestStop(); } @@ -520,6 +526,7 @@ interface OnTrackedStrategy { int totalRequiredPlayTimeMs; int totalQualifiedPlayCounter; boolean isTracked; + Integer minimumVisiblePx; } static class NativeVideoProgressRunnable extends RepeatingHandlerRunnable { @@ -607,7 +614,7 @@ void checkImpressionTrackers(final boolean forceTrigger) { continue; } if (forceTrigger || mVisibilityChecker.isVisible(mTextureView, mTextureView, - event.minimumPercentageVisible)) { + event.minimumPercentageVisible, event.minimumVisiblePx)) { event.totalQualifiedPlayCounter += mUpdateIntervalMillis; if (forceTrigger || event.totalQualifiedPlayCounter >= event.totalRequiredPlayTimeMs) { diff --git a/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedAd.java b/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedAd.java index 62dd02dbf..7298bf8cb 100644 --- a/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedAd.java +++ b/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedAd.java @@ -25,6 +25,7 @@ public abstract class MoPubRewardedAd extends CustomEventRewardedAd { private boolean mIsLoaded; @Nullable private String mRewardedAdCurrencyName; private int mRewardedAdCurrencyAmount; + @Nullable protected String mAdUnitId; @Nullable @Override @@ -84,6 +85,13 @@ protected void loadWithSdkInitialized(@NonNull final Activity activity, MoPubReward.DEFAULT_REWARD_AMOUNT); mRewardedAdCurrencyAmount = MoPubReward.DEFAULT_REWARD_AMOUNT; } + + final Object adUnitId = localExtras.get(DataKeys.AD_UNIT_ID_KEY); + if (adUnitId instanceof String) { + mAdUnitId = (String) adUnitId; + } else { + MoPubLog.d("Unable to set ad unit for rewarded ad."); + } } @Override diff --git a/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedPlayable.java b/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedPlayable.java index 6af773a2d..963ec1cba 100644 --- a/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedPlayable.java +++ b/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedPlayable.java @@ -16,7 +16,7 @@ */ public class MoPubRewardedPlayable extends MoPubRewardedAd { - @NonNull private static final String MOPUB_REWARDED_PLAYABLE_ID = "mopub_rewarded_playable_id"; + @NonNull static final String MOPUB_REWARDED_PLAYABLE_ID = "mopub_rewarded_playable_id"; @Nullable private RewardedMraidInterstitial mRewardedMraidInterstitial; public MoPubRewardedPlayable() { @@ -40,7 +40,7 @@ protected void loadWithSdkInitialized(@NonNull final Activity activity, @NonNull @Override protected String getAdNetworkId() { - return MOPUB_REWARDED_PLAYABLE_ID; + return mAdUnitId != null ? mAdUnitId : MOPUB_REWARDED_PLAYABLE_ID; } @Override diff --git a/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedVideo.java b/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedVideo.java index e85a0e92d..ca70f8b3f 100644 --- a/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedVideo.java +++ b/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/MoPubRewardedVideo.java @@ -15,7 +15,7 @@ */ public class MoPubRewardedVideo extends MoPubRewardedAd { - @NonNull private static final String MOPUB_REWARDED_VIDEO_ID = "mopub_rewarded_video_id"; + @NonNull static final String MOPUB_REWARDED_VIDEO_ID = "mopub_rewarded_video_id"; @Nullable private RewardedVastVideoInterstitial mRewardedVastVideoInterstitial; @@ -26,7 +26,7 @@ public MoPubRewardedVideo() { @NonNull @Override protected String getAdNetworkId() { - return MOPUB_REWARDED_VIDEO_ID; + return mAdUnitId != null ? mAdUnitId : MOPUB_REWARDED_VIDEO_ID; } @Override diff --git a/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/RewardedMraidActivity.java b/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/RewardedMraidActivity.java index a5bf95a00..5855cc691 100644 --- a/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/RewardedMraidActivity.java +++ b/mopub-sdk/mopub-sdk-rewardedvideo/src/main/java/com/mopub/mobileads/RewardedMraidActivity.java @@ -26,8 +26,8 @@ import static com.mopub.common.DataKeys.SHOULD_REWARD_ON_CLICK_KEY; import static com.mopub.common.IntentActions.ACTION_INTERSTITIAL_CLICK; import static com.mopub.common.IntentActions.ACTION_INTERSTITIAL_FAIL; -import static com.mopub.mobileads.BaseInterstitialActivity.JavaScriptWebViewCallbacks.WEB_VIEW_DID_APPEAR; -import static com.mopub.mobileads.BaseInterstitialActivity.JavaScriptWebViewCallbacks.WEB_VIEW_DID_CLOSE; +import static com.mopub.common.util.JavaScriptWebViewCallbacks.WEB_VIEW_DID_APPEAR; +import static com.mopub.common.util.JavaScriptWebViewCallbacks.WEB_VIEW_DID_CLOSE; import static com.mopub.mobileads.EventForwardingBroadcastReceiver.broadcastAction; public class RewardedMraidActivity extends MraidActivity { diff --git a/mopub-sdk/publisher.gradle b/mopub-sdk/publisher.gradle index 2744493a1..9c40f84aa 100644 --- a/mopub-sdk/publisher.gradle +++ b/mopub-sdk/publisher.gradle @@ -23,7 +23,7 @@ android.libraryVariants.all { variant -> task.dependsOn variant.javaCompile task.from variant.javaCompile.destinationDir - artifacts.add('archives', task); + artifacts.add('archives', task) } android.libraryVariants.all { variant -> diff --git a/mopub-sdk/shared-build.gradle b/mopub-sdk/shared-build.gradle index d5d31f36f..7a2d33404 100644 --- a/mopub-sdk/shared-build.gradle +++ b/mopub-sdk/shared-build.gradle @@ -11,7 +11,7 @@ repositories { } project.group = 'com.mopub' -project.version = '4.18.0' +project.version = '4.19.0' android { compileSdkVersion 26 @@ -20,7 +20,7 @@ android { useLibrary 'org.apache.http.legacy' defaultConfig { - versionCode 56 + versionCode 57 versionName version minSdkVersion 16 targetSdkVersion 26 diff --git a/mopub-sdk/src/main/resources/fabric/com.mopub.sdk.android.mopub.properties b/mopub-sdk/src/main/resources/fabric/com.mopub.sdk.android.mopub.properties index 6b29df7ac..208a50026 100644 --- a/mopub-sdk/src/main/resources/fabric/com.mopub.sdk.android.mopub.properties +++ b/mopub-sdk/src/main/resources/fabric/com.mopub.sdk.android.mopub.properties @@ -1,3 +1,3 @@ fabric-identifier=com.mopub.sdk.android:mopub -fabric-version=4.18.0+kit +fabric-version=4.19.0+kit fabric-build-type=source diff --git a/mopub-sdk/src/test/java/com/mopub/common/UrlHandlerTest.java b/mopub-sdk/src/test/java/com/mopub/common/UrlHandlerTest.java index 66ccbdd2b..909292e8f 100644 --- a/mopub-sdk/src/test/java/com/mopub/common/UrlHandlerTest.java +++ b/mopub-sdk/src/test/java/com/mopub/common/UrlHandlerTest.java @@ -13,7 +13,6 @@ import com.mopub.network.Networking; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/BannerVisibilityTrackerTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/BannerVisibilityTrackerTest.java new file mode 100644 index 000000000..73423b795 --- /dev/null +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/BannerVisibilityTrackerTest.java @@ -0,0 +1,310 @@ +package com.mopub.mobileads; + +import android.app.Activity; +import android.graphics.Rect; +import android.os.Handler; +import android.view.View; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.Window; + +import com.mopub.common.test.support.SdkTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowSystemClock; + +import static android.view.ViewTreeObserver.OnPreDrawListener; +import static com.mopub.mobileads.BannerVisibilityTracker.BannerVisibilityChecker; +import static com.mopub.mobileads.BannerVisibilityTracker.BannerVisibilityTrackerListener; +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(SdkTestRunner.class) +@Config(constants = BuildConfig.class) +public class BannerVisibilityTrackerTest { + private static final int MIN_VISIBLE_DIPS = 1; + private static final int MIN_VISIBLE_MILLIS = 0; + + private Activity activity; + private BannerVisibilityTracker subject; + private BannerVisibilityChecker visibilityChecker; + private Handler visibilityHandler; + + private View mockView; + @Mock + private BannerVisibilityTrackerListener visibilityTrackerListener; + + @Before + public void setUp() throws Exception { + activity = Robolectric.buildActivity(Activity.class).create().get(); + mockView = createViewMock(View.VISIBLE, 100, 100, 100, 100, true, true); + subject = new BannerVisibilityTracker(activity, mockView, mockView, MIN_VISIBLE_DIPS, MIN_VISIBLE_MILLIS); + + subject.setBannerVisibilityTrackerListener(visibilityTrackerListener); + + visibilityChecker = subject.getBannerVisibilityChecker(); + visibilityHandler = subject.getVisibilityHandler(); + + // XXX We need this to ensure that our SystemClock starts + ShadowSystemClock.uptimeMillis(); + } + + @Test + public void constructor_shouldSetOnPreDrawListenerForDecorView() throws Exception { + Activity spyActivity = spy(Robolectric.buildActivity(Activity.class).create().get()); + Window window = mock(Window.class); + View decorView = mock(View.class); + ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); + + when(spyActivity.getWindow()).thenReturn(window); + when(window.getDecorView()).thenReturn(decorView); + when(decorView.findViewById(anyInt())).thenReturn(decorView); + when(decorView.getViewTreeObserver()).thenReturn(viewTreeObserver); + when(viewTreeObserver.isAlive()).thenReturn(true); + + subject = new BannerVisibilityTracker(spyActivity, mockView, mockView, MIN_VISIBLE_DIPS, MIN_VISIBLE_MILLIS); + assertThat(subject.mOnPreDrawListener).isNotNull(); + verify(viewTreeObserver).addOnPreDrawListener(subject.mOnPreDrawListener); + assertThat(subject.mWeakViewTreeObserver.get()).isEqualTo(viewTreeObserver); + } + + @Test + public void constructor_withNonAliveViewTreeObserver_shouldNotSetOnPreDrawListenerForDecorView() throws Exception { + Activity mockActivity = mock(Activity.class); + Window window = mock(Window.class); + View decorView = mock(View.class); + ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); + + when(mockActivity.getWindow()).thenReturn(window); + when(window.getDecorView()).thenReturn(decorView); + when(decorView.getViewTreeObserver()).thenReturn(viewTreeObserver); + when(viewTreeObserver.isAlive()).thenReturn(false); + + subject = new BannerVisibilityTracker(mockActivity, mockView, mockView, MIN_VISIBLE_DIPS, MIN_VISIBLE_MILLIS); + verify(viewTreeObserver, never()).addOnPreDrawListener(subject.mOnPreDrawListener); + assertThat(subject.mWeakViewTreeObserver.get()).isNull(); + } + + @Test + public void constructor_withApplicationContext_shouldNotSetOnPreDrawListener() { + subject = new BannerVisibilityTracker(activity.getApplicationContext(), mockView, mockView, MIN_VISIBLE_DIPS, MIN_VISIBLE_MILLIS); + + assertThat(subject.mWeakViewTreeObserver.get()).isNull(); + } + + @Test + public void constructor_withViewTreeObserverNotSet_shouldSetViewTreeObserver() { + ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); + View rootView = mock(View.class); + + when(mockView.getContext()).thenReturn(activity.getApplicationContext()); + when(mockView.getRootView()).thenReturn(rootView); + when(rootView.getViewTreeObserver()).thenReturn(viewTreeObserver); + when(viewTreeObserver.isAlive()).thenReturn(true); + + subject = new BannerVisibilityTracker(activity.getApplicationContext(), rootView, mockView, MIN_VISIBLE_DIPS, MIN_VISIBLE_MILLIS); + assertThat(subject.mWeakViewTreeObserver.get()).isEqualTo(viewTreeObserver); + } + + @Test + public void destroy_shouldRemoveListenerFromDecorView() throws Exception { + Activity spyActivity = spy(Robolectric.buildActivity(Activity.class).create().get()); + Window window = mock(Window.class); + View decorView = mock(View.class); + ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); + + when(spyActivity.getWindow()).thenReturn(window); + when(window.getDecorView()).thenReturn(decorView); + when(decorView.findViewById(anyInt())).thenReturn(decorView); + when(decorView.getViewTreeObserver()).thenReturn(viewTreeObserver); + when(viewTreeObserver.isAlive()).thenReturn(true); + + subject = new BannerVisibilityTracker(spyActivity, mockView, mockView, MIN_VISIBLE_DIPS, MIN_VISIBLE_MILLIS); + subject.destroy(); + + assertThat(visibilityHandler.hasMessages(0)).isFalse(); + assertThat(subject.isVisibilityScheduled()).isFalse(); + verify(viewTreeObserver).removeOnPreDrawListener(any(OnPreDrawListener.class)); + assertThat(subject.mWeakViewTreeObserver.get()).isNull(); + assertThat(subject.getBannerVisibilityTrackerListener()).isNull(); + } + + // BannerVisibilityRunnable Tests + @Test + public void visibilityRunnable_run_withViewVisibleForAtLeastMinDuration_shouldCallOnVisibilityChangedCallback_shouldMarkImpTrackerAsFired_shouldNotScheduleVisibilityCheck() throws Exception { + subject.new BannerVisibilityRunnable().run(); + + verify(visibilityTrackerListener).onVisibilityChanged(); + assertThat(subject.isImpTrackerFired()).isTrue(); + assertThat(subject.isVisibilityScheduled()).isFalse(); + } + + @Test + public void visibilityRunnable_run_withViewNotVisible_shouldNotCallOnVisibilityChangedCallback_shouldNotMarkImpTrackerAsFired_shouldScheduleVisibilityCheck() throws Exception { + when(mockView.getVisibility()).thenReturn(View.INVISIBLE); + + subject.new BannerVisibilityRunnable().run(); + + verify(visibilityTrackerListener, never()).onVisibilityChanged(); + assertThat(subject.isImpTrackerFired()).isFalse(); + assertThat(subject.isVisibilityScheduled()).isTrue(); + } + + @Test + public void visibilityRunnable_run_witViewVisibleForLessThanMinDuration_shouldNotCallOnVisibilityChangedCallback_shouldNotMarkImpTrackerAsFired_shouldScheduleVisibilityCheck() throws Exception { + subject = new BannerVisibilityTracker(activity, mockView, mockView, 1, 1000); + subject.new BannerVisibilityRunnable().run(); + + verify(visibilityTrackerListener, never()).onVisibilityChanged(); + assertThat(subject.isImpTrackerFired()).isFalse(); + assertThat(subject.isVisibilityScheduled()).isTrue(); + } + + // BannerVisibilityChecker Tests + @Test + public void hasRequiredTimeElapsed_withStartTimeNotSetYet_shouldReturnFalse() throws Exception { + assertThat(visibilityChecker.hasRequiredTimeElapsed()).isFalse(); + } + + @Test + public void hasRequiredTimeElapsed_withStartTimeSet_withElapsedTimeGreaterThanMinTimeViewed_shouldReturnTrue() throws Exception { + visibilityChecker.setStartTimeMillis(); + + // minVisibleMillis is 0 ms as defined by constant MIN_VISIBLE_MILLIS + assertThat(visibilityChecker.hasRequiredTimeElapsed()).isTrue(); + } + + @Test + public void hasRequiredTimeElapsed_withStartTimeSet_withElapsedTimeLessThanMinTimeViewed_shouldReturnFalse() throws Exception { + subject = new BannerVisibilityTracker(activity, mockView, mockView, 1, 1000); + visibilityChecker = subject.getBannerVisibilityChecker(); + visibilityChecker.setStartTimeMillis(); + + // minVisibleMillis is 1 sec, should return false since we are checking immediately before 1 sec elapses + assertThat(visibilityChecker.hasRequiredTimeElapsed()).isFalse(); + } + + @Test + public void isVisible_whenParentIsNull_shouldReturnFalse() throws Exception { + mockView = createViewMock(View.VISIBLE, 100, 100, 100, 100, false, true); + assertThat(visibilityChecker.isVisible(mockView, mockView)).isFalse(); + } + + @Test + public void isVisible_whenViewIsOffScreen_shouldReturnFalse() throws Exception { + mockView = createViewMock(View.VISIBLE, 100, 100, 100, 100, true, false); + assertThat(visibilityChecker.isVisible(mockView, mockView)).isFalse(); + } + + @Test + public void isVisible_whenViewIsEntirelyOnScreen_shouldReturnTrue() throws Exception { + mockView = createViewMock(View.VISIBLE, 100, 100, 100, 100, true, true); + + assertThat(visibilityChecker.isVisible(mockView, mockView)).isTrue(); + } + + @Test + public void isVisible_whenViewHasMoreVisibleDipsThanMinVisibleDips_shouldReturnTrue() throws Exception { + mockView = createViewMock(View.VISIBLE, 1, 2, 100, 100, true, true); + + assertThat(visibilityChecker.isVisible(mockView, mockView)).isTrue(); + } + + @Test + public void isVisible_whenViewHasExactlyMinVisibleDips_shouldReturnTrue() throws Exception { + mockView = createViewMock(View.VISIBLE, 1, 1, 100, 100, true, true); + + assertThat(visibilityChecker.isVisible(mockView, mockView)).isTrue(); + } + + @Test + public void isVisible_whenViewHasLessVisibleDipsThanMinVisibleDips_shouldReturnFalse() throws Exception { + mockView = createViewMock(View.VISIBLE, 0, 1, 100, 100, true, true); + + assertThat(visibilityChecker.isVisible(mockView, mockView)).isFalse(); + } + + @Test + public void isVisible_whenVisibleAreaIsZero_shouldReturnFalse() throws Exception { + mockView = createViewMock(View.VISIBLE, 0, 0, 100, 100, true, true); + + assertThat(visibilityChecker.isVisible(mockView, mockView)).isFalse(); + } + + @Test + public void isVisible_whenViewIsInvisibleOrGone_shouldReturnFalse() throws Exception { + View view = createViewMock(View.INVISIBLE, 100, 100, 100, 100, true, true); + assertThat(visibilityChecker.isVisible(view, view)).isFalse(); + + reset(view); + view = createViewMock(View.GONE, 100, 100, 100, 100, true, true); + assertThat(visibilityChecker.isVisible(view, view)).isFalse(); + } + + @Test + public void isVisible_whenViewHasZeroWidth_shouldReturnFalse() throws Exception { + mockView = createViewMock(View.VISIBLE, 100, 100, 0, 100, true, true); + + assertThat(visibilityChecker.isVisible(mockView, mockView)).isFalse(); + } + + @Test + public void isVisible_whenViewHasZeroHeight_shouldReturnFalse() throws Exception { + mockView = createViewMock(View.VISIBLE, 100, 100, 100, 0, true, true); + + assertThat(visibilityChecker.isVisible(mockView, mockView)).isFalse(); + } + + @Test + public void isVisible_whenViewIsNull_shouldReturnFalse() throws Exception { + assertThat(visibilityChecker.isVisible(null, null)).isFalse(); + } + + static View createViewMock(final int visibility, + final int visibleWidth, + final int visibleHeight, + final int viewWidth, + final int viewHeight, + final boolean isParentSet, + final boolean isOnScreen) { + View view = mock(View.class); + when(view.getContext()).thenReturn(new Activity()); + when(view.getVisibility()).thenReturn(visibility); + + when(view.getGlobalVisibleRect(any(Rect.class))) + .thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable { + Object[] args = invocationOnMock.getArguments(); + Rect rect = (Rect) args[0]; + rect.set(0, 0, visibleWidth, visibleHeight); + return isOnScreen; + } + }); + + when(view.getWidth()).thenReturn(viewWidth); + when(view.getHeight()).thenReturn(viewHeight); + + if (isParentSet) { + when(view.getParent()).thenReturn(mock(ViewParent.class)); + } + + when(view.getViewTreeObserver()).thenCallRealMethod(); + + return view; + } +} diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/CustomEventBannerAdapterTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/CustomEventBannerAdapterTest.java index 1dd64a581..18dd5961e 100644 --- a/mopub-sdk/src/test/java/com/mopub/mobileads/CustomEventBannerAdapterTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/CustomEventBannerAdapterTest.java @@ -55,7 +55,6 @@ public class CustomEventBannerAdapterTest { @Before public void setUp() throws Exception { - when(moPubView.getAdTimeoutDelay()).thenReturn(null); when(moPubView.getAdWidth()).thenReturn(320); when(moPubView.getAdHeight()).thenReturn(50); @@ -128,7 +127,6 @@ public void timeout_withNonNullAdTimeoutDelay_shouldSignalFailureAndInvalidateWi assertThat(subject.isInvalidated()).isTrue(); } - @Test public void loadAd_shouldPropagateLocationInLocalExtras() throws Exception { Location expectedLocation = new Location(""); @@ -210,7 +208,6 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { subject.loadAd(); } - @Test public void loadAd_whenCallingOnBannerFailed_shouldCancelExistingTimeoutRunnable() throws Exception { ShadowLooper.pauseMainLooper(); @@ -241,25 +238,77 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable { } @Test - public void onBannerLoaded_shouldSignalMoPubView() throws Exception { + public void onBannerLoaded_whenViewIsHtmlBannerWebView_shouldNotTrackImpression() throws Exception { + View mockHtmlBannerWebView = mock(HtmlBannerWebView.class); + subject.onBannerLoaded(mockHtmlBannerWebView); + + verify(moPubView).nativeAdLoaded(); + verify(moPubView).setAdContentView(eq(mockHtmlBannerWebView)); + verify(moPubView, never()).trackNativeImpression(); + + // Since there are no visibility imp tracking headers, imp tracking should not be enabled. + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isFalse(); + assertThat(subject.getVisibilityTracker()).isNull(); + } + + @Test + public void onBannerLoaded_whenViewIsNotHtmlBannerWebView_shouldSignalMoPubView() throws Exception { View view = new View(Robolectric.buildActivity(Activity.class).create().get()); subject.onBannerLoaded(view); verify(moPubView).nativeAdLoaded(); verify(moPubView).setAdContentView(eq(view)); verify(moPubView).trackNativeImpression(); + + // Since there are no visibility imp tracking headers, imp tracking should not be enabled. + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isFalse(); + assertThat(subject.getVisibilityTracker()).isNull(); } @Test - public void onBannerLoaded_whenViewIsHtmlBannerWebView_shouldNotTrackImpression() throws Exception { + public void onBannerLoaded_whenViewIsHtmlBannerWebView_withVisibilityImpressionTrackingEnabled_shouldSetUpVisibilityTrackerWithListener_shouldNotTrackNativeImpressionImmediately() { View mockHtmlBannerWebView = mock(HtmlBannerWebView.class); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, "1"); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, "0"); + + subject = new CustomEventBannerAdapter(moPubView, CLASS_NAME, serverExtras, BROADCAST_IDENTIFIER, mockAdReport); subject.onBannerLoaded(mockHtmlBannerWebView); + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(1); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(0); + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isTrue(); + assertThat(subject.getVisibilityTracker()).isNotNull(); + assertThat(subject.getVisibilityTracker().getBannerVisibilityTrackerListener()).isNotNull(); verify(moPubView).nativeAdLoaded(); verify(moPubView).setAdContentView(eq(mockHtmlBannerWebView)); verify(moPubView, never()).trackNativeImpression(); } + @Test + public void onBannerLoaded_whenViewIsNotHtmlBannerWebView_withVisibilityImpressionTrackingEnabled_shouldSetUpVisibilityTrackerWithListener_shouldNotTrackNativeImpressionImmediately() { + View view = new View(Robolectric.buildActivity(Activity.class).create().get()); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, "1"); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, "0"); + + subject = new CustomEventBannerAdapter(moPubView, CLASS_NAME, serverExtras, BROADCAST_IDENTIFIER, mockAdReport); + subject.onBannerLoaded(view); + + // When visibility impression tracking is enabled, regardless of whether the banner view is + // HtmlBannerWebView or not, the behavior should be the same. + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(1); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(0); + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isTrue(); + assertThat(subject.getVisibilityTracker()).isNotNull(); + assertThat(subject.getVisibilityTracker().getBannerVisibilityTrackerListener()).isNotNull(); + verify(moPubView).nativeAdLoaded(); + verify(moPubView).setAdContentView(eq(view)); + verify(moPubView, never()).trackNativeImpression(); + } + @Test public void onBannerFailed_shouldLoadFailUrl() throws Exception { subject.onBannerFailed(ADAPTER_CONFIGURATION_ERROR); @@ -347,4 +396,106 @@ public void invalidate_shouldCauseBannerListenerMethodsToDoNothing() throws Exce verify(moPubView, never()).adClosed(); verify(moPubView, never()).registerClick(); } + + @Test + public void parseBannerImpressionTrackingHeaders_whenMissingInServerExtras_shouldUseDefaultValues_shouldNotEnableVisibilityImpressionTracking() { + // If headers are missing, use default values + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isFalse(); + } + + @Test + public void parseBannerImpressionTrackingHeaders_withBothValuesNonInteger_shouldUseDefaultValues_shouldNotEnableVisibilityImpressionTracking() { + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, ""); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, null); + + subject = new CustomEventBannerAdapter(moPubView, CLASS_NAME, serverExtras, BROADCAST_IDENTIFIER, mockAdReport); + + // Both header values must be Integers in order to be parsed + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isFalse(); + } + + @Test + public void parseBannerImpressionTrackingHeaders_withNonIntegerMinVisibleDipsValue_shouldUseDefaultValues_shouldNotEnableVisibilityImpressionTracking() { + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, null); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, "0"); + + subject = new CustomEventBannerAdapter(moPubView, CLASS_NAME, serverExtras, BROADCAST_IDENTIFIER, mockAdReport); + + // Both header values must be Integers in order to be parsed + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isFalse(); + } + + @Test + public void parseBannerImpressionTrackingHeaders_withNonIntegerMinVisibleMsValue_shouldUseDefaultValues_shouldNotEnableVisibilityImpressionTracking() { + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, "1"); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, ""); + + subject = new CustomEventBannerAdapter(moPubView, CLASS_NAME, serverExtras, BROADCAST_IDENTIFIER, mockAdReport); + + // Both header values must be Integers in order to be parsed + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(Integer.MIN_VALUE); + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isFalse(); + } + + @Test + public void parseBannerImpressionTrackingHeaders_withBothValuesValid_shouldParseValues_shouldEnableVisibilityImpressionTracking() { + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, "1"); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, "0"); + + subject = new CustomEventBannerAdapter(moPubView, CLASS_NAME, serverExtras, BROADCAST_IDENTIFIER, mockAdReport); + + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(1); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(0); + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isTrue(); + } + + @Test + public void parseBannerImpressionTrackingHeaders_withBothValuesInvalid_shouldParseValues_shouldNotEnableVisibilityImpressionTracking() { + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, "0"); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, "-1"); + + subject = new CustomEventBannerAdapter(moPubView, CLASS_NAME, serverExtras, BROADCAST_IDENTIFIER, mockAdReport); + + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(0); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(-1); + + // ImpressionMinVisibleDips must be > 0 AND ImpressionMinVisibleMs must be >= 0 in order to + // enable viewable impression tracking + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isFalse(); + } + + @Test + public void parseBannerImpressionTrackingHeaders_withInvalidMinVisibleDipsValue_shouldParseValues_shouldNotEnableVisibilityImpressionTracking() { + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, "0"); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, "0"); + + subject = new CustomEventBannerAdapter(moPubView, CLASS_NAME, serverExtras, BROADCAST_IDENTIFIER, mockAdReport); + + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(0); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(0); + + // ImpressionMinVisibleDips must be > 0 in order to enable viewable impression tracking + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isFalse(); + } + + @Test + public void parseBannerImpressionTrackingHeaders_withInvalidMinVisibleMsValue_shouldParseValues_shouldNotEnableVisibilityImpressionTracking() { + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS, "1"); + serverExtras.put(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS, "-1"); + + subject = new CustomEventBannerAdapter(moPubView, CLASS_NAME, serverExtras, BROADCAST_IDENTIFIER, mockAdReport); + + assertThat(subject.getImpressionMinVisibleDips()).isEqualTo(1); + assertThat(subject.getImpressionMinVisibleMs()).isEqualTo(-1); + + // ImpressionMinVisibleMs must be >= 0 in order to enable viewable impression tracking + assertThat(subject.isVisibilityImpressionTrackingEnabled()).isFalse(); + } } diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/HtmlBannerTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/HtmlBannerTest.java index 4b0d48b1d..f824f149e 100644 --- a/mopub-sdk/src/test/java/com/mopub/mobileads/HtmlBannerTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/HtmlBannerTest.java @@ -123,4 +123,13 @@ public void loadBanner_shouldCauseServerDimensionsToBeHonoredWhenLayingOutView() assertThat(layoutParams.height).isEqualTo(50); assertThat(layoutParams.gravity).isEqualTo(Gravity.CENTER); } + + @Test + public void trackMpxAndThirdPartyImpressions_shouldFireJavascriptWebViewDidAppear() throws Exception { + subject.loadBanner(context, customEventBannerListener, localExtras, serverExtras); + subject.trackMpxAndThirdPartyImpressions(); + + verify(htmlBannerWebView).loadHtmlResponse(responseBody); + verify(htmlBannerWebView).loadUrl(eq("javascript:webviewDidAppear();")); + } } diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubRewardedPlayableTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubRewardedPlayableTest.java index 479fe1d47..1b4708c87 100644 --- a/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubRewardedPlayableTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubRewardedPlayableTest.java @@ -15,7 +15,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.TreeMap; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Matchers.any; @@ -61,7 +60,7 @@ public void onInvalidate_withNullRewardedMraidActivity_shouldNotInvalidateReward @Test public void loadWithSdkInitialized_withCorrectLocalExtras_shouldLoadVastVideoInterstitial() throws Exception { subject.setRewardedMraidInterstitial(mockRewardedMraidInterstitial); - final Map localExtras = new TreeMap(); + final Map localExtras = new HashMap(); final Map serverExtras = new HashMap(); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_NAME_KEY, "currencyName"); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_AMOUNT_STRING_KEY, "10"); @@ -79,6 +78,25 @@ public void loadWithSdkInitialized_withCorrectLocalExtras_shouldLoadVastVideoInt assertThat(subject.getRewardedAdCurrencyAmount()).isEqualTo(10); } + @Test + public void loadWithSdkInitialized_withAdUnitId_shouldSetAdNetworkId() throws Exception { + final Map localExtras = new HashMap(); + localExtras.put(DataKeys.AD_UNIT_ID_KEY, "adUnit"); + + subject.loadWithSdkInitialized(activity, localExtras, new HashMap()); + + assertThat(subject.getAdNetworkId()).isEqualTo("adUnit"); + } + + @Test + public void loadWithSdkInitialized_withNoAdUnitId_shouldUseDefaultAdNetworkId() throws Exception { + subject.loadWithSdkInitialized(activity, new HashMap(), + new HashMap()); + + assertThat(subject.getAdNetworkId()).isEqualTo( + MoPubRewardedPlayable.MOPUB_REWARDED_PLAYABLE_ID); + } + @Test public void show_withMraidLoaded_shouldShowRewardedMraidInterstitial() { subject.setRewardedMraidInterstitial(mockRewardedMraidInterstitial); diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubRewardedVideoTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubRewardedVideoTest.java index ecb80584a..bfeb5d34f 100644 --- a/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubRewardedVideoTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubRewardedVideoTest.java @@ -15,7 +15,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.TreeMap; import static com.mopub.common.Constants.FOUR_HOURS_MILLIS; import static com.mopub.mobileads.MoPubErrorCode.EXPIRED; @@ -67,12 +66,12 @@ public void onInvalidate_withNullVastVideoInterstitial_shouldNotInvalidateVastVi @Test public void loadWithSdkInitialized_withLocalExtrasIncomplete_shouldLoadVastVideoInterstitial() throws Exception { subject.setRewardedVastVideoInterstitial(mockRewardedVastVideoInterstitial); - subject.loadWithSdkInitialized(activity, new TreeMap(), + subject.loadWithSdkInitialized(activity, new HashMap(), new HashMap()); verify(mockRewardedVastVideoInterstitial).loadInterstitial(eq(activity), any( CustomEventInterstitial.CustomEventInterstitialListener.class), - eq(new TreeMap()), + eq(new HashMap()), eq(new HashMap())); verifyNoMoreInteractions(mockRewardedVastVideoInterstitial); assertThat(subject.getRewardedAdCurrencyName()).isEqualTo(""); @@ -82,7 +81,7 @@ public void loadWithSdkInitialized_withLocalExtrasIncomplete_shouldLoadVastVideo @Test public void loadWithSdkInitialized_withRewardedVideoCurrencyNameIncorrectType_shouldLoadVastVideoInterstitial_shouldSetCurrencyNameToEmptyString() throws Exception { subject.setRewardedVastVideoInterstitial(mockRewardedVastVideoInterstitial); - final Map localExtras = new TreeMap(); + final Map localExtras = new HashMap(); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_NAME_KEY, new Object()); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_AMOUNT_STRING_KEY, "10"); @@ -100,7 +99,7 @@ public void loadWithSdkInitialized_withRewardedVideoCurrencyNameIncorrectType_sh @Test public void loadWithSdkInitialized_withRewardedVideoCurrencyAmountIncorrectType_shouldLoadVastVideoInterstitial_shouldSetCurrencyAmountToZero() throws Exception { subject.setRewardedVastVideoInterstitial(mockRewardedVastVideoInterstitial); - final Map localExtras = new TreeMap(); + final Map localExtras = new HashMap(); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_NAME_KEY, "currencyName"); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_AMOUNT_STRING_KEY, new Object()); @@ -118,7 +117,7 @@ public void loadWithSdkInitialized_withRewardedVideoCurrencyAmountIncorrectType_ @Test public void loadWithSdkInitialized_withRewardedVideoCurrencyAmountNotInteger_shouldLoadVastVideoInterstitial_shouldSetCurrencyAmountToZero() throws Exception { subject.setRewardedVastVideoInterstitial(mockRewardedVastVideoInterstitial); - final Map localExtras = new TreeMap(); + final Map localExtras = new HashMap(); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_NAME_KEY, "currencyName"); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_AMOUNT_STRING_KEY, "foo"); @@ -136,7 +135,7 @@ public void loadWithSdkInitialized_withRewardedVideoCurrencyAmountNotInteger_sho @Test public void loadWithSdkInitialized_withRewardedVideoCurrencyAmountNegative_shouldLoadVastVideoInterstitial_shouldSetCurrencyAmountToZero() throws Exception { subject.setRewardedVastVideoInterstitial(mockRewardedVastVideoInterstitial); - final Map localExtras = new TreeMap(); + final Map localExtras = new HashMap(); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_NAME_KEY, "currencyName"); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_AMOUNT_STRING_KEY, "-42"); @@ -154,7 +153,7 @@ public void loadWithSdkInitialized_withRewardedVideoCurrencyAmountNegative_shoul @Test public void loadWithSdkInitialized_withCorrectLocalExtras_shouldLoadVastVideoInterstitial() throws Exception { subject.setRewardedVastVideoInterstitial(mockRewardedVastVideoInterstitial); - final Map localExtras = new TreeMap(); + final Map localExtras = new HashMap(); final Map serverExtras = new HashMap(); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_NAME_KEY, "currencyName"); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_AMOUNT_STRING_KEY, "10"); @@ -174,7 +173,7 @@ public void loadWithSdkInitialized_withEmptyCurrencyName_withNegativeCurrencyAmo // We pass whatever was sent to this custom event to the app as long as it exists, but // if the currency value is negative, set it to 0 subject.setRewardedVastVideoInterstitial(mockRewardedVastVideoInterstitial); - final Map localExtras = new TreeMap(); + final Map localExtras = new HashMap(); final Map serverExtras = new HashMap(); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_NAME_KEY, ""); localExtras.put(DataKeys.REWARDED_AD_CURRENCY_AMOUNT_STRING_KEY, "-10"); @@ -189,6 +188,24 @@ public void loadWithSdkInitialized_withEmptyCurrencyName_withNegativeCurrencyAmo assertThat(subject.getRewardedAdCurrencyAmount()).isEqualTo(0); } + @Test + public void loadWithSdkInitialized_withAdUnitId_shouldSetAdNetworkId() throws Exception { + final Map localExtras = new HashMap(); + localExtras.put(DataKeys.AD_UNIT_ID_KEY, "adUnit"); + + subject.loadWithSdkInitialized(activity, localExtras, new HashMap()); + + assertThat(subject.getAdNetworkId()).isEqualTo("adUnit"); + } + + @Test + public void loadWithSdkInitialized_withNoAdUnitId_shouldUseDefaultAdNetworkId() throws Exception { + subject.loadWithSdkInitialized(activity, new HashMap(), + new HashMap()); + + assertThat(subject.getAdNetworkId()).isEqualTo(MoPubRewardedVideo.MOPUB_REWARDED_VIDEO_ID); + } + @Test public void show_withVideoLoaded_shouldShowVastVideoInterstitial() { subject.setRewardedVastVideoInterstitial(mockRewardedVastVideoInterstitial); diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubViewTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubViewTest.java index 565920c55..71e4b64f8 100644 --- a/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubViewTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/MoPubViewTest.java @@ -11,7 +11,6 @@ import com.mopub.mobileads.test.support.TestAdViewControllerFactory; import com.mopub.mobileads.test.support.TestCustomEventBannerAdapterFactory; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoBlurLastVideoFrameTaskTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoBlurLastVideoFrameTaskTest.java index 3a26eed3c..eeaace597 100644 --- a/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoBlurLastVideoFrameTaskTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoBlurLastVideoFrameTaskTest.java @@ -1,12 +1,9 @@ package com.mopub.mobileads; -import android.annotation.TargetApi; import android.graphics.Bitmap; import android.media.MediaMetadataRetriever; -import android.os.Build; import android.widget.ImageView; -import com.mopub.TestSdkHelper; import com.mopub.common.test.support.SdkTestRunner; import org.junit.Before; diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoViewControllerTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoViewControllerTest.java index 757c5c28d..461d72a47 100644 --- a/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoViewControllerTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoViewControllerTest.java @@ -874,19 +874,6 @@ public void onTouch_whenCloseButtonNotVisible_shouldNotPingClickThroughTrackers( assertThat(FakeHttp.httpRequestWasMade()).isFalse(); } - @Test - public void onTouch_withNullBaseVideoViewListener_andActionTouchUp_shouldReturnTrueAndNotBlowUp() throws Exception { - subject = new VastVideoViewController((Activity) context, bundle, null, - testBroadcastIdentifier, null); - - boolean result = getShadowVideoView().getOnTouchListener().onTouch(null, GestureUtils.createActionUp( - 0, 0)); - - // pass - - assertThat(result).isTrue(); - } - @Test public void onTouch_withActionTouchDown_shouldConsumeMotionEvent() throws Exception { initializeSubject(); diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoViewTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoViewTest.java index 877a26c94..30b8ccba7 100644 --- a/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoViewTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/VastVideoViewTest.java @@ -15,7 +15,6 @@ import org.robolectric.Robolectric; import org.robolectric.annotation.Config; -import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; diff --git a/mopub-sdk/src/test/java/com/mopub/mobileads/WebViewCacheServiceTest.java b/mopub-sdk/src/test/java/com/mopub/mobileads/WebViewCacheServiceTest.java index 89651a0aa..02e3b6d99 100644 --- a/mopub-sdk/src/test/java/com/mopub/mobileads/WebViewCacheServiceTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mobileads/WebViewCacheServiceTest.java @@ -1,6 +1,5 @@ package com.mopub.mobileads; -import android.app.Activity; import android.os.Handler; import com.mopub.common.ExternalViewabilitySessionManager; @@ -10,7 +9,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.robolectric.Robolectric; import org.robolectric.annotation.Config; import java.util.Map; diff --git a/mopub-sdk/src/test/java/com/mopub/mraid/MraidBannerTest.java b/mopub-sdk/src/test/java/com/mopub/mraid/MraidBannerTest.java index 7a3ba220f..ff0c47be6 100644 --- a/mopub-sdk/src/test/java/com/mopub/mraid/MraidBannerTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mraid/MraidBannerTest.java @@ -110,6 +110,16 @@ public void bannerMraidListener_onClose_shouldNotifyBannerCollapsed() { verify(mockBannerListener).onBannerCollapsed(); } + @Test + public void trackMpxAndThirdPartyImpressions_shouldFireJavascriptWebViewDidAppear() { + MraidListener mraidListener = captureMraidListener(); + mraidListener.onLoaded(null); + verify(mockBannerListener).onBannerLoaded(any(View.class)); + + subject.trackMpxAndThirdPartyImpressions(); + verify(mockMraidController).loadJavascript(eq("webviewDidAppear();")); + } + private MraidListener captureMraidListener() { subject.loadBanner(context, mockBannerListener, localExtras, serverExtras); ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(MraidListener.class); diff --git a/mopub-sdk/src/test/java/com/mopub/mraid/MraidVideoViewControllerTest.java b/mopub-sdk/src/test/java/com/mopub/mraid/MraidVideoViewControllerTest.java index b46389dea..ba9c96b04 100644 --- a/mopub-sdk/src/test/java/com/mopub/mraid/MraidVideoViewControllerTest.java +++ b/mopub-sdk/src/test/java/com/mopub/mraid/MraidVideoViewControllerTest.java @@ -132,10 +132,6 @@ public void onCompletionListener_shouldShowCloseButton() throws Exception { assertThat(getCloseButton().getVisibility()).isEqualTo(VISIBLE); } - @Test - public void onCompletionListener_withNullBaseVideoViewControllerListener_shouldNotCallOnFinish() throws Exception { - } - @Test public void onErrorListener_shouldReturnFalseAndNotCallBaseVideoControllerListenerOnFinish() throws Exception { initializeSubject(); diff --git a/mopub-sdk/src/test/java/com/mopub/nativeads/ImpressionTrackerTest.java b/mopub-sdk/src/test/java/com/mopub/nativeads/ImpressionTrackerTest.java index 6df8b430c..0a0b0d71f 100644 --- a/mopub-sdk/src/test/java/com/mopub/nativeads/ImpressionTrackerTest.java +++ b/mopub-sdk/src/test/java/com/mopub/nativeads/ImpressionTrackerTest.java @@ -56,8 +56,10 @@ public void setUp() { when(impressionInterface.getImpressionMinPercentageViewed()).thenReturn(50); when(impressionInterface.getImpressionMinTimeViewed()).thenReturn(1000); + when(impressionInterface.getImpressionMinVisiblePx()).thenReturn(null); when(impressionInterface2.getImpressionMinPercentageViewed()).thenReturn(50); when(impressionInterface2.getImpressionMinTimeViewed()).thenReturn(1000); + when(impressionInterface2.getImpressionMinVisiblePx()).thenReturn(null); // XXX We need this to ensure that our SystemClock starts ShadowSystemClock.uptimeMillis(); @@ -70,7 +72,7 @@ public void addView_shouldAddViewToTrackedViews_shouldAddViewToVisibilityTracker assertThat(trackedViews).hasSize(1); assertThat(trackedViews.get(view)).isEqualTo(impressionInterface); verify(visibilityTracker).addView(view, impressionInterface - .getImpressionMinPercentageViewed()); + .getImpressionMinPercentageViewed(), null); } @Test @@ -81,7 +83,8 @@ public void addView_withRecordedImpression_shouldNotAddView() { assertThat(trackedViews).hasSize(0); verify(visibilityTracker, never()) - .addView(view, impressionInterface.getImpressionMinPercentageViewed()); + .addView(view, impressionInterface.getImpressionMinPercentageViewed(), + null); } @Test @@ -90,7 +93,8 @@ public void addView_withDifferentImpressionInterface_shouldRemoveFromPollingView assertThat(trackedViews).hasSize(1); assertThat(trackedViews.get(view)).isEqualTo(impressionInterface); - verify(visibilityTracker).addView(view, impressionInterface.getImpressionMinPercentageViewed()); + verify(visibilityTracker).addView(view, + impressionInterface.getImpressionMinPercentageViewed(), null); pollingViews.put(view, timeStampWrapper); @@ -100,7 +104,7 @@ public void addView_withDifferentImpressionInterface_shouldRemoveFromPollingView assertThat(trackedViews.get(view)).isEqualTo(impressionInterface2); assertThat(pollingViews).isEmpty(); verify(visibilityTracker, times(2)) - .addView(view, impressionInterface.getImpressionMinPercentageViewed()); + .addView(view, impressionInterface.getImpressionMinPercentageViewed(), null); } @Test @@ -111,7 +115,8 @@ public void addView_withDifferentAlreadyImpressedImpressionInterface_shouldRemov assertThat(trackedViews).hasSize(1); assertThat(trackedViews.get(view)).isEqualTo(impressionInterface); - verify(visibilityTracker).addView(view, impressionInterface.getImpressionMinPercentageViewed()); + verify(visibilityTracker).addView(view, + impressionInterface.getImpressionMinPercentageViewed(), null); pollingViews.put(view, timeStampWrapper); @@ -120,7 +125,8 @@ public void addView_withDifferentAlreadyImpressedImpressionInterface_shouldRemov assertThat(trackedViews).hasSize(0); assertThat(trackedViews.get(view)).isNull(); assertThat(pollingViews).isEmpty(); - verify(visibilityTracker).addView(view, impressionInterface.getImpressionMinPercentageViewed()); + verify(visibilityTracker).addView(view, + impressionInterface.getImpressionMinPercentageViewed(), null); } @Test @@ -129,7 +135,8 @@ public void addView_withSameImpressionInterface_shouldNotAddView() { assertThat(trackedViews).hasSize(1); assertThat(trackedViews.get(view)).isEqualTo(impressionInterface); - verify(visibilityTracker).addView(view, impressionInterface.getImpressionMinPercentageViewed()); + verify(visibilityTracker).addView(view, + impressionInterface.getImpressionMinPercentageViewed(), null); pollingViews.put(view, timeStampWrapper); @@ -140,14 +147,16 @@ public void addView_withSameImpressionInterface_shouldNotAddView() { assertThat(pollingViews.keySet()).containsOnly(view); // Still only one call - verify(visibilityTracker).addView(view, impressionInterface.getImpressionMinPercentageViewed()); + verify(visibilityTracker).addView(view, + impressionInterface.getImpressionMinPercentageViewed(), null); } @Test public void removeView_shouldRemoveViewFromViewTrackedViews_shouldRemoveViewFromPollingMap_shouldRemoveViewFromVisibilityTracker() { trackedViews.put(view, impressionInterface); pollingViews.put(view, new TimestampWrapper(impressionInterface)); - visibilityTracker.addView(view, impressionInterface.getImpressionMinPercentageViewed()); + visibilityTracker.addView(view, + impressionInterface.getImpressionMinPercentageViewed(), null); subject.removeView(view); @@ -162,8 +171,10 @@ public void clear_shouldClearViewTrackedViews_shouldClearPollingViews_shouldClea trackedViews.put(view2, impressionInterface); pollingViews.put(view, timeStampWrapper); pollingViews.put(view2, timeStampWrapper); - visibilityTracker.addView(view, impressionInterface.getImpressionMinPercentageViewed()); - visibilityTracker.addView(view2, impressionInterface.getImpressionMinPercentageViewed()); + visibilityTracker.addView(view, + impressionInterface.getImpressionMinPercentageViewed(), null); + visibilityTracker.addView(view2, + impressionInterface.getImpressionMinPercentageViewed(), null); subject.clear(); @@ -179,8 +190,10 @@ public void destroy_shouldCallClear_shouldDestroyVisibilityTracker_shouldSetVisi trackedViews.put(view2, impressionInterface); pollingViews.put(view, timeStampWrapper); pollingViews.put(view2, timeStampWrapper); - visibilityTracker.addView(view, impressionInterface.getImpressionMinPercentageViewed()); - visibilityTracker.addView(view2, impressionInterface.getImpressionMinPercentageViewed()); + visibilityTracker.addView(view, + impressionInterface.getImpressionMinPercentageViewed(), null); + visibilityTracker.addView(view2, + impressionInterface.getImpressionMinPercentageViewed(), null); assertThat(subject.getVisibilityTrackerListener()).isNotNull(); subject.destroy(); @@ -293,4 +306,4 @@ public void pollingRunnableRun_whenImpressionInterfaceIsNull_shouldThrowNPE() { verify(impressionInterface, never()).recordImpression(view); } -} \ No newline at end of file +} diff --git a/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubAdAdapterTest.java b/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubAdAdapterTest.java index 7e91e7dbf..669006f8c 100644 --- a/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubAdAdapterTest.java +++ b/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubAdAdapterTest.java @@ -18,6 +18,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -331,7 +332,8 @@ public void getView_withAdPosition_shouldReturnAdView_shouldTrackVisibility() { assertThat(view).isEqualTo(mockAdView); - verify(mockVisibilityTracker).addView(eq(mockAdView), anyInt()); + verify(mockVisibilityTracker).addView(eq(mockAdView), anyInt(), + Matchers.isNull(Integer.class)); } @Test @@ -340,7 +342,8 @@ public void getView_withNonAdPosition_shouldOriginalAdapterView_shouldTrackVisib assertThat(view).isNotEqualTo(mockAdView); - verify(mockVisibilityTracker).addView(any(View.class), anyInt()); + verify(mockVisibilityTracker).addView(any(View.class), anyInt(), + Matchers.isNull(Integer.class)); } @Test diff --git a/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubStaticNativeAdTest.java b/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubStaticNativeAdTest.java index 63b43fc00..eb1455754 100644 --- a/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubStaticNativeAdTest.java +++ b/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubStaticNativeAdTest.java @@ -18,7 +18,6 @@ import com.mopub.volley.toolbox.ImageLoader; import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; diff --git a/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubVideoNativeAdTest.java b/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubVideoNativeAdTest.java index bc9f21298..f373be7b2 100644 --- a/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubVideoNativeAdTest.java +++ b/mopub-sdk/src/test/java/com/mopub/nativeads/MoPubVideoNativeAdTest.java @@ -353,7 +353,7 @@ public void render_shouldAddViewToVisibilityTracker() { subject.prepare(mockRootView); subject.render(mockMediaLayout); - verify(mockVisibilityTracker).addView(mockRootView, mockMediaLayout, 10, 5); + verify(mockVisibilityTracker).addView(mockRootView, mockMediaLayout, 10, 5, null); } @Test diff --git a/mopub-sdk/src/test/java/com/mopub/nativeads/NativeVideoControllerTest.java b/mopub-sdk/src/test/java/com/mopub/nativeads/NativeVideoControllerTest.java index 76187ea08..a3ef9e698 100644 --- a/mopub-sdk/src/test/java/com/mopub/nativeads/NativeVideoControllerTest.java +++ b/mopub-sdk/src/test/java/com/mopub/nativeads/NativeVideoControllerTest.java @@ -17,16 +17,16 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.mopub.common.test.support.SdkTestRunner; import com.mopub.mobileads.BuildConfig; import com.mopub.mobileads.VastTracker; import com.mopub.mobileads.VastVideoConfig; -import com.mopub.nativeads.NativeVideoController.MoPubExoPlayerFactory; import com.mopub.nativeads.NativeVideoController.Listener; +import com.mopub.nativeads.NativeVideoController.MoPubExoPlayerFactory; import com.mopub.nativeads.NativeVideoController.NativeVideoProgressRunnable; import com.mopub.nativeads.NativeVideoController.NativeVideoProgressRunnable.ProgressListener; import com.mopub.nativeads.NativeVideoController.VisibilityTrackingEvent; @@ -39,6 +39,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.Mockito; import org.robolectric.Robolectric; @@ -538,8 +539,10 @@ public void NativeVideoProgressRunnable_doWork_shouldTrackEventsWithMinimumPerce when(mockExoPlayer.getCurrentPosition()).thenReturn(10L); when(mockExoPlayer.getDuration()).thenReturn(25L); when(mockExoPlayer.getPlayWhenReady()).thenReturn(true); - when(mockVisibilityChecker.isVisible(mockTextureView, mockTextureView, 10)).thenReturn(true); - when(mockVisibilityChecker.isVisible(mockTextureView, mockTextureView, 20)).thenReturn(false); + when(mockVisibilityChecker.isVisible(mockTextureView, mockTextureView, + 10, null)).thenReturn(true); + when(mockVisibilityChecker.isVisible(mockTextureView, mockTextureView, + 20, null)).thenReturn(false); nativeVideoProgressRunnable.setUpdateIntervalMillis(10); nativeVideoProgressRunnable.doWork(); @@ -639,7 +642,8 @@ public void NativeVideoProgressRunnable_doWork_withExoPlayerGetPlayWhenReadyFals public void NativeVideoProgressRunnable_checkImpressionTrackers_withForceTriggerFalse_shouldOnlyTriggerNotTrackedEvents_shouldNotStopRunnable() { when(mockExoPlayer.getCurrentPosition()).thenReturn(50L); when(mockExoPlayer.getDuration()).thenReturn(50L); - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), anyInt())) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), anyInt(), + Matchers.isNull(Integer.class))) .thenReturn(true); spyNativeVideoProgressRunnable.setUpdateIntervalMillis(50); @@ -659,7 +663,8 @@ public void NativeVideoProgressRunnable_checkImpressionTrackers_withForceTrigger // Enough time has passed for all impressions to trigger organically when(mockExoPlayer.getCurrentPosition()).thenReturn(50L); when(mockExoPlayer.getDuration()).thenReturn(50L); - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), anyInt())) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), anyInt(), + Matchers.isNull(Integer.class))) .thenReturn(true); spyNativeVideoProgressRunnable.setUpdateIntervalMillis(50); spyNativeVideoProgressRunnable.requestStop(); @@ -681,7 +686,8 @@ public void NativeVideoProgressRunnable_checkImpressionTrackers_withForceTrigger // be triggered because forceTrigger is true when(mockExoPlayer.getCurrentPosition()).thenReturn(5L); when(mockExoPlayer.getDuration()).thenReturn(50L); - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), anyInt())) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), anyInt(), + Matchers.isNull(Integer.class))) .thenReturn(true); spyNativeVideoProgressRunnable.setUpdateIntervalMillis(50); @@ -700,7 +706,8 @@ public void NativeVideoProgressRunnable_checkImpressionTrackers_withForceTrigger public void NativeVideoProgressRunnable_checkImpressionTrackers_withForceTriggerTrue_withStopRequested_shouldOnlyTriggerNotTrackedEvents_shouldStopRunnable() { when(mockExoPlayer.getCurrentPosition()).thenReturn(50L); when(mockExoPlayer.getDuration()).thenReturn(50L); - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), anyInt())) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), anyInt(), + Matchers.isNull(Integer.class))) .thenReturn(true); spyNativeVideoProgressRunnable.setUpdateIntervalMillis(50); spyNativeVideoProgressRunnable.requestStop(); @@ -726,16 +733,20 @@ public void NativeVideoProgressRunnable_checkImpressionTrackers_withForceTrigger // track: whether the impression should be organically triggered // trackingUrl1: visible & played = track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(10))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(10), Matchers.isNull(Integer.class))) .thenReturn(true); // trackingUrl2: visible & !played = !track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(20))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(20), Matchers.isNull(Integer.class))) .thenReturn(true); // trackingUrl3: already tracked = !track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(30))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(30), Matchers.isNull(Integer.class))) .thenReturn(true); // trackingUrl4: !visible & played = !track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(9))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(9), Matchers.isNull(Integer.class))) .thenReturn(false); spyNativeVideoProgressRunnable.setUpdateIntervalMillis(10); spyNativeVideoProgressRunnable.requestStop(); @@ -763,16 +774,20 @@ public void NativeVideoProgressRunnable_checkImpressionTrackers_withForceTrigger // track: whether the impression should be organically triggered // trackingUrl1: visible & played = track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(10))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(10), Matchers.isNull(Integer.class))) .thenReturn(true); // trackingUrl2: visible & !played = !track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(20))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(20), Matchers.isNull(Integer.class))) .thenReturn(true); // trackingUrl3: already tracked = !track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(30))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(30), Matchers.isNull(Integer.class))) .thenReturn(true); // trackingUrl4: !visible & played = !track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(9))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(9), Matchers.isNull(Integer.class))) .thenReturn(false); spyNativeVideoProgressRunnable.setUpdateIntervalMillis(10); @@ -800,16 +815,20 @@ public void NativeVideoProgressRunnable_checkImpressionTrackers_withForceTrigger // track: whether the impression should be organically triggered // trackingUrl1: visible & played = track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(10))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(10), Matchers.isNull(Integer.class))) .thenReturn(true); // trackingUrl2: visible & !played = !track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(20))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(20), Matchers.isNull(Integer.class))) .thenReturn(true); // trackingUrl3: already tracked = !track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(30))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(30), Matchers.isNull(Integer.class))) .thenReturn(true); // trackingUrl4: !visible & played = !track - when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), eq(9))) + when(mockVisibilityChecker.isVisible(eq(mockTextureView), eq(mockTextureView), + eq(9), Matchers.isNull(Integer.class))) .thenReturn(false); spyNativeVideoProgressRunnable.setUpdateIntervalMillis(10); spyNativeVideoProgressRunnable.requestStop(); diff --git a/mopub-sdk/src/test/java/com/mopub/nativeads/VisibilityTrackerTest.java b/mopub-sdk/src/test/java/com/mopub/nativeads/VisibilityTrackerTest.java index 2c6be2a78..4a903940d 100644 --- a/mopub-sdk/src/test/java/com/mopub/nativeads/VisibilityTrackerTest.java +++ b/mopub-sdk/src/test/java/com/mopub/nativeads/VisibilityTrackerTest.java @@ -47,6 +47,7 @@ @Config(constants = BuildConfig.class) public class VisibilityTrackerTest { private static final int MIN_PERCENTAGE_VIEWED = 50; + private static final Integer DEFAULT_MIN_VISIBLE_PX = 1; private Activity activity; private VisibilityTracker subject; @@ -128,7 +129,7 @@ public void constructor_withApplicationContext_shouldNotSetOnPreDrawListener() { @Test public void addView_withVisibleView_shouldAddVisibleViewToTrackedViews() throws Exception { - subject.addView(view, MIN_PERCENTAGE_VIEWED); + subject.addView(view, MIN_PERCENTAGE_VIEWED, null); assertThat(trackedViews).hasSize(1); } @@ -145,21 +146,21 @@ public void addView_withViewTreeObserverNotSet_shouldSetViewTreeObserver() { subject = new VisibilityTracker(activity.getApplicationContext(), trackedViews, visibilityChecker, visibilityHandler); - subject.addView(view, MIN_PERCENTAGE_VIEWED); + subject.addView(view, MIN_PERCENTAGE_VIEWED, null); assertThat(subject.mWeakViewTreeObserver.get()).isEqualTo(viewTreeObserver); } @Test(expected = NullPointerException.class) public void addView_whenViewIsNull_shouldThrowNPE() throws Exception { - subject.addView(null, MIN_PERCENTAGE_VIEWED); + subject.addView(null, MIN_PERCENTAGE_VIEWED, null); assertThat(trackedViews).isEmpty(); } @Test public void removeView_shouldRemoveFromTrackedViews() throws Exception { - subject.addView(view, MIN_PERCENTAGE_VIEWED); + subject.addView(view, MIN_PERCENTAGE_VIEWED, null); assertThat(trackedViews).hasSize(1); assertThat(trackedViews).containsKey(view); @@ -171,8 +172,8 @@ public void removeView_shouldRemoveFromTrackedViews() throws Exception { @Test public void clear_shouldRemoveAllViewsFromTrackedViews_shouldRemoveMessagesFromVisibilityHandler_shouldResetIsVisibilityScheduled() throws Exception { - subject.addView(view, MIN_PERCENTAGE_VIEWED); - subject.addView(view2, MIN_PERCENTAGE_VIEWED); + subject.addView(view, MIN_PERCENTAGE_VIEWED, null); + subject.addView(view2, MIN_PERCENTAGE_VIEWED, null); assertThat(trackedViews).hasSize(2); subject.clear(); @@ -196,8 +197,8 @@ public void destroy_shouldCallClear_shouldRemoveListenerFromDecorView() throws E subject = new VisibilityTracker(activity1, trackedViews, visibilityChecker, visibilityHandler); - subject.addView(view, MIN_PERCENTAGE_VIEWED); - subject.addView(view2, MIN_PERCENTAGE_VIEWED); + subject.addView(view, MIN_PERCENTAGE_VIEWED, null); + subject.addView(view2, MIN_PERCENTAGE_VIEWED, null); assertThat(trackedViews).hasSize(2); subject.destroy(); @@ -210,7 +211,7 @@ public void destroy_shouldCallClear_shouldRemoveListenerFromDecorView() throws E @Test public void visibilityRunnable_run_withVisibleView_shouldCallOnVisibleCallback() throws Exception { - subject.addView(view, MIN_PERCENTAGE_VIEWED); + subject.addView(view, MIN_PERCENTAGE_VIEWED, null); subject.new VisibilityRunnable().run(); @@ -221,7 +222,7 @@ public void visibilityRunnable_run_withVisibleView_shouldCallOnVisibleCallback() @Test public void visibilityRunnable_run_withNonVisibleView_shouldCallOnNonVisibleCallback() throws Exception { when(view.getVisibility()).thenReturn(View.INVISIBLE); - subject.addView(view, MIN_PERCENTAGE_VIEWED); + subject.addView(view, MIN_PERCENTAGE_VIEWED, null); subject.new VisibilityRunnable().run(); @@ -249,89 +250,121 @@ public void hasRequiredTimeElapsed_withElapsedTimeLessThanMinTimeViewed_shouldRe @Test public void isMostlyVisible_whenParentIsNull_shouldReturnFalse() throws Exception { view = createViewMock(View.VISIBLE, 100, 100, 100, 100, false, true); - assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED)).isFalse(); + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, null)).isFalse(); } @Test public void isMostlyVisible_whenViewIsOffScreen_shouldReturnFalse() throws Exception { view = createViewMock(View.VISIBLE, 100, 100, 100, 100, true, false); - assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED)).isFalse(); + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, null)).isFalse(); } @Test public void isMostlyVisible_whenViewIsEntirelyOnScreen_shouldReturnTrue() throws Exception { view = createViewMock(View.VISIBLE, 100, 100, 100, 100, true, true); - assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED)).isTrue(); + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, null)).isTrue(); } @Test public void isMostlyVisible_whenViewIs50PercentVisible_shouldReturnTrue() throws Exception { view = createViewMock(View.VISIBLE, 50, 100, 100, 100, true, true); - assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED)).isTrue(); + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, null)).isTrue(); } @Test public void isMostlyVisible_whenViewIs49PercentVisible_shouldReturnFalse() throws Exception { view = createViewMock(View.VISIBLE, 49, 100, 100, 100, true, true); - assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED)).isFalse(); + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, null)).isFalse(); } @Test public void isMostlyVisible_whenVisibleAreaIsZero_shouldReturnFalse() throws Exception { view = createViewMock(View.VISIBLE, 0, 0, 100, 100, true, true); - assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED)).isFalse(); + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, null)).isFalse(); } @Test public void isMostlyVisible_whenViewIsInvisibleOrGone_shouldReturnFalse() throws Exception { View view = createViewMock(View.INVISIBLE, 100, 100, 100, 100, true, true); - assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED)).isFalse(); + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, null)).isFalse(); reset(view); view = createViewMock(View.GONE, 100, 100, 100, 100, true, true); - assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED)).isFalse(); + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, null)).isFalse(); } @Test public void isMostlyVisible_whenViewHasZeroWidthAndHeight_shouldReturnFalse() throws Exception { view = createViewMock(View.VISIBLE, 100, 100, 0, 0, true, true); - assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED)).isFalse(); + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, null)).isFalse(); } @Test public void isMostlyVisible_whenViewIsNull_shouldReturnFalse() throws Exception { - assertThat(visibilityChecker.isVisible(null, null, MIN_PERCENTAGE_VIEWED)).isFalse(); + assertThat(visibilityChecker.isVisible(null, null, MIN_PERCENTAGE_VIEWED, null)).isFalse(); + } + + @Test + public void isMostlyVisible_whenVisibleAreaIsCheckedByPixel_shouldReturnTrue() throws Exception { + view = createViewMock(View.VISIBLE, 90, 90, 100, 100, true, true); + + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, + DEFAULT_MIN_VISIBLE_PX)).isTrue(); + } + + @Test + public void isVisible_whenVisibleAreaIsCheckedByPixel_withExactlyOnePixelVisible_shouldReturnTrue() throws Exception { + view = createViewMock(View.VISIBLE, 1, 1, 100, 100, true, true); + + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, + DEFAULT_MIN_VISIBLE_PX)).isTrue(); + } + + @Test + public void isVisible_whenVisibleAreaIsCheckedByPixel_withLargeNonDefaultMinimumPixel_shouldReturnFalse() throws Exception { + view = createViewMock(View.VISIBLE, 3, 3, 100, 100, true, true); + + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, + 25)).isFalse(); + } + + @Test + public void isVisible_whenVisibleAreaIsCheckedByPixel_withSmallNonDefaultMinimumPixel_shouldReturnTrue() throws Exception { + view = createViewMock(View.VISIBLE, 3, 3, 100, 100, true, true); + + assertThat(visibilityChecker.isVisible(view, view, MIN_PERCENTAGE_VIEWED, + 5)).isTrue(); } @Test public void addView_shouldClearViewAfterNumAccesses() { // Access 1 time - subject.addView(view, MIN_PERCENTAGE_VIEWED); + subject.addView(view, MIN_PERCENTAGE_VIEWED, null); assertThat(trackedViews).hasSize(1); // Access 2-49 times for (int i = 0; i < VisibilityTracker.NUM_ACCESSES_BEFORE_TRIMMING - 2; ++i) { - subject.addView(view2, MIN_PERCENTAGE_VIEWED); + subject.addView(view2, MIN_PERCENTAGE_VIEWED, null); } assertThat(trackedViews).hasSize(2); // 50th time - subject.addView(view2, MIN_PERCENTAGE_VIEWED); + subject.addView(view2, MIN_PERCENTAGE_VIEWED, null); assertThat(trackedViews).hasSize(2); // 51-99 for (int i = 0; i < VisibilityTracker.NUM_ACCESSES_BEFORE_TRIMMING - 1; ++i) { - subject.addView(view2, MIN_PERCENTAGE_VIEWED); + subject.addView(view2, MIN_PERCENTAGE_VIEWED, null); } assertThat(trackedViews).hasSize(2); // 100 - subject.addView(view2, MIN_PERCENTAGE_VIEWED); + subject.addView(view2, MIN_PERCENTAGE_VIEWED, null); assertThat(trackedViews).hasSize(1); } diff --git a/mopub-sdk/src/test/java/com/mopub/nativeads/factories/CustomEventNativeFactoryTest.java b/mopub-sdk/src/test/java/com/mopub/nativeads/factories/CustomEventNativeFactoryTest.java index f13a663f0..5db56a217 100644 --- a/mopub-sdk/src/test/java/com/mopub/nativeads/factories/CustomEventNativeFactoryTest.java +++ b/mopub-sdk/src/test/java/com/mopub/nativeads/factories/CustomEventNativeFactoryTest.java @@ -7,13 +7,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricGradleTestRunner; import org.robolectric.annotation.Config; import static org.fest.assertions.api.Assertions.assertThat; import static org.junit.Assert.fail; - @Config(constants = BuildConfig.class) @RunWith(SdkTestRunner.class) public class CustomEventNativeFactoryTest { diff --git a/mopub-sdk/src/test/java/com/mopub/network/AdRequestTest.java b/mopub-sdk/src/test/java/com/mopub/network/AdRequestTest.java index 2d5a13f1c..094878e14 100644 --- a/mopub-sdk/src/test/java/com/mopub/network/AdRequestTest.java +++ b/mopub-sdk/src/test/java/com/mopub/network/AdRequestTest.java @@ -71,6 +71,7 @@ public void setup() { defaultHeaders.put(ResponseHeader.PAUSE_VISIBLE_PERCENT.getKey(), "25"); defaultHeaders.put(ResponseHeader.IMPRESSION_MIN_VISIBLE_PERCENT.getKey(), "33%"); defaultHeaders.put(ResponseHeader.IMPRESSION_VISIBLE_MS.getKey(), "2000"); + defaultHeaders.put(ResponseHeader.IMPRESSION_MIN_VISIBLE_PX.getKey(), "1"); defaultHeaders.put(ResponseHeader.MAX_BUFFER_MS.getKey(), "1000"); MoPubEvents.setEventDispatcher(mockEventDispatcher); @@ -169,6 +170,7 @@ public void parseNetworkResponse_forNativeVideo_shouldSucceed() throws Exception assertThat(serverExtras.get(DataKeys.PAUSE_VISIBLE_PERCENT)).isEqualTo("25"); assertThat(serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PERCENT)).isEqualTo("33"); assertThat(serverExtras.get(DataKeys.IMPRESSION_VISIBLE_MS)).isEqualTo("2000"); + assertThat(serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PX)).isEqualTo("1"); assertThat(serverExtras.get(DataKeys.MAX_BUFFER_MS)).isEqualTo("1000"); } @@ -186,6 +188,7 @@ public void parseNetworkResponse_forNativeStatic_shouldSucceed() throws Exceptio assertThat(serverExtras).isNotEmpty(); assertThat(serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PERCENT)).isEqualTo("33"); assertThat(serverExtras.get(DataKeys.IMPRESSION_VISIBLE_MS)).isEqualTo("2000"); + assertThat(serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PX)).isEqualTo("1"); } @Test @@ -208,6 +211,7 @@ public void parseNetworkResponse_forNativeVideo_shouldCombineServerExtrasAndEven assertThat(serverExtras.get(DataKeys.PAUSE_VISIBLE_PERCENT)).isEqualTo("25"); assertThat(serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PERCENT)).isEqualTo("33"); assertThat(serverExtras.get(DataKeys.IMPRESSION_VISIBLE_MS)).isEqualTo("2000"); + assertThat(serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PX)).isEqualTo("1"); assertThat(serverExtras.get(DataKeys.MAX_BUFFER_MS)).isEqualTo("1000"); assertThat(serverExtras.get("customEventKey1")).isEqualTo("value1"); @@ -219,6 +223,7 @@ public void parseNetworkResponse_forNativeVideo_withInvalidValues_shouldSucceed_ defaultHeaders.put(ResponseHeader.AD_TYPE.getKey(), AdType.VIDEO_NATIVE); defaultHeaders.put(ResponseHeader.PLAY_VISIBLE_PERCENT.getKey(), "-1"); defaultHeaders.put(ResponseHeader.PAUSE_VISIBLE_PERCENT.getKey(), "101%"); + defaultHeaders.put(ResponseHeader.IMPRESSION_MIN_VISIBLE_PX.getKey(), "bob"); defaultHeaders.put(ResponseHeader.IMPRESSION_MIN_VISIBLE_PERCENT.getKey(), "XX%"); NetworkResponse testResponse = new NetworkResponse(200, "{\"abc\": \"def\"}".getBytes(Charset.defaultCharset()), defaultHeaders, false); @@ -233,6 +238,7 @@ public void parseNetworkResponse_forNativeVideo_withInvalidValues_shouldSucceed_ assertThat(serverExtras.get(DataKeys.PAUSE_VISIBLE_PERCENT)).isNull(); assertThat(serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PERCENT)).isNull(); assertThat(serverExtras.get(DataKeys.IMPRESSION_VISIBLE_MS)).isEqualTo("2000"); + assertThat(serverExtras.get(DataKeys.IMPRESSION_MIN_VISIBLE_PX)).isEqualTo("bob"); assertThat(serverExtras.get(DataKeys.MAX_BUFFER_MS)).isEqualTo("1000"); } @@ -415,6 +421,73 @@ public void parseNetworkResponse_withUndefinedBrowserAgent_shouldDefaultToInApp( assertThat(response.result.getBrowserAgent()).isEqualTo(BrowserAgent.IN_APP); } + @Test + public void parseNetworkResponse_forBannerAdFormat_withoutImpTrackingHeaders_shouldSucceed() { + subject = new AdRequest("testUrl", AdFormat.BANNER, "testAdUnitId", activity, mockListener); + + NetworkResponse testResponse = + new NetworkResponse(200, "abc".getBytes(Charset.defaultCharset()), defaultHeaders, false); + + final Response response = subject.parseNetworkResponse(testResponse); + + assertThat(response.result).isNotNull(); + assertThat(response.result.getStringBody()).isEqualTo("abc"); + + // Check the server extras + final Map serverExtras = response.result.getServerExtras(); + assertThat(serverExtras).isNotNull(); + assertThat(serverExtras).isNotEmpty(); + assertThat(serverExtras.get(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS)).isNull(); + assertThat(serverExtras.get(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS)).isNull(); + } + + @Test + public void parseNetworkResponse_forBannerAdFormat_withImpTrackingHeaders_shouldSucceed_shouldStoreHeadersInServerExtras() { + defaultHeaders.put(ResponseHeader.BANNER_IMPRESSION_MIN_VISIBLE_DIPS.getKey(), "1"); + defaultHeaders.put(ResponseHeader.BANNER_IMPRESSION_MIN_VISIBLE_MS.getKey(), "0"); + + subject = new AdRequest("testUrl", AdFormat.BANNER, "testAdUnitId", activity, mockListener); + + NetworkResponse testResponse = + new NetworkResponse(200, "abc".getBytes(Charset.defaultCharset()), defaultHeaders, false); + + final Response response = subject.parseNetworkResponse(testResponse); + + assertThat(response.result).isNotNull(); + assertThat(response.result.getStringBody()).isEqualTo("abc"); + + // Check the server extras + final Map serverExtras = response.result.getServerExtras(); + assertThat(serverExtras).isNotNull(); + assertThat(serverExtras).isNotEmpty(); + assertThat(serverExtras.get(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS)).isEqualTo("1"); + assertThat(serverExtras.get(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS)).isEqualTo("0"); + } + + @Test + public void parseNetworkResponse_forNonBannerAdFormat_withImpTrackingHeaders_shouldSucceed_shouldIgnoreHeaders() { + defaultHeaders.put(ResponseHeader.BANNER_IMPRESSION_MIN_VISIBLE_DIPS.getKey(), "1"); + defaultHeaders.put(ResponseHeader.BANNER_IMPRESSION_MIN_VISIBLE_MS.getKey(), "0"); + + // Non-banner AdFormat + subject = new AdRequest("testUrl", AdFormat.INTERSTITIAL, "testAdUnitId", activity, mockListener); + + NetworkResponse testResponse = + new NetworkResponse(200, "abc".getBytes(Charset.defaultCharset()), defaultHeaders, false); + + final Response response = subject.parseNetworkResponse(testResponse); + + assertThat(response.result).isNotNull(); + assertThat(response.result.getStringBody()).isEqualTo("abc"); + + // Check the server extras + final Map serverExtras = response.result.getServerExtras(); + assertThat(serverExtras).isNotNull(); + assertThat(serverExtras).isNotEmpty(); + assertThat(serverExtras.get(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_DIPS)).isNull(); + assertThat(serverExtras.get(DataKeys.BANNER_IMPRESSION_MIN_VISIBLE_MS)).isNull(); + } + @Test public void deliverResponse_shouldCallListenerOnSuccess() throws Exception { subject.deliverResponse(mockAdResponse);