From a26bb350eec38c512571a862cde02a37642111af Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 12 Jul 2018 08:16:13 -0700 Subject: [PATCH] Add IMA and FLAC tests to the codebase ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=204302135 --- extensions/flac/src/test/AndroidManifest.xml | 17 ++ .../flac/DefaultExtractorsFactoryTest.java | 74 +++++ .../src/test/resources/robolectric.properties | 1 + extensions/ima/src/test/AndroidManifest.xml | 16 ++ .../android/exoplayer2/ext/ima/FakeAd.java | 189 ++++++++++++ .../exoplayer2/ext/ima/FakeAdsLoader.java | 100 +++++++ .../exoplayer2/ext/ima/FakeAdsRequest.java | 132 +++++++++ .../exoplayer2/ext/ima/FakePlayer.java | 196 +++++++++++++ .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 272 ++++++++++++++++++ .../ext/ima/SingletonImaFactory.java | 71 +++++ .../src/test/resources/robolectric.properties | 1 + 11 files changed, 1069 insertions(+) create mode 100644 extensions/flac/src/test/AndroidManifest.xml create mode 100644 extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java create mode 100644 extensions/flac/src/test/resources/robolectric.properties create mode 100644 extensions/ima/src/test/AndroidManifest.xml create mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java create mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java create mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java create mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java create mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java create mode 100644 extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java create mode 100644 extensions/ima/src/test/resources/robolectric.properties diff --git a/extensions/flac/src/test/AndroidManifest.xml b/extensions/flac/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..1d68b376ac --- /dev/null +++ b/extensions/flac/src/test/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java new file mode 100644 index 0000000000..e08f4dc28c --- /dev/null +++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultExtractorsFactoryTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.flac; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.amr.AmrExtractor; +import com.google.android.exoplayer2.extractor.flv.FlvExtractor; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; +import com.google.android.exoplayer2.extractor.ogg.OggExtractor; +import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; +import com.google.android.exoplayer2.extractor.ts.AdtsExtractor; +import com.google.android.exoplayer2.extractor.ts.PsExtractor; +import com.google.android.exoplayer2.extractor.ts.TsExtractor; +import com.google.android.exoplayer2.extractor.wav.WavExtractor; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link DefaultExtractorsFactory}. */ +@RunWith(RobolectricTestRunner.class) +public final class DefaultExtractorsFactoryTest { + + @Test + public void testCreateExtractors_returnExpectedClasses() { + DefaultExtractorsFactory defaultExtractorsFactory = new DefaultExtractorsFactory(); + + Extractor[] extractors = defaultExtractorsFactory.createExtractors(); + List listCreatedExtractorClasses = new ArrayList<>(); + for (Extractor extractor : extractors) { + listCreatedExtractorClasses.add(extractor.getClass()); + } + + Class[] expectedExtractorClassses = + new Class[] { + MatroskaExtractor.class, + FragmentedMp4Extractor.class, + Mp4Extractor.class, + Mp3Extractor.class, + AdtsExtractor.class, + Ac3Extractor.class, + TsExtractor.class, + FlvExtractor.class, + OggExtractor.class, + PsExtractor.class, + WavExtractor.class, + AmrExtractor.class, + FlacExtractor.class + }; + + assertThat(listCreatedExtractorClasses).containsNoDuplicates(); + assertThat(listCreatedExtractorClasses).containsExactlyElementsIn(expectedExtractorClassses); + } +} diff --git a/extensions/flac/src/test/resources/robolectric.properties b/extensions/flac/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/extensions/flac/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml diff --git a/extensions/ima/src/test/AndroidManifest.xml b/extensions/ima/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..9a4e33189e --- /dev/null +++ b/extensions/ima/src/test/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java new file mode 100644 index 0000000000..284471adfc --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import com.google.ads.interactivemedia.v3.api.Ad; +import com.google.ads.interactivemedia.v3.api.AdPodInfo; +import com.google.ads.interactivemedia.v3.api.UiElement; +import java.util.Set; + +/** A fake ad for testing. */ +/* package */ final class FakeAd implements Ad { + + private final boolean skippable; + private final AdPodInfo adPodInfo; + + public FakeAd(boolean skippable, int podIndex, int totalAds, int adPosition) { + this.skippable = skippable; + adPodInfo = + new AdPodInfo() { + @Override + public int getTotalAds() { + return totalAds; + } + + @Override + public int getAdPosition() { + return adPosition; + } + + @Override + public int getPodIndex() { + return podIndex; + } + + @Override + public boolean isBumper() { + throw new UnsupportedOperationException(); + } + + @Override + public double getMaxDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public double getTimeOffset() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public boolean isSkippable() { + return skippable; + } + + @Override + public AdPodInfo getAdPodInfo() { + return adPodInfo; + } + + @Override + public String getAdId() { + throw new UnsupportedOperationException(); + } + + @Override + public String getCreativeId() { + throw new UnsupportedOperationException(); + } + + @Override + public String getCreativeAdId() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUniversalAdIdValue() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUniversalAdIdRegistry() { + throw new UnsupportedOperationException(); + } + + @Override + public String getAdSystem() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getAdWrapperIds() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getAdWrapperSystems() { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getAdWrapperCreativeIds() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLinear() { + throw new UnsupportedOperationException(); + } + + @Override + public double getSkipTimeOffset() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isUiDisabled() { + throw new UnsupportedOperationException(); + } + + @Override + public String getDescription() { + throw new UnsupportedOperationException(); + } + + @Override + public String getTitle() { + throw new UnsupportedOperationException(); + } + + @Override + public String getContentType() { + throw new UnsupportedOperationException(); + } + + @Override + public String getAdvertiserName() { + throw new UnsupportedOperationException(); + } + + @Override + public String getSurveyUrl() { + throw new UnsupportedOperationException(); + } + + @Override + public String getDealId() { + throw new UnsupportedOperationException(); + } + + @Override + public int getWidth() { + throw new UnsupportedOperationException(); + } + + @Override + public int getHeight() { + throw new UnsupportedOperationException(); + } + + @Override + public String getTraffickingParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public double getDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getUiElements() { + throw new UnsupportedOperationException(); + } +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java new file mode 100644 index 0000000000..a8f3daae33 --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsLoader.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; +import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.ads.interactivemedia.v3.api.StreamManager; +import com.google.ads.interactivemedia.v3.api.StreamRequest; +import com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; + +/** Fake {@link com.google.ads.interactivemedia.v3.api.AdsLoader} implementation for tests. */ +public final class FakeAdsLoader implements com.google.ads.interactivemedia.v3.api.AdsLoader { + + private final ImaSdkSettings imaSdkSettings; + private final AdsManager adsManager; + private final ArrayList adsLoadedListeners; + private final ArrayList adErrorListeners; + + public FakeAdsLoader(ImaSdkSettings imaSdkSettings, AdsManager adsManager) { + this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); + this.adsManager = Assertions.checkNotNull(adsManager); + adsLoadedListeners = new ArrayList<>(); + adErrorListeners = new ArrayList<>(); + } + + @Override + public void contentComplete() { + // Do nothing. + } + + @Override + public ImaSdkSettings getSettings() { + return imaSdkSettings; + } + + @Override + public void requestAds(AdsRequest adsRequest) { + for (AdsLoadedListener listener : adsLoadedListeners) { + listener.onAdsManagerLoaded( + new AdsManagerLoadedEvent() { + @Override + public AdsManager getAdsManager() { + return adsManager; + } + + @Override + public StreamManager getStreamManager() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getUserRequestContext() { + return adsRequest.getUserRequestContext(); + } + }); + } + } + + @Override + public String requestStream(StreamRequest streamRequest) { + throw new UnsupportedOperationException(); + } + + @Override + public void addAdsLoadedListener(AdsLoadedListener adsLoadedListener) { + adsLoadedListeners.add(adsLoadedListener); + } + + @Override + public void removeAdsLoadedListener(AdsLoadedListener adsLoadedListener) { + adsLoadedListeners.remove(adsLoadedListener); + } + + @Override + public void addAdErrorListener(AdErrorListener adErrorListener) { + adErrorListeners.add(adErrorListener); + } + + @Override + public void removeAdErrorListener(AdErrorListener adErrorListener) { + adErrorListeners.remove(adErrorListener); + } +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java new file mode 100644 index 0000000000..7c2c8a6e0b --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAdsRequest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; +import java.util.List; +import java.util.Map; + +/** Fake {@link AdsRequest} implementation for tests. */ +public final class FakeAdsRequest implements AdsRequest { + + private String adTagUrl; + private String adsResponse; + private Object userRequestContext; + private AdDisplayContainer adDisplayContainer; + private ContentProgressProvider contentProgressProvider; + + @Override + public void setAdTagUrl(String adTagUrl) { + this.adTagUrl = adTagUrl; + } + + @Override + public String getAdTagUrl() { + return adTagUrl; + } + + @Override + public void setExtraParameter(String s, String s1) { + throw new UnsupportedOperationException(); + } + + @Override + public String getExtraParameter(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public Map getExtraParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public void setUserRequestContext(Object userRequestContext) { + this.userRequestContext = userRequestContext; + } + + @Override + public Object getUserRequestContext() { + return userRequestContext; + } + + @Override + public AdDisplayContainer getAdDisplayContainer() { + return adDisplayContainer; + } + + @Override + public void setAdDisplayContainer(AdDisplayContainer adDisplayContainer) { + this.adDisplayContainer = adDisplayContainer; + } + + @Override + public ContentProgressProvider getContentProgressProvider() { + return contentProgressProvider; + } + + @Override + public void setContentProgressProvider(ContentProgressProvider contentProgressProvider) { + this.contentProgressProvider = contentProgressProvider; + } + + @Override + public String getAdsResponse() { + return adsResponse; + } + + @Override + public void setAdsResponse(String adsResponse) { + this.adsResponse = adsResponse; + } + + @Override + public void setAdWillAutoPlay(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public void setAdWillPlayMuted(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentDuration(float v) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentKeywords(List list) { + throw new UnsupportedOperationException(); + } + + @Override + public void setContentTitle(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public void setVastLoadTimeout(float v) { + throw new UnsupportedOperationException(); + } + + @Override + public void setLiveStreamPrefetchSeconds(float v) { + throw new UnsupportedOperationException(); + } +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java new file mode 100644 index 0000000000..11ed214279 --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.testutil.StubExoPlayer; +import java.util.ArrayList; + +/** A fake player for testing content/ad playback. */ +/* package */ final class FakePlayer extends StubExoPlayer { + + private final ArrayList listeners; + private final Timeline.Window window; + private final Timeline.Period period; + + private boolean prepared; + private Timeline timeline; + private int state; + private boolean playWhenReady; + private long position; + private long contentPosition; + private boolean isPlayingAd; + private int adGroupIndex; + private int adIndexInAdGroup; + + public FakePlayer() { + listeners = new ArrayList<>(); + window = new Timeline.Window(); + period = new Timeline.Period(); + state = Player.STATE_IDLE; + playWhenReady = true; + timeline = Timeline.EMPTY; + } + + /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */ + public void updateTimeline(Timeline timeline) { + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged( + timeline, + null, + prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED); + } + prepared = true; + } + + /** + * Sets the state of this player as if it were playing content at the given {@code position}. If + * an ad is currently playing, this will trigger a position discontinuity. + */ + public void setPlayingContentPosition(long position) { + boolean notify = isPlayingAd; + isPlayingAd = false; + adGroupIndex = C.INDEX_UNSET; + adIndexInAdGroup = C.INDEX_UNSET; + this.position = position; + contentPosition = position; + if (notify) { + for (Player.EventListener listener : listeners) { + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION); + } + } + } + + /** + * Sets the state of this player as if it were playing an ad with the given indices at the given + * {@code position}. If the player is playing a different ad or content, this will trigger a + * position discontinuity. + */ + public void setPlayingAdPosition( + int adGroupIndex, int adIndexInAdGroup, long position, long contentPosition) { + boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup; + isPlayingAd = true; + this.adGroupIndex = adGroupIndex; + this.adIndexInAdGroup = adIndexInAdGroup; + this.position = position; + this.contentPosition = contentPosition; + if (notify) { + for (Player.EventListener listener : listeners) { + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION); + } + } + } + + /** Sets the state of this player with the given {@code STATE} constant. */ + public void setState(int state, boolean playWhenReady) { + boolean notify = this.state != state || this.playWhenReady != playWhenReady; + this.state = state; + this.playWhenReady = playWhenReady; + if (notify) { + for (Player.EventListener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, state); + } + } + } + + // ExoPlayer methods. Other methods are unsupported. + + @Override + public void addListener(Player.EventListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(Player.EventListener listener) { + listeners.remove(listener); + } + + @Override + public int getPlaybackState() { + return state; + } + + @Override + public boolean getPlayWhenReady() { + return playWhenReady; + } + + @Override + public Timeline getCurrentTimeline() { + return timeline; + } + + @Override + public int getCurrentPeriodIndex() { + return 0; + } + + @Override + public int getCurrentWindowIndex() { + return 0; + } + + @Override + public int getNextWindowIndex() { + return C.INDEX_UNSET; + } + + @Override + public int getPreviousWindowIndex() { + return C.INDEX_UNSET; + } + + @Override + public long getDuration() { + if (timeline.isEmpty()) { + return C.INDEX_UNSET; + } + if (isPlayingAd()) { + long adDurationUs = + timeline.getPeriod(0, period).getAdDurationUs(adGroupIndex, adIndexInAdGroup); + return C.usToMs(adDurationUs); + } else { + return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + } + } + + @Override + public long getCurrentPosition() { + return position; + } + + @Override + public boolean isPlayingAd() { + return isPlayingAd; + } + + @Override + public int getCurrentAdGroupIndex() { + return adGroupIndex; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return adIndexInAdGroup; + } + + @Override + public long getContentPosition() { + return contentPosition; + } +} 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 new file mode 100644 index 0000000000..b0fe731480 --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.net.Uri; +import android.support.annotation.Nullable; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import com.google.ads.interactivemedia.v3.api.Ad; +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdEvent; +import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; +import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.SinglePeriodTimeline; +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.SinglePeriodAdTimeline; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Test for {@link ImaAdsLoader}. */ +@RunWith(RobolectricTestRunner.class) +public class ImaAdsLoaderTest { + + private static final long CONTENT_DURATION_US = 10 * C.MICROS_PER_SECOND; + private static final Timeline CONTENT_TIMELINE = + new SinglePeriodTimeline(CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false); + private static final Uri TEST_URI = Uri.EMPTY; + private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND; + private static final long[][] PREROLL_ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}}; + private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f}; + private static final FakeAd UNSKIPPABLE_AD = + new FakeAd(/* skippable= */ false, /* podIndex= */ 0, /* totalAds= */ 1, /* adPosition= */ 1); + + private @Mock ImaSdkSettings imaSdkSettings; + private @Mock AdsRenderingSettings adsRenderingSettings; + private @Mock AdDisplayContainer adDisplayContainer; + private @Mock AdsManager adsManager; + private SingletonImaFactory testImaFactory; + private ViewGroup adUiViewGroup; + private TestAdsLoaderListener adsLoaderListener; + private FakePlayer fakeExoPlayer; + private ImaAdsLoader imaAdsLoader; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + FakeAdsRequest fakeAdsRequest = new FakeAdsRequest(); + FakeAdsLoader fakeAdsLoader = new FakeAdsLoader(imaSdkSettings, adsManager); + testImaFactory = + new SingletonImaFactory( + imaSdkSettings, + adsRenderingSettings, + adDisplayContainer, + fakeAdsRequest, + fakeAdsLoader); + adUiViewGroup = new FrameLayout(RuntimeEnvironment.application); + } + + @After + public void teardown() { + if (imaAdsLoader != null) { + imaAdsLoader.release(); + } + } + + @Test + public void testBuilder_overridesPlayerType() { + when(imaSdkSettings.getPlayerType()).thenReturn("test player type"); + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + + verify(imaSdkSettings).setPlayerType("google/exo.ext.ima"); + } + + @Test + public void testAttachPlayer_setsAdUiViewGroup() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + + verify(adDisplayContainer, atLeastOnce()).setAdContainer(adUiViewGroup); + } + + @Test + public void testAttachPlayer_updatesAdPlaybackState() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + + assertThat(adsLoaderListener.adPlaybackState) + .isEqualTo( + new AdPlaybackState(/* adGroupTimesUs= */ 0) + .withAdDurationsUs(PREROLL_ADS_DURATIONS_US)); + } + + @Test + public void testAttachAfterRelease() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.release(); + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + } + + @Test + public void testAttachAndCallbacksAfterRelease() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + imaAdsLoader.release(); + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + fakeExoPlayer.setPlayingContentPosition(/* position= */ 0); + fakeExoPlayer.setState(Player.STATE_READY, true); + + // If callbacks are invoked there is no crash. + // Note: we can't currently call getContentProgress/getAdProgress as a VerifyError is thrown + // when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA + // SDK being proguarded. + imaAdsLoader.requestAds(adUiViewGroup); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD)); + imaAdsLoader.loadAd(TEST_URI.toString()); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD)); + imaAdsLoader.playAd(); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD)); + imaAdsLoader.pauseAd(); + imaAdsLoader.stopAd(); + imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException())); + imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); + imaAdsLoader.handlePrepareError( + /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException()); + } + + @Test + public void testPlayback_withPrerollAd_marksAdAsPlayed() { + setupPlayback(CONTENT_TIMELINE, PREROLL_ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS); + + // Load the preroll ad. + imaAdsLoader.attachPlayer(fakeExoPlayer, adsLoaderListener, adUiViewGroup); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, UNSKIPPABLE_AD)); + imaAdsLoader.loadAd(TEST_URI.toString()); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, UNSKIPPABLE_AD)); + + // Play the preroll ad. + imaAdsLoader.playAd(); + fakeExoPlayer.setPlayingAdPosition( + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* position= */ 0, + /* contentPosition= */ 0); + fakeExoPlayer.setState(Player.STATE_READY, true); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, UNSKIPPABLE_AD)); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, UNSKIPPABLE_AD)); + + // Play the content. + fakeExoPlayer.setPlayingContentPosition(0); + imaAdsLoader.stopAd(); + imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null)); + + // Verify that the preroll ad has been marked as played. + assertThat(adsLoaderListener.adPlaybackState) + .isEqualTo( + new AdPlaybackState(/* adGroupTimesUs= */ 0) + .withContentDurationUs(CONTENT_DURATION_US) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI) + .withAdDurationsUs(PREROLL_ADS_DURATIONS_US) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .withAdResumePositionUs(/* adResumePositionUs= */ 0)); + } + + private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) { + fakeExoPlayer = new FakePlayer(); + adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs); + when(adsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints)); + imaAdsLoader = + new ImaAdsLoader.Builder(RuntimeEnvironment.application) + .setImaFactory(testImaFactory) + .setImaSdkSettings(imaSdkSettings) + .buildForAdTag(TEST_URI); + } + + private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) { + return new AdEvent() { + @Override + public AdEventType getType() { + return adEventType; + } + + @Override + public @Nullable Ad getAd() { + return ad; + } + + @Override + public Map getAdData() { + return Collections.emptyMap(); + } + }; + } + + /** Ad loader event listener that forwards ad playback state to a fake player. */ + private static final class TestAdsLoaderListener implements AdsLoader.EventListener { + + private final FakePlayer fakeExoPlayer; + private final Timeline contentTimeline; + private final long[][] adDurationsUs; + + public AdPlaybackState adPlaybackState; + + public TestAdsLoaderListener( + FakePlayer fakeExoPlayer, Timeline contentTimeline, long[][] adDurationsUs) { + this.fakeExoPlayer = fakeExoPlayer; + this.contentTimeline = contentTimeline; + this.adDurationsUs = adDurationsUs; + } + + @Override + public void onAdPlaybackState(AdPlaybackState adPlaybackState) { + adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); + this.adPlaybackState = adPlaybackState; + fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState)); + } + + @Override + public void onAdLoadError(AdLoadException error, DataSpec dataSpec) { + assertThat(error.type).isNotEqualTo(AdLoadException.TYPE_UNEXPECTED); + } + + @Override + public void onAdClicked() { + // Do nothing. + } + + @Override + public void onAdTapped() { + // Do nothing. + } + } +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java new file mode 100644 index 0000000000..dd46d8a68b --- /dev/null +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import android.content.Context; +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; + +/** {@link ImaAdsLoader.ImaFactory} that returns provided instances from each getter, for tests. */ +final class SingletonImaFactory implements ImaAdsLoader.ImaFactory { + + private final ImaSdkSettings imaSdkSettings; + private final AdsRenderingSettings adsRenderingSettings; + private final AdDisplayContainer adDisplayContainer; + private final AdsRequest adsRequest; + private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + + public SingletonImaFactory( + ImaSdkSettings imaSdkSettings, + AdsRenderingSettings adsRenderingSettings, + AdDisplayContainer adDisplayContainer, + AdsRequest adsRequest, + com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) { + this.imaSdkSettings = imaSdkSettings; + this.adsRenderingSettings = adsRenderingSettings; + this.adDisplayContainer = adDisplayContainer; + this.adsRequest = adsRequest; + this.adsLoader = adsLoader; + } + + @Override + public ImaSdkSettings createImaSdkSettings() { + return imaSdkSettings; + } + + @Override + public AdsRenderingSettings createAdsRenderingSettings() { + return adsRenderingSettings; + } + + @Override + public AdDisplayContainer createAdDisplayContainer() { + return adDisplayContainer; + } + + @Override + public AdsRequest createAdsRequest() { + return adsRequest; + } + + @Override + public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( + Context context, ImaSdkSettings imaSdkSettings) { + return adsLoader; + } +} diff --git a/extensions/ima/src/test/resources/robolectric.properties b/extensions/ima/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/extensions/ima/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml