Add support for audio-only ad display containers

Issue: #7689
PiperOrigin-RevId: 325752377
This commit is contained in:
andrewlewis 2020-08-10 08:34:55 +01:00 committed by kim-vde
parent f2866a4942
commit acc8453628
4 changed files with 80 additions and 16 deletions

View File

@ -266,6 +266,10 @@
* Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to
register a purpose and detail reason for overlay views via
`AdsLoader.AdViewProvider`.
* Add support for audio-only ads display containers by returning `null`
from `AdsLoader.AdViewProvider.getAdViewGroup`, and allow skipping
audio-only ads via `ImaAdsLoader.skipAd`
([#7689](https://github.com/google/ExoPlayer/issues/7689)).
* Add `ImaAdsLoader.Builder.setCompanionAdSlots` so it's possible to set
companion ad slots without accessing the `AdDisplayContainer`.
* Add missing notification of `VideoAdPlayerCallback.onLoaded`.

View File

@ -609,15 +609,21 @@ public final class ImaAdsLoader
* called, so it is only necessary to call this method if you want to request ads before preparing
* the player.
*
* @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
* @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code
* null} if playing audio-only ads.
*/
public void requestAds(ViewGroup adViewGroup) {
public void requestAds(@Nullable ViewGroup adViewGroup) {
if (hasAdPlaybackState || adsManager != null || pendingAdRequestContext != null) {
// Ads have already been requested.
return;
}
adDisplayContainer =
imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener);
if (adViewGroup != null) {
adDisplayContainer =
imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener);
} else {
adDisplayContainer =
imaFactory.createAudioAdDisplayContainer(context, /* player= */ componentListener);
}
if (companionAdSlots != null) {
adDisplayContainer.setCompanionSlots(companionAdSlots);
}
@ -639,6 +645,19 @@ public final class ImaAdsLoader
adsLoader.requestAds(request);
}
/**
* Skips the current ad.
*
* <p>This method is intended for apps that play audio-only ads and so need to provide their own
* UI for users to skip skippable ads. Apps showing video ads should not call this method, as the
* IMA SDK provides the UI to skip ads in the ad view group passed via {@link AdViewProvider}.
*/
public void skipAd() {
if (adsManager != null) {
adsManager.skip();
}
}
// com.google.android.exoplayer2.source.ads.AdsLoader implementation.
@Override
@ -1582,6 +1601,8 @@ public final class ImaAdsLoader
* non-linear ads, and slots for companion ads.
*/
AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player);
/** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */
AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player);
/**
* Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for
* viewability measurement purposes.
@ -1817,6 +1838,11 @@ public final class ImaAdsLoader
return ImaSdkFactory.createAdDisplayContainer(container, player);
}
@Override
public AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player) {
return ImaSdkFactory.createAudioAdDisplayContainer(context, player);
}
// The reasonDetail parameter to createFriendlyObstruction is annotated @Nullable but the
// annotation is not kept in the obfuscated dependency.
@SuppressWarnings("nullness:argument.type.incompatible")

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.ima;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
@ -31,7 +32,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
@ -117,6 +117,7 @@ public final class ImaAdsLoaderTest {
private ViewGroup adViewGroup;
private AdsLoader.AdViewProvider adViewProvider;
private AdsLoader.AdViewProvider audioAdsAdViewProvider;
private AdEvent.AdEventListener adEventListener;
private ContentProgressProvider contentProgressProvider;
private VideoAdPlayer videoAdPlayer;
@ -127,8 +128,8 @@ public final class ImaAdsLoaderTest {
@Before
public void setUp() {
setupMocks();
adViewGroup = new FrameLayout(ApplicationProvider.getApplicationContext());
View adOverlayView = new View(ApplicationProvider.getApplicationContext());
adViewGroup = new FrameLayout(getApplicationContext());
View adOverlayView = new View(getApplicationContext());
adViewProvider =
new AdsLoader.AdViewProvider() {
@Override
@ -142,6 +143,18 @@ public final class ImaAdsLoaderTest {
new AdsLoader.OverlayInfo(adOverlayView, AdsLoader.OverlayInfo.PURPOSE_CLOSE_AD));
}
};
audioAdsAdViewProvider =
new AdsLoader.AdViewProvider() {
@Override
public ViewGroup getAdViewGroup() {
return null;
}
@Override
public ImmutableList<AdsLoader.OverlayInfo> getAdOverlayInfos() {
return ImmutableList.of();
}
};
}
@After
@ -165,9 +178,21 @@ public final class ImaAdsLoaderTest {
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockImaFactory, atLeastOnce()).createAdDisplayContainer(adViewGroup, videoAdPlayer);
verify(mockImaFactory, never()).createAudioAdDisplayContainer(any(), any());
verify(mockAdDisplayContainer).registerFriendlyObstruction(mockFriendlyObstruction);
}
@Test
public void startForAudioOnlyAds_createsAudioOnlyAdDisplayContainer() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, audioAdsAdViewProvider);
verify(mockImaFactory, atLeastOnce())
.createAudioAdDisplayContainer(getApplicationContext(), videoAdPlayer);
verify(mockImaFactory, never()).createAdDisplayContainer(any(), any());
verify(mockAdDisplayContainer, never()).registerFriendlyObstruction(any());
}
@Test
public void start_withPlaceholderContent_initializedAdsLoader() {
Timeline placeholderTimeline = new PlaceholderTimeline(MediaItem.fromUri(Uri.EMPTY));
@ -470,7 +495,7 @@ public final class ImaAdsLoaderTest {
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
new ImaAdsLoader.Builder(getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
@ -502,7 +527,7 @@ public final class ImaAdsLoaderTest {
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
new ImaAdsLoader.Builder(getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
@ -534,7 +559,7 @@ public final class ImaAdsLoaderTest {
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
new ImaAdsLoader.Builder(getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
@ -570,7 +595,7 @@ public final class ImaAdsLoaderTest {
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
new ImaAdsLoader.Builder(getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
@ -609,7 +634,7 @@ public final class ImaAdsLoaderTest {
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
new ImaAdsLoader.Builder(getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
@ -696,7 +721,7 @@ public final class ImaAdsLoaderTest {
setupPlayback(
contentTimeline,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
new ImaAdsLoader.Builder(getApplicationContext())
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
@ -765,6 +790,13 @@ public final class ImaAdsLoaderTest {
})
.when(mockImaFactory)
.createAdDisplayContainer(any(), any());
doAnswer(
invocation -> {
videoAdPlayer = invocation.getArgument(1);
return mockAdDisplayContainer;
})
.when(mockImaFactory)
.createAudioAdDisplayContainer(any(), any());
when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings);
when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest);
when(mockImaFactory.createAdsLoader(any(), any(), any())).thenReturn(mockAdsLoader);

View File

@ -80,10 +80,12 @@ public interface AdsLoader {
interface AdViewProvider {
/**
* 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.
* Returns the {@link ViewGroup} on top of the player that will show any ad UI, or {@code null}
* if playing audio-only ads. Any views on top of the returned view group must be described by
* {@link OverlayInfo OverlayInfos} returned by {@link #getAdOverlayInfos()}, for accurate
* viewability measurement.
*/
@Nullable
ViewGroup getAdViewGroup();
/** @deprecated Use {@link #getAdOverlayInfos()} instead. */