Migrate off deprecated IMA SDK APIs

AdDisplayContainer now takes the video ad player at construction time,
and obstructions are registered/unregistered via a new method. Also
'content complete' is now notified via ad callbacks rather than the
AdsLoader.

PiperOrigin-RevId: 320567666
This commit is contained in:
andrewlewis 2020-07-10 10:04:43 +01:00 committed by kim-vde
parent bcbe310681
commit 3214851fbb
7 changed files with 233 additions and 82 deletions

View file

@ -239,9 +239,13 @@
`androidx.media2.session.MediaSession`. `androidx.media2.session.MediaSession`.
* Cast extension: Implement playlist API and deprecate the old queue * Cast extension: Implement playlist API and deprecate the old queue
manipulation API. manipulation API.
* IMA extension: Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the * IMA extension:
media load timeout * Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the
([#7170](https://github.com/google/ExoPlayer/issues/7170)). media load timeout
([#7170](https://github.com/google/ExoPlayer/issues/7170)).
* Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to
register a purpose and detail reason for overlay views via
`AdsLoader.AdViewProvider`.
* Demo app: Retain previous position in list of samples. * Demo app: Retain previous position in list of samples.
* Add Guava dependency. * Add Guava dependency.

View file

@ -29,6 +29,7 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion

View file

@ -40,11 +40,14 @@ import com.google.ads.interactivemedia.v3.api.AdEvent;
import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;
import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType;
import com.google.ads.interactivemedia.v3.api.AdPodInfo; import com.google.ads.interactivemedia.v3.api.AdPodInfo;
import com.google.ads.interactivemedia.v3.api.AdsLoader;
import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener;
import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.FriendlyObstruction;
import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.UiElement; import com.google.ads.interactivemedia.v3.api.UiElement;
@ -58,7 +61,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
@ -77,19 +79,25 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread. * {@link com.google.android.exoplayer2.source.ads.AdsLoader} using the IMA SDK. All methods must be
* called on the main thread.
* *
* <p>The player instance that will play the loaded ads must be set before playback using {@link * <p>The player instance that will play the loaded ads must be set before playback using {@link
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
* {@link #release()}. * {@link #release()}.
* *
* <p>The IMA SDK can take into account video control overlay views when calculating ad viewability. * <p>The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This
* For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link * means that any overlay views that obstruct the ad overlay but are essential for playback need to
* AdViewProvider#getAdOverlayViews()}. * be registered via the {@link AdViewProvider} passed to the {@link
* com.google.android.exoplayer2.source.ads.AdsMediaSource}. See the <a
* href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/omsdk">
* IMA SDK Open Measurement documentation</a> for more information.
*/ */
public final class ImaAdsLoader implements Player.EventListener, AdsLoader { public final class ImaAdsLoader
implements Player.EventListener, com.google.android.exoplayer2.source.ads.AdsLoader {
static { static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); ExoPlayerLibraryInfo.registerModule("goog.exo.ima");
@ -329,7 +337,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
* Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is
* the interval recommended by the IMA documentation. * the interval recommended by the IMA documentation.
* *
* @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback * @see VideoAdPlayer.VideoAdPlayerCallback
*/ */
private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100; private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100;
@ -370,6 +378,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
*/ */
private static final int IMA_AD_STATE_PAUSED = 2; private static final int IMA_AD_STATE_PAUSED = 2;
private final Context context;
@Nullable private final Uri adTagUri; @Nullable private final Uri adTagUri;
@Nullable private final String adsResponse; @Nullable private final String adsResponse;
private final long adPreloadTimeoutMs; private final long adPreloadTimeoutMs;
@ -381,15 +390,16 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
@Nullable private final Set<UiElement> adUiElements; @Nullable private final Set<UiElement> adUiElements;
@Nullable private final AdEventListener adEventListener; @Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory; private final ImaFactory imaFactory;
private final ImaSdkSettings imaSdkSettings;
private final Timeline.Period period; private final Timeline.Period period;
private final Handler handler; private final Handler handler;
private final ComponentListener componentListener; private final ComponentListener componentListener;
private final List<VideoAdPlayer.VideoAdPlayerCallback> adCallbacks; private final List<VideoAdPlayer.VideoAdPlayerCallback> adCallbacks;
private final AdDisplayContainer adDisplayContainer;
private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
private final Runnable updateAdProgressRunnable; private final Runnable updateAdProgressRunnable;
private final Map<AdMediaInfo, AdInfo> adInfoByAdMediaInfo; private final Map<AdMediaInfo, AdInfo> adInfoByAdMediaInfo;
private @MonotonicNonNull AdDisplayContainer adDisplayContainer;
private @MonotonicNonNull AdsLoader adsLoader;
private boolean wasSetPlayerCalled; private boolean wasSetPlayerCalled;
@Nullable private Player nextPlayer; @Nullable private Player nextPlayer;
@Nullable private Object pendingAdRequestContext; @Nullable private Object pendingAdRequestContext;
@ -418,10 +428,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
@Nullable private AdMediaInfo imaAdMediaInfo; @Nullable private AdMediaInfo imaAdMediaInfo;
/** The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */ /** The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */
@Nullable private AdInfo imaAdInfo; @Nullable private AdInfo imaAdInfo;
/** /** Whether IMA has been notified that playback of content has finished. */
* Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been
* called since starting ad playback.
*/
private boolean sentContentComplete; private boolean sentContentComplete;
// Fields tracking the player/loader state. // Fields tracking the player/loader state.
@ -508,6 +515,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
@Nullable AdEventListener adEventListener, @Nullable AdEventListener adEventListener,
ImaFactory imaFactory) { ImaFactory imaFactory) {
checkArgument(adTagUri != null || adsResponse != null); checkArgument(adTagUri != null || adsResponse != null);
this.context = context.getApplicationContext();
this.adTagUri = adTagUri; this.adTagUri = adTagUri;
this.adsResponse = adsResponse; this.adsResponse = adsResponse;
this.adPreloadTimeoutMs = adPreloadTimeoutMs; this.adPreloadTimeoutMs = adPreloadTimeoutMs;
@ -527,17 +535,11 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
} }
imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE);
imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION);
this.imaSdkSettings = imaSdkSettings;
period = new Timeline.Period(); period = new Timeline.Period();
handler = Util.createHandler(getImaLooper(), /* callback= */ null); handler = Util.createHandler(getImaLooper(), /* callback= */ null);
componentListener = new ComponentListener(); componentListener = new ComponentListener();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ componentListener);
adsLoader =
imaFactory.createAdsLoader(
context.getApplicationContext(), imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(componentListener);
adsLoader.addAdsLoadedListener(componentListener);
updateAdProgressRunnable = this::updateAdProgress; updateAdProgressRunnable = this::updateAdProgress;
adInfoByAdMediaInfo = new HashMap<>(); adInfoByAdMediaInfo = new HashMap<>();
supportedMimeTypes = Collections.emptyList(); supportedMimeTypes = Collections.emptyList();
@ -553,23 +555,26 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
} }
/** /**
* Returns the underlying {@code com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by this * Returns the underlying {@link AdsLoader} wrapped by this instance, or {@code null} if ads have
* instance. * not been requested yet.
*/ */
public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { @Nullable
public AdsLoader getAdsLoader() {
return adsLoader; return adsLoader;
} }
/** /**
* Returns the {@link AdDisplayContainer} used by this loader. * Returns the {@link AdDisplayContainer} used by this loader, or {@code null} if ads have not
* been requested yet.
* *
* <p>Note: any video controls overlays registered via {@link * <p>Note: any video controls overlays registered via {@link
* AdDisplayContainer#registerVideoControlsOverlay(View)} will be unregistered automatically when * AdDisplayContainer#registerFriendlyObstruction(FriendlyObstruction)} will be unregistered
* the media source detaches from this instance. It is therefore necessary to re-register views * automatically when the media source detaches from this instance. It is therefore necessary to
* each time the ads loader is reused. Alternatively, provide overlay views via the {@link * re-register views each time the ads loader is reused. Alternatively, provide overlay views via
* AdsLoader.AdViewProvider} when creating the media source to benefit from automatic * the {@link com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider} when creating the
* registration. * media source to benefit from automatic registration.
*/ */
@Nullable
public AdDisplayContainer getAdDisplayContainer() { public AdDisplayContainer getAdDisplayContainer() {
return adDisplayContainer; return adDisplayContainer;
} }
@ -588,7 +593,11 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
// Ads have already been requested. // Ads have already been requested.
return; return;
} }
adDisplayContainer.setAdContainer(adViewGroup); adDisplayContainer =
imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener);
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(componentListener);
adsLoader.addAdsLoadedListener(componentListener);
AdsRequest request = imaFactory.createAdsRequest(); AdsRequest request = imaFactory.createAdsRequest();
if (adTagUri != null) { if (adTagUri != null) {
request.setAdTagUrl(adTagUri.toString()); request.setAdTagUrl(adTagUri.toString());
@ -604,7 +613,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
adsLoader.requestAds(request); adsLoader.requestAds(request);
} }
// AdsLoader implementation. // com.google.android.exoplayer2.source.ads.AdsLoader implementation.
@Override @Override
public void setPlayer(@Nullable Player player) { public void setPlayer(@Nullable Player player) {
@ -650,12 +659,6 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
lastVolumePercent = 0; lastVolumePercent = 0;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY; lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
ViewGroup adViewGroup = adViewProvider.getAdViewGroup();
adDisplayContainer.setAdContainer(adViewGroup);
View[] adOverlayViews = adViewProvider.getAdOverlayViews();
for (View view : adOverlayViews) {
adDisplayContainer.registerVideoControlsOverlay(view);
}
maybeNotifyPendingAdLoadError(); maybeNotifyPendingAdLoadError();
if (hasAdPlaybackState) { if (hasAdPlaybackState) {
// Pass the ad playback state to the player, and resume ads if necessary. // Pass the ad playback state to the player, and resume ads if necessary.
@ -668,7 +671,16 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
updateAdPlaybackState(); updateAdPlaybackState();
} else { } else {
// Ads haven't loaded yet, so request them. // Ads haven't loaded yet, so request them.
requestAds(adViewGroup); requestAds(adViewProvider.getAdViewGroup());
}
if (adDisplayContainer != null) {
for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
adDisplayContainer.registerFriendlyObstruction(
imaFactory.createFriendlyObstruction(
overlayInfo.view,
getFriendlyObstructionPurpose(overlayInfo.purpose),
overlayInfo.reasonDetail));
}
} }
} }
@ -687,7 +699,9 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
lastVolumePercent = getPlayerVolumePercent(); lastVolumePercent = getPlayerVolumePercent();
lastAdProgress = getAdVideoProgressUpdate(); lastAdProgress = getAdVideoProgressUpdate();
lastContentProgress = getContentVideoProgressUpdate(); lastContentProgress = getContentVideoProgressUpdate();
adDisplayContainer.unregisterAllVideoControlsOverlays(); if (adDisplayContainer != null) {
adDisplayContainer.unregisterAllFriendlyObstructions();
}
player.removeListener(this); player.removeListener(this);
this.player = null; this.player = null;
eventListener = null; eventListener = null;
@ -697,8 +711,10 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
public void release() { public void release() {
pendingAdRequestContext = null; pendingAdRequestContext = null;
destroyAdsManager(); destroyAdsManager();
adsLoader.removeAdsLoadedListener(componentListener); if (adsLoader != null) {
adsLoader.removeAdErrorListener(componentListener); adsLoader.removeAdsLoadedListener(componentListener);
adsLoader.removeAdErrorListener(componentListener);
}
imaPausedContent = false; imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE; imaAdState = IMA_AD_STATE_NONE;
imaAdMediaInfo = null; imaAdMediaInfo = null;
@ -1356,7 +1372,9 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
} }
private void sendContentComplete() { private void sendContentComplete() {
adsLoader.contentComplete(); for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onContentComplete();
}
sentContentComplete = true; sentContentComplete = true;
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "adsLoader.contentComplete"); Log.d(TAG, "adsLoader.contentComplete");
@ -1442,6 +1460,21 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]";
} }
private static FriendlyObstructionPurpose getFriendlyObstructionPurpose(
@OverlayInfo.Purpose int purpose) {
switch (purpose) {
case OverlayInfo.PURPOSE_CONTROLS:
return FriendlyObstructionPurpose.VIDEO_CONTROLS;
case OverlayInfo.PURPOSE_CLOSE_AD:
return FriendlyObstructionPurpose.CLOSE_AD;
case OverlayInfo.PURPOSE_NOT_VISIBLE:
return FriendlyObstructionPurpose.NOT_VISIBLE;
case OverlayInfo.PURPOSE_OTHER:
default:
return FriendlyObstructionPurpose.OTHER;
}
}
private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) { private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) {
return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY); return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY);
} }
@ -1495,16 +1528,30 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
/** Factory for objects provided by the IMA SDK. */ /** Factory for objects provided by the IMA SDK. */
@VisibleForTesting @VisibleForTesting
/* package */ interface ImaFactory { /* package */ interface ImaFactory {
/** @see ImaSdkSettings */ /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */
ImaSdkSettings createImaSdkSettings(); ImaSdkSettings createImaSdkSettings();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRenderingSettings() */ /**
* Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that
* control rendering of ads.
*/
AdsRenderingSettings createAdsRenderingSettings(); AdsRenderingSettings createAdsRenderingSettings();
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() */ /**
AdDisplayContainer createAdDisplayContainer(); * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for
/** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */ * non-linear ads, and slots for companion ads.
*/
AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player);
/**
* Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for
* viewability measurement purposes.
*/
FriendlyObstruction createFriendlyObstruction(
View view,
FriendlyObstructionPurpose friendlyObstructionPurpose,
@Nullable String reasonDetail);
/** Creates an {@link AdsRequest} to contain the data used to request ads. */
AdsRequest createAdsRequest(); AdsRequest createAdsRequest();
/** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */ /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */
com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
} }
@ -1515,7 +1562,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
AdErrorListener, AdErrorListener,
VideoAdPlayer { VideoAdPlayer {
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation. // AdsLoader.AdsLoadedListener implementation.
@Override @Override
public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) {
@ -1724,8 +1771,20 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
} }
@Override @Override
public AdDisplayContainer createAdDisplayContainer() { public AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player) {
return ImaSdkFactory.getInstance().createAdDisplayContainer(); return ImaSdkFactory.createAdDisplayContainer(container, player);
}
// The reasonDetail parameter to createFriendlyObstruction is annotated @Nullable but the
// annotation is not kept in the obfuscated dependency.
@SuppressWarnings("nullness:argument.type.incompatible")
@Override
public FriendlyObstruction createFriendlyObstruction(
View view,
FriendlyObstructionPurpose friendlyObstructionPurpose,
@Nullable String reasonDetail) {
return ImaSdkFactory.getInstance()
.createFriendlyObstruction(view, friendlyObstructionPurpose, reasonDetail);
} }
@Override @Override
@ -1734,7 +1793,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
} }
@Override @Override
public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( public AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
return ImaSdkFactory.getInstance() return ImaSdkFactory.getInstance()
.createAdsLoader(context, imaSdkSettings, adDisplayContainer); .createAdsLoader(context, imaSdkSettings, adDisplayContainer);

View file

@ -42,6 +42,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.FriendlyObstruction;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
@ -108,6 +109,7 @@ public final class ImaAdsLoaderTest {
@Mock private AdsRequest mockAdsRequest; @Mock private AdsRequest mockAdsRequest;
@Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent; @Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent;
@Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader; @Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader;
@Mock private FriendlyObstruction mockFriendlyObstruction;
@Mock private ImaFactory mockImaFactory; @Mock private ImaFactory mockImaFactory;
@Mock private AdPodInfo mockAdPodInfo; @Mock private AdPodInfo mockAdPodInfo;
@Mock private Ad mockPrerollSingleAd; @Mock private Ad mockPrerollSingleAd;
@ -162,8 +164,8 @@ public final class ImaAdsLoaderTest {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider); imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup); verify(mockImaFactory, atLeastOnce()).createAdDisplayContainer(adViewGroup, videoAdPlayer);
verify(mockAdDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView); verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction);
} }
@Test @Test
@ -637,8 +639,8 @@ public final class ImaAdsLoaderTest {
imaAdsLoader.stop(); imaAdsLoader.stop();
InOrder inOrder = inOrder(mockAdDisplayContainer); InOrder inOrder = inOrder(mockAdDisplayContainer);
inOrder.verify(mockAdDisplayContainer).registerVideoControlsOverlay(adOverlayView); inOrder.verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction);
inOrder.verify(mockAdDisplayContainer).unregisterAllVideoControlsOverlays(); inOrder.verify(mockAdDisplayContainer).unregisterAllFriendlyObstructions();
} }
@Test @Test
@ -758,16 +760,16 @@ public final class ImaAdsLoaderTest {
doAnswer( doAnswer(
invocation -> { invocation -> {
videoAdPlayer = invocation.getArgument(0); videoAdPlayer = invocation.getArgument(1);
return null; return mockAdDisplayContainer;
}) })
.when(mockAdDisplayContainer) .when(mockImaFactory)
.setPlayer(any()); .createAdDisplayContainer(any(), any());
when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer);
when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings); when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings);
when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest); when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest);
when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader); when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader);
when(mockImaFactory.createFriendlyObstruction(any(), any(), any()))
.thenReturn(mockFriendlyObstruction);
when(mockAdPodInfo.getPodIndex()).thenReturn(0); when(mockAdPodInfo.getPodIndex()).thenReturn(0);
when(mockAdPodInfo.getTotalAds()).thenReturn(1); when(mockAdPodInfo.getTotalAds()).thenReturn(1);

