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