diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c975d64f89..47babde849 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -239,9 +239,13 @@ `androidx.media2.session.MediaSession`. * Cast extension: Implement playlist API and deprecate the old queue manipulation API. -* IMA extension: Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the - media load timeout - ([#7170](https://github.com/google/ExoPlayer/issues/7170)). +* IMA extension: + * Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the + 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. * Add Guava dependency. diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 9e4eddf073..33985b2639 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion 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 androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index b564d345b3..a581c6825b 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -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.AdEventType; 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.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; 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.ImaSdkSettings; 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.Timeline; 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.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSpec; @@ -77,19 +79,25 @@ import java.util.HashSet; import java.util.List; import java.util.Map; 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. * *

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 * {@link #release()}. * - *

The IMA SDK can take into account video control overlay views when calculating ad viewability. - * For more details see {@link AdDisplayContainer#registerVideoControlsOverlay(View)} and {@link - * AdViewProvider#getAdOverlayViews()}. + *

The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This + * means that any overlay views that obstruct the ad overlay but are essential for playback need to + * be registered via the {@link AdViewProvider} passed to the {@link + * com.google.android.exoplayer2.source.ads.AdsMediaSource}. See the + * IMA SDK Open Measurement documentation 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 { 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 * 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; @@ -370,6 +378,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { */ private static final int IMA_AD_STATE_PAUSED = 2; + private final Context context; @Nullable private final Uri adTagUri; @Nullable private final String adsResponse; private final long adPreloadTimeoutMs; @@ -381,15 +390,16 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { @Nullable private final Set adUiElements; @Nullable private final AdEventListener adEventListener; private final ImaFactory imaFactory; + private final ImaSdkSettings imaSdkSettings; private final Timeline.Period period; private final Handler handler; private final ComponentListener componentListener; private final List adCallbacks; - private final AdDisplayContainer adDisplayContainer; - private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; private final Runnable updateAdProgressRunnable; private final Map adInfoByAdMediaInfo; + private @MonotonicNonNull AdDisplayContainer adDisplayContainer; + private @MonotonicNonNull AdsLoader adsLoader; private boolean wasSetPlayerCalled; @Nullable private Player nextPlayer; @Nullable private Object pendingAdRequestContext; @@ -418,10 +428,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { @Nullable private AdMediaInfo imaAdMediaInfo; /** The current ad info, or {@code null} if in state {@link #IMA_AD_STATE_NONE}. */ @Nullable private AdInfo imaAdInfo; - /** - * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been - * called since starting ad playback. - */ + /** Whether IMA has been notified that playback of content has finished. */ private boolean sentContentComplete; // Fields tracking the player/loader state. @@ -508,6 +515,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { @Nullable AdEventListener adEventListener, ImaFactory imaFactory) { checkArgument(adTagUri != null || adsResponse != null); + this.context = context.getApplicationContext(); this.adTagUri = adTagUri; this.adsResponse = adsResponse; this.adPreloadTimeoutMs = adPreloadTimeoutMs; @@ -527,17 +535,11 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { } imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); + this.imaSdkSettings = imaSdkSettings; period = new Timeline.Period(); handler = Util.createHandler(getImaLooper(), /* callback= */ null); componentListener = new ComponentListener(); 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; adInfoByAdMediaInfo = new HashMap<>(); 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 - * instance. + * Returns the underlying {@link AdsLoader} wrapped by this instance, or {@code null} if ads have + * not been requested yet. */ - public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { + @Nullable + public AdsLoader getAdsLoader() { 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. * *

Note: any video controls overlays registered via {@link - * AdDisplayContainer#registerVideoControlsOverlay(View)} will be unregistered automatically when - * the media source detaches from this instance. It is therefore necessary to re-register views - * each time the ads loader is reused. Alternatively, provide overlay views via the {@link - * AdsLoader.AdViewProvider} when creating the media source to benefit from automatic - * registration. + * AdDisplayContainer#registerFriendlyObstruction(FriendlyObstruction)} will be unregistered + * automatically when the media source detaches from this instance. It is therefore necessary to + * re-register views each time the ads loader is reused. Alternatively, provide overlay views via + * the {@link com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider} when creating the + * media source to benefit from automatic registration. */ + @Nullable public AdDisplayContainer getAdDisplayContainer() { return adDisplayContainer; } @@ -588,7 +593,11 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { // Ads have already been requested. 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(); if (adTagUri != null) { request.setAdTagUrl(adTagUri.toString()); @@ -604,7 +613,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { adsLoader.requestAds(request); } - // AdsLoader implementation. + // com.google.android.exoplayer2.source.ads.AdsLoader implementation. @Override public void setPlayer(@Nullable Player player) { @@ -650,12 +659,6 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { lastVolumePercent = 0; lastAdProgress = 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(); if (hasAdPlaybackState) { // 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(); } else { // 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(); lastAdProgress = getAdVideoProgressUpdate(); lastContentProgress = getContentVideoProgressUpdate(); - adDisplayContainer.unregisterAllVideoControlsOverlays(); + if (adDisplayContainer != null) { + adDisplayContainer.unregisterAllFriendlyObstructions(); + } player.removeListener(this); this.player = null; eventListener = null; @@ -697,8 +711,10 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { public void release() { pendingAdRequestContext = null; destroyAdsManager(); - adsLoader.removeAdsLoadedListener(componentListener); - adsLoader.removeAdErrorListener(componentListener); + if (adsLoader != null) { + adsLoader.removeAdsLoadedListener(componentListener); + adsLoader.removeAdErrorListener(componentListener); + } imaPausedContent = false; imaAdState = IMA_AD_STATE_NONE; imaAdMediaInfo = null; @@ -1356,7 +1372,9 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { } private void sendContentComplete() { - adsLoader.contentComplete(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onContentComplete(); + } sentContentComplete = true; if (DEBUG) { Log.d(TAG, "adsLoader.contentComplete"); @@ -1442,6 +1460,21 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { 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) { 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. */ @VisibleForTesting /* package */ interface ImaFactory { - /** @see ImaSdkSettings */ + /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */ 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(); - /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdDisplayContainer() */ - AdDisplayContainer createAdDisplayContainer(); - /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */ + /** + * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for + * 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(); - /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */ - com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( + /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */ + AdsLoader createAdsLoader( Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); } @@ -1515,7 +1562,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { AdErrorListener, VideoAdPlayer { - // com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation. + // AdsLoader.AdsLoadedListener implementation. @Override public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { @@ -1724,8 +1771,20 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { } @Override - public AdDisplayContainer createAdDisplayContainer() { - return ImaSdkFactory.getInstance().createAdDisplayContainer(); + public AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player) { + 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 @@ -1734,7 +1793,7 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader { } @Override - public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( + public AdsLoader createAdsLoader( Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { return ImaSdkFactory.getInstance() .createAdsLoader(context, imaSdkSettings, adDisplayContainer); diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index 5a59690b44..e313535e9a 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -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.AdsRenderingSettings; 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.player.AdMediaInfo; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; @@ -108,6 +109,7 @@ public final class ImaAdsLoaderTest { @Mock private AdsRequest mockAdsRequest; @Mock private AdsManagerLoadedEvent mockAdsManagerLoadedEvent; @Mock private com.google.ads.interactivemedia.v3.api.AdsLoader mockAdsLoader; + @Mock private FriendlyObstruction mockFriendlyObstruction; @Mock private ImaFactory mockImaFactory; @Mock private AdPodInfo mockAdPodInfo; @Mock private Ad mockPrerollSingleAd; @@ -162,8 +164,8 @@ public final class ImaAdsLoaderTest { setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS); imaAdsLoader.start(adsLoaderListener, adViewProvider); - verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup); - verify(mockAdDisplayContainer, atLeastOnce()).registerVideoControlsOverlay(adOverlayView); + verify(mockImaFactory, atLeastOnce()).createAdDisplayContainer(adViewGroup, videoAdPlayer); + verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction); } @Test @@ -637,8 +639,8 @@ public final class ImaAdsLoaderTest { imaAdsLoader.stop(); InOrder inOrder = inOrder(mockAdDisplayContainer); - inOrder.verify(mockAdDisplayContainer).registerVideoControlsOverlay(adOverlayView); - inOrder.verify(mockAdDisplayContainer).unregisterAllVideoControlsOverlays(); + inOrder.verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction); + inOrder.verify(mockAdDisplayContainer).unregisterAllFriendlyObstructions(); } @Test @@ -758,16 +760,16 @@ public final class ImaAdsLoaderTest { doAnswer( invocation -> { - videoAdPlayer = invocation.getArgument(0); - return null; + videoAdPlayer = invocation.getArgument(1); + return mockAdDisplayContainer; }) - .when(mockAdDisplayContainer) - .setPlayer(any()); - - when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer); + .when(mockImaFactory) + .createAdDisplayContainer(any(), any()); when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings); when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest); when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader); + when(mockImaFactory.createFriendlyObstruction(any(), any(), any())) + .thenReturn(mockFriendlyObstruction); when(mockAdPodInfo.getPodIndex()).thenReturn(0); when(mockAdPodInfo.getTotalAds()).thenReturn(1); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index 11947218a3..df50a31d74 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -17,12 +17,18 @@ package com.google.android.exoplayer2.source.ads; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.common.collect.ImmutableList; 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}. @@ -70,23 +76,90 @@ public interface AdsLoader { default void onAdTapped() {} } - /** Provides views for the ad UI. */ + /** Provides information about views for the ad playback UI. */ 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(); + /** @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 - * for controlling playback and should be excluded from ad viewability measurements by the - * {@link AdsLoader} (if it supports this). + * Returns a list of {@link OverlayInfo} instances describing views that are on top of the ad + * view group, but that are essential for controlling playback and should be excluded from ad + * viewability measurements by the {@link AdsLoader} (if it supports this). * *

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 * button to pause/resume playback or a transient full-screen or cast button). For more * information see the documentation for your ads loader. */ - View[] getAdOverlayViews(); + @SuppressWarnings("deprecation") + default List getAdOverlayInfos() { + ImmutableList.Builder 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. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 985fbd3dda..049af9b64a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -68,6 +68,7 @@ import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView; import com.google.android.exoplayer2.video.VideoListener; +import com.google.common.collect.ImmutableList; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -1258,15 +1259,20 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider } @Override - public View[] getAdOverlayViews() { - ArrayList overlayViews = new ArrayList<>(); + public List getAdOverlayInfos() { + List overlayViews = new ArrayList<>(); 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) { - 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. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java index c2c090f835..3817d15a47 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java @@ -67,6 +67,7 @@ import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView; import com.google.android.exoplayer2.video.VideoListener; +import com.google.common.collect.ImmutableList; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -1248,15 +1249,20 @@ public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewPro } @Override - public View[] getAdOverlayViews() { - ArrayList overlayViews = new ArrayList<>(); + public List getAdOverlayInfos() { + List overlayViews = new ArrayList<>(); 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) { - 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.