View file

@ -17,12 +17,18 @@ package com.google.android.exoplayer2.source.ads;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.common.collect.ImmutableList;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/** /**
* Interface for loaders of ads, which can be used with {@link AdsMediaSource}. * Interface for loaders of ads, which can be used with {@link AdsMediaSource}.
@ -70,23 +76,90 @@ public interface AdsLoader {
default void onAdTapped() {} default void onAdTapped() {}
} }
/** Provides views for the ad UI. */ /** Provides information about views for the ad playback UI. */
interface AdViewProvider { interface AdViewProvider {
/** Returns the {@link ViewGroup} on top of the player that will show any ad UI. */ /**
* Returns the {@link ViewGroup} on top of the player that will show any ad UI. Any views on top
* of the returned view group must be described by {@link OverlayInfo OverlayInfos} returned by
* {@link #getAdOverlayInfos()}, for accurate viewability measurement.
*/
ViewGroup getAdViewGroup(); ViewGroup getAdViewGroup();
/** @deprecated Use {@link #getAdOverlayInfos()} instead. */
@Deprecated
default View[] getAdOverlayViews() {
return new View[0];
}
/** /**
* Returns an array of views that are shown on top of the ad view group, but that are essential * Returns a list of {@link OverlayInfo} instances describing views that are on top of the ad
* for controlling playback and should be excluded from ad viewability measurements by the * view group, but that are essential for controlling playback and should be excluded from ad
* {@link AdsLoader} (if it supports this). * viewability measurements by the {@link AdsLoader} (if it supports this).
* *
* <p>Each view must be either a fully transparent overlay (for capturing touch events), or a * <p>Each view must be either a fully transparent overlay (for capturing touch events), or a
* small piece of transient UI that is essential to the user experience of playback (such as a * small piece of transient UI that is essential to the user experience of playback (such as a
* button to pause/resume playback or a transient full-screen or cast button). For more * button to pause/resume playback or a transient full-screen or cast button). For more
* information see the documentation for your ads loader. * information see the documentation for your ads loader.
*/ */
View[] getAdOverlayViews(); @SuppressWarnings("deprecation")
default List<OverlayInfo> getAdOverlayInfos() {
ImmutableList.Builder<OverlayInfo> listBuilder = new ImmutableList.Builder<>();
// Call through to deprecated version.
for (View view : getAdOverlayViews()) {
listBuilder.add(new OverlayInfo(view, OverlayInfo.PURPOSE_CONTROLS));
}
return listBuilder.build();
}
}
/** Provides information about an overlay view shown on top of an ad view group. */
final class OverlayInfo {
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({PURPOSE_CONTROLS, PURPOSE_CLOSE_AD, PURPOSE_OTHER, PURPOSE_NOT_VISIBLE})
public @interface Purpose {}
/** Purpose for playback controls overlaying the player. */
public static final int PURPOSE_CONTROLS = 0;
/** Purpose for ad close buttons overlaying the player. */
public static final int PURPOSE_CLOSE_AD = 1;
/** Purpose for other overlays. */
public static final int PURPOSE_OTHER = 2;
/** Purpose for overlays that are not visible. */
public static final int PURPOSE_NOT_VISIBLE = 3;
/** The overlay view. */
public final View view;
/** The purpose of the overlay view. */
@Purpose public final int purpose;
/** An optional, detailed reason that the overlay view is needed. */
@Nullable public final String reasonDetail;
/**
* Creates a new overlay info.
*
* @param view The view that is overlaying the player.
* @param purpose The purpose of the view.
*/
public OverlayInfo(View view, @Purpose int purpose) {
this(view, purpose, /* detailedReason= */ null);
}
/**
* Creates a new overlay info.
*
* @param view The view that is overlaying the player.
* @param purpose The purpose of the view.
* @param detailedReason An optional, detailed reason that the view is on top of the player. See
* the documentation for the {@link AdsLoader} implementation for more information on this
* string's formatting.
*/
public OverlayInfo(View view, @Purpose int purpose, @Nullable String detailedReason) {
this.view = view;
this.purpose = purpose;
this.reasonDetail = detailedReason;
}
} }
// Methods called by the application. // Methods called by the application.

View file

@ -68,6 +68,7 @@ import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView; import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView;
import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.VideoListener;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -1258,15 +1259,20 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
} }
@Override @Override
public View[] getAdOverlayViews() { public List<AdsLoader.OverlayInfo> getAdOverlayInfos() {
ArrayList<View> overlayViews = new ArrayList<>(); List<AdsLoader.OverlayInfo> overlayViews = new ArrayList<>();
if (overlayFrameLayout != null) { if (overlayFrameLayout != null) {
overlayViews.add(overlayFrameLayout); overlayViews.add(
new AdsLoader.OverlayInfo(
overlayFrameLayout,
AdsLoader.OverlayInfo.PURPOSE_NOT_VISIBLE,
/* detailedReason= */ "Transparent overlay does not impact viewability"));
} }
if (controller != null) { if (controller != null) {
overlayViews.add(controller); overlayViews.add(
new AdsLoader.OverlayInfo(controller, AdsLoader.OverlayInfo.PURPOSE_CONTROLS));
} }
return overlayViews.toArray(new View[0]); return ImmutableList.copyOf(overlayViews);
} }
// Internal methods. // Internal methods.

View file

@ -67,6 +67,7 @@ import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView; import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView;
import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.VideoListener;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -1248,15 +1249,20 @@ public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewPro
} }
@Override @Override
public View[] getAdOverlayViews() { public List<AdsLoader.OverlayInfo> getAdOverlayInfos() {
ArrayList<View> overlayViews = new ArrayList<>(); List<AdsLoader.OverlayInfo> overlayViews = new ArrayList<>();
if (overlayFrameLayout != null) { if (overlayFrameLayout != null) {
overlayViews.add(overlayFrameLayout); overlayViews.add(
new AdsLoader.OverlayInfo(
overlayFrameLayout,
AdsLoader.OverlayInfo.PURPOSE_NOT_VISIBLE,
/* detailedReason= */ "Transparent overlay does not impact viewability"));
} }
if (controller != null) { if (controller != null) {
overlayViews.add(controller); overlayViews.add(
new AdsLoader.OverlayInfo(controller, AdsLoader.OverlayInfo.PURPOSE_CONTROLS));
} }
return overlayViews.toArray(new View[0]); return ImmutableList.copyOf(overlayViews);
} }
// Internal methods. // Internal methods.