diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
new file mode 100644
index 0000000000..b336d84dd2
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2019 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.analytics;
+
+import androidx.annotation.Nullable;
+import android.util.Base64;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Player.DiscontinuityReason;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Random;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the
+ * timeline and also for each ad within the windows.
+ *
+ *
Sessions are identified by Base64-encoded, URL-safe, random strings.
+ */
+public final class DefaultPlaybackSessionManager implements PlaybackSessionManager {
+
+ private static final Random RANDOM = new Random();
+ private static final int SESSION_ID_LENGTH = 12;
+
+ private final Timeline.Window window;
+ private final Timeline.Period period;
+ private final HashMap sessions;
+
+ @MonotonicNonNull private Listener listener;
+ private Timeline currentTimeline;
+ @Nullable private MediaPeriodId currentMediaPeriodId;
+ @Nullable private String activeSessionId;
+
+ /** Creates session manager. */
+ public DefaultPlaybackSessionManager() {
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ sessions = new HashMap<>();
+ currentTimeline = Timeline.EMPTY;
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public synchronized String getSessionForMediaPeriodId(
+ Timeline timeline, MediaPeriodId mediaPeriodId) {
+ int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
+ return getOrAddSession(windowIndex, mediaPeriodId).sessionId;
+ }
+
+ @Override
+ public synchronized boolean belongsToSession(EventTime eventTime, String sessionId) {
+ SessionDescriptor sessionDescriptor = sessions.get(sessionId);
+ if (sessionDescriptor == null) {
+ return false;
+ }
+ sessionDescriptor.maybeSetWindowSequenceNumber(eventTime.windowIndex, eventTime.mediaPeriodId);
+ return sessionDescriptor.belongsToSession(eventTime.windowIndex, eventTime.mediaPeriodId);
+ }
+
+ @Override
+ public synchronized void updateSessions(EventTime eventTime) {
+ boolean isObviouslyFinished =
+ eventTime.mediaPeriodId != null
+ && currentMediaPeriodId != null
+ && eventTime.mediaPeriodId.windowSequenceNumber
+ < currentMediaPeriodId.windowSequenceNumber;
+ if (!isObviouslyFinished) {
+ SessionDescriptor descriptor =
+ getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
+ if (!descriptor.isCreated) {
+ descriptor.isCreated = true;
+ Assertions.checkNotNull(listener).onSessionCreated(eventTime, descriptor.sessionId);
+ if (activeSessionId == null) {
+ updateActiveSession(eventTime, descriptor);
+ }
+ }
+ }
+ }
+
+ @Override
+ public synchronized void handleTimelineUpdate(EventTime eventTime) {
+ Assertions.checkNotNull(listener);
+ Timeline previousTimeline = currentTimeline;
+ currentTimeline = eventTime.timeline;
+ Iterator iterator = sessions.values().iterator();
+ while (iterator.hasNext()) {
+ SessionDescriptor session = iterator.next();
+ if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) {
+ iterator.remove();
+ if (session.isCreated) {
+ if (session.sessionId.equals(activeSessionId)) {
+ activeSessionId = null;
+ }
+ listener.onSessionFinished(
+ eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);
+ }
+ }
+ }
+ handlePositionDiscontinuity(eventTime, Player.DISCONTINUITY_REASON_INTERNAL);
+ }
+
+ @Override
+ public synchronized void handlePositionDiscontinuity(
+ EventTime eventTime, @DiscontinuityReason int reason) {
+ Assertions.checkNotNull(listener);
+ boolean hasAutomaticTransition =
+ reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
+ || reason == Player.DISCONTINUITY_REASON_AD_INSERTION;
+ Iterator iterator = sessions.values().iterator();
+ while (iterator.hasNext()) {
+ SessionDescriptor session = iterator.next();
+ if (session.isFinishedAtEventTime(eventTime)) {
+ iterator.remove();
+ if (session.isCreated) {
+ boolean isRemovingActiveSession = session.sessionId.equals(activeSessionId);
+ boolean isAutomaticTransition = hasAutomaticTransition && isRemovingActiveSession;
+ if (isRemovingActiveSession) {
+ activeSessionId = null;
+ }
+ listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition);
+ }
+ }
+ }
+ SessionDescriptor activeSessionDescriptor =
+ getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
+ if (eventTime.mediaPeriodId != null
+ && eventTime.mediaPeriodId.isAd()
+ && (currentMediaPeriodId == null
+ || currentMediaPeriodId.windowSequenceNumber
+ != eventTime.mediaPeriodId.windowSequenceNumber
+ || currentMediaPeriodId.adGroupIndex != eventTime.mediaPeriodId.adGroupIndex
+ || currentMediaPeriodId.adIndexInAdGroup != eventTime.mediaPeriodId.adIndexInAdGroup)) {
+ // New ad playback started. Find corresponding content session and notify ad playback started.
+ MediaPeriodId contentMediaPeriodId =
+ new MediaPeriodId(
+ eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber);
+ SessionDescriptor contentSession =
+ getOrAddSession(eventTime.windowIndex, contentMediaPeriodId);
+ if (contentSession.isCreated && activeSessionDescriptor.isCreated) {
+ listener.onAdPlaybackStarted(
+ eventTime, contentSession.sessionId, activeSessionDescriptor.sessionId);
+ }
+ }
+ updateActiveSession(eventTime, activeSessionDescriptor);
+ }
+
+ private SessionDescriptor getOrAddSession(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is
+ // null, there may be multiple matching sessions with different window sequence numbers or
+ // adMediaPeriodIds. The best match is the one with the smaller window sequence number, and for
+ // windows with ads, the content session is preferred over ad sessions.
+ SessionDescriptor bestMatch = null;
+ long bestMatchWindowSequenceNumber = Long.MAX_VALUE;
+ for (SessionDescriptor sessionDescriptor : sessions.values()) {
+ sessionDescriptor.maybeSetWindowSequenceNumber(windowIndex, mediaPeriodId);
+ if (sessionDescriptor.belongsToSession(windowIndex, mediaPeriodId)) {
+ long windowSequenceNumber = sessionDescriptor.windowSequenceNumber;
+ if (windowSequenceNumber == C.INDEX_UNSET
+ || windowSequenceNumber < bestMatchWindowSequenceNumber) {
+ bestMatch = sessionDescriptor;
+ bestMatchWindowSequenceNumber = windowSequenceNumber;
+ } else if (windowSequenceNumber == bestMatchWindowSequenceNumber
+ && Util.castNonNull(bestMatch).adMediaPeriodId != null
+ && sessionDescriptor.adMediaPeriodId != null) {
+ bestMatch = sessionDescriptor;
+ }
+ }
+ }
+ if (bestMatch == null) {
+ String sessionId = generateSessionId();
+ bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId);
+ sessions.put(sessionId, bestMatch);
+ }
+ return bestMatch;
+ }
+
+ @RequiresNonNull("listener")
+ private void updateActiveSession(EventTime eventTime, SessionDescriptor sessionDescriptor) {
+ currentMediaPeriodId = eventTime.mediaPeriodId;
+ if (sessionDescriptor.isCreated && !sessionDescriptor.isActive) {
+ sessionDescriptor.isActive = true;
+ activeSessionId = sessionDescriptor.sessionId;
+ listener.onSessionActive(eventTime, sessionDescriptor.sessionId);
+ }
+ }
+
+ private static String generateSessionId() {
+ byte[] randomBytes = new byte[SESSION_ID_LENGTH];
+ RANDOM.nextBytes(randomBytes);
+ return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP);
+ }
+
+ /**
+ * Descriptor for a session.
+ *
+ * The session may be described in one of three ways:
+ *
+ *
+ * - A window index with unset window sequence number and a null ad media period id
+ *
- A content window with index and sequence number, but a null ad media period id.
+ *
- An ad with all values set.
+ *
+ */
+ private final class SessionDescriptor {
+
+ private final String sessionId;
+
+ private int windowIndex;
+ private long windowSequenceNumber;
+ private @MonotonicNonNull MediaPeriodId adMediaPeriodId;
+
+ private boolean isCreated;
+ private boolean isActive;
+
+ public SessionDescriptor(
+ String sessionId, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ this.sessionId = sessionId;
+ this.windowIndex = windowIndex;
+ this.windowSequenceNumber =
+ mediaPeriodId == null ? C.INDEX_UNSET : mediaPeriodId.windowSequenceNumber;
+ if (mediaPeriodId != null && mediaPeriodId.isAd()) {
+ this.adMediaPeriodId = mediaPeriodId;
+ }
+ }
+
+ public boolean tryResolvingToNewTimeline(Timeline oldTimeline, Timeline newTimeline) {
+ windowIndex = resolveWindowIndexToNewTimeline(oldTimeline, newTimeline, windowIndex);
+ if (windowIndex == C.INDEX_UNSET) {
+ return false;
+ }
+ if (adMediaPeriodId != null) {
+ int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid);
+ if (newPeriodIndex == C.INDEX_UNSET) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public boolean belongsToSession(
+ int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {
+ if (eventMediaPeriodId == null) {
+ // Events without concrete media period id are for all sessions of the same window.
+ return eventWindowIndex == windowIndex;
+ }
+ if (adMediaPeriodId == null) {
+ // If this is a content session, only events for content with the same window sequence
+ // number belong to this session.
+ return !eventMediaPeriodId.isAd()
+ && eventMediaPeriodId.windowSequenceNumber == windowSequenceNumber;
+ }
+ // If this is an ad session, only events for this ad belong to the session.
+ return eventMediaPeriodId.windowSequenceNumber == adMediaPeriodId.windowSequenceNumber
+ && eventMediaPeriodId.adGroupIndex == adMediaPeriodId.adGroupIndex
+ && eventMediaPeriodId.adIndexInAdGroup == adMediaPeriodId.adIndexInAdGroup;
+ }
+
+ public void maybeSetWindowSequenceNumber(
+ int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {
+ if (windowSequenceNumber == C.INDEX_UNSET
+ && eventWindowIndex == windowIndex
+ && eventMediaPeriodId != null
+ && !eventMediaPeriodId.isAd()) {
+ // Set window sequence number for this session as soon as we have one.
+ windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber;
+ }
+ }
+
+ public boolean isFinishedAtEventTime(EventTime eventTime) {
+ if (windowSequenceNumber == C.INDEX_UNSET) {
+ // Sessions with unspecified window sequence number are kept until we know more.
+ return false;
+ }
+ if (eventTime.mediaPeriodId == null) {
+ // For event times without media period id (e.g. after seek to new window), we only keep
+ // sessions of this window.
+ return windowIndex != eventTime.windowIndex;
+ }
+ if (eventTime.mediaPeriodId.windowSequenceNumber > windowSequenceNumber) {
+ // All past window sequence numbers are finished.
+ return true;
+ }
+ if (adMediaPeriodId == null) {
+ // Current or future content is not finished.
+ return false;
+ }
+ int eventPeriodIndex = eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid);
+ int adPeriodIndex = eventTime.timeline.getIndexOfPeriod(adMediaPeriodId.periodUid);
+ if (eventTime.mediaPeriodId.windowSequenceNumber < adMediaPeriodId.windowSequenceNumber
+ || eventPeriodIndex < adPeriodIndex) {
+ // Ads in future windows or periods are not finished.
+ return false;
+ }
+ if (eventPeriodIndex > adPeriodIndex) {
+ // Ads in past periods are finished.
+ return true;
+ }
+ if (eventTime.mediaPeriodId.isAd()) {
+ int eventAdGroup = eventTime.mediaPeriodId.adGroupIndex;
+ int eventAdIndex = eventTime.mediaPeriodId.adIndexInAdGroup;
+ // Finished if event is for an ad after this one in the same period.
+ return eventAdGroup > adMediaPeriodId.adGroupIndex
+ || (eventAdGroup == adMediaPeriodId.adGroupIndex
+ && eventAdIndex > adMediaPeriodId.adIndexInAdGroup);
+ } else {
+ eventTime.timeline.getPeriod(adPeriodIndex, period);
+ long adGroupTimeMs =
+ adMediaPeriodId.adGroupIndex < period.getAdGroupCount()
+ ? C.usToMs(period.getAdGroupTimeUs(adMediaPeriodId.adGroupIndex))
+ : 0;
+ // Finished if the event is for content after this ad.
+ return adGroupTimeMs <= eventTime.currentPlaybackPositionMs;
+ }
+ }
+
+ private int resolveWindowIndexToNewTimeline(
+ Timeline oldTimeline, Timeline newTimeline, int windowIndex) {
+ if (windowIndex >= oldTimeline.getWindowCount()) {
+ return windowIndex < newTimeline.getWindowCount() ? windowIndex : C.INDEX_UNSET;
+ }
+ oldTimeline.getWindow(windowIndex, window);
+ for (int periodIndex = window.firstPeriodIndex;
+ periodIndex <= window.lastPeriodIndex;
+ periodIndex++) {
+ Object periodUid = oldTimeline.getUidOfPeriod(periodIndex);
+ int newPeriodIndex = newTimeline.getIndexOfPeriod(periodUid);
+ if (newPeriodIndex != C.INDEX_UNSET) {
+ return newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java
new file mode 100644
index 0000000000..53d63e23fc
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2019 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.analytics;
+
+import com.google.android.exoplayer2.Player.DiscontinuityReason;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+
+/**
+ * Manager for active playback sessions.
+ *
+ * The manager keeps track of the association between window index and/or media period id to
+ * session identifier.
+ */
+public interface PlaybackSessionManager {
+
+ /** A listener for session updates. */
+ interface Listener {
+
+ /**
+ * Called when a new session is created as a result of {@link #updateSessions(EventTime)}.
+ *
+ * @param eventTime The {@link EventTime} at which the session is created.
+ * @param sessionId The identifier of the new session.
+ */
+ void onSessionCreated(EventTime eventTime, String sessionId);
+
+ /**
+ * Called when a session becomes active, i.e. playing in the foreground.
+ *
+ * @param eventTime The {@link EventTime} at which the session becomes active.
+ * @param sessionId The identifier of the session.
+ */
+ void onSessionActive(EventTime eventTime, String sessionId);
+
+ /**
+ * Called when a session is interrupted by ad playback.
+ *
+ * @param eventTime The {@link EventTime} at which the ad playback starts.
+ * @param contentSessionId The session identifier of the content session.
+ * @param adSessionId The identifier of the ad session.
+ */
+ void onAdPlaybackStarted(EventTime eventTime, String contentSessionId, String adSessionId);
+
+ /**
+ * Called when a session is permanently finished.
+ *
+ * @param eventTime The {@link EventTime} at which the session finished.
+ * @param sessionId The identifier of the finished session.
+ * @param automaticTransitionToNextPlayback Whether the session finished because of an automatic
+ * transition to the next playback item.
+ */
+ void onSessionFinished(
+ EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback);
+ }
+
+ /**
+ * Sets the listener to be notified of session updates. Must be called before the session manager
+ * is used.
+ *
+ * @param listener The {@link Listener} to be notified of session updates.
+ */
+ void setListener(Listener listener);
+
+ /**
+ * Returns the session identifier for the given media period id.
+ *
+ *
Note that this will reserve a new session identifier if it doesn't exist yet, but will not
+ * call any {@link Listener} callbacks.
+ *
+ * @param timeline The timeline, {@code mediaPeriodId} is part of.
+ * @param mediaPeriodId A {@link MediaPeriodId}.
+ */
+ String getSessionForMediaPeriodId(Timeline timeline, MediaPeriodId mediaPeriodId);
+
+ /**
+ * Returns whether an event time belong to a session.
+ *
+ * @param eventTime The {@link EventTime}.
+ * @param sessionId A session identifier.
+ * @return Whether the event belongs to the specified session.
+ */
+ boolean belongsToSession(EventTime eventTime, String sessionId);
+
+ /**
+ * Updates or creates sessions based on a player {@link EventTime}.
+ *
+ * @param eventTime The {@link EventTime}.
+ */
+ void updateSessions(EventTime eventTime);
+
+ /**
+ * Updates the session associations to a new timeline.
+ *
+ * @param eventTime The event time with the timeline change.
+ */
+ void handleTimelineUpdate(EventTime eventTime);
+
+ /**
+ * Handles a position discontinuity.
+ *
+ * @param eventTime The event time of the position discontinuity.
+ * @param reason The {@link DiscontinuityReason}.
+ */
+ void handlePositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason);
+}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java
new file mode 100644
index 0000000000..2993e960b4
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java
@@ -0,0 +1,957 @@
+/*
+ * Copyright (C) 2019 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.analytics;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.source.ads.AdPlaybackState;
+import com.google.android.exoplayer2.testutil.FakeTimeline;
+import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit test for {@link DefaultPlaybackSessionManager}. */
+@RunWith(AndroidJUnit4.class)
+public final class DefaultPlaybackSessionManagerTest {
+
+ private DefaultPlaybackSessionManager sessionManager;
+
+ @Mock private PlaybackSessionManager.Listener mockListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ sessionManager = new DefaultPlaybackSessionManager();
+ sessionManager.setListener(mockListener);
+ }
+
+ @Test
+ public void updateSessions_withoutMediaPeriodId_createsNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ EventTime eventTime = createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId */ null);
+
+ sessionManager.updateSessions(eventTime);
+
+ verify(mockListener).onSessionCreated(eq(eventTime), anyString());
+ verify(mockListener).onSessionActive(eq(eventTime), anyString());
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void updateSessions_withMediaPeriodId_createsNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ EventTime eventTime = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId);
+
+ sessionManager.updateSessions(eventTime);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture());
+ verify(mockListener).onSessionActive(eventTime, sessionId.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId))
+ .isEqualTo(sessionId.getValue());
+ }
+
+ @Test
+ public void
+ updateSessions_ofSameWindow_withMediaPeriodId_afterWithoutMediaPeriodId_doesNotCreateNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ EventTime eventTime1 =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture());
+ verify(mockListener).onSessionActive(eventTime1, sessionId.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId))
+ .isEqualTo(sessionId.getValue());
+ }
+
+ @Test
+ public void updateSessions_ofSameWindow_withAd_afterWithoutMediaPeriodId_createsNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0);
+ EventTime eventTime1 =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor contentSessionId = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor adSessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), contentSessionId.capture());
+ verify(mockListener).onSessionCreated(eq(eventTime2), adSessionId.capture());
+ verify(mockListener).onSessionActive(eventTime1, contentSessionId.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(contentSessionId).isNotEqualTo(adSessionId);
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId))
+ .isEqualTo(adSessionId.getValue());
+ }
+
+ @Test
+ public void
+ updateSessions_ofSameWindow_withoutMediaPeriodId_afterMediaPeriodId_doesNotCreateNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId);
+ EventTime eventTime2 =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture());
+ verify(mockListener).onSessionActive(eventTime1, sessionId.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId))
+ .isEqualTo(sessionId.getValue());
+ }
+
+ @Test
+ public void updateSessions_ofSameWindow_withoutMediaPeriodId_afterAd_doesNotCreateNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0);
+ EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId);
+ EventTime eventTime2 =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture());
+ verify(mockListener).onSessionActive(eventTime1, sessionId.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId))
+ .isEqualTo(sessionId.getValue());
+ }
+
+ @Test
+ public void updateSessions_withOtherMediaPeriodId_ofSameWindow_doesNotCreateNewSession() {
+ Timeline timeline =
+ new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 0));
+ MediaPeriodId mediaPeriodId1 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ MediaPeriodId mediaPeriodId2 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0);
+ EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1);
+ EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture());
+ verify(mockListener).onSessionActive(eventTime1, sessionId.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId1))
+ .isEqualTo(sessionId.getValue());
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId2))
+ .isEqualTo(sessionId.getValue());
+ }
+
+ @Test
+ public void updateSessions_withAd_ofSameWindow_createsNewSession() {
+ Timeline timeline =
+ new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 0));
+ MediaPeriodId mediaPeriodId1 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ MediaPeriodId mediaPeriodId2 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0);
+ EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1);
+ EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor contentSessionId = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor adSessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), contentSessionId.capture());
+ verify(mockListener).onSessionActive(eventTime1, contentSessionId.getValue());
+ verify(mockListener).onSessionCreated(eq(eventTime2), adSessionId.capture());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(contentSessionId).isNotEqualTo(adSessionId);
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId1))
+ .isEqualTo(contentSessionId.getValue());
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId2))
+ .isEqualTo(adSessionId.getValue());
+ }
+
+ @Test
+ public void updateSessions_ofOtherWindow_createsNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
+ EventTime eventTime1 =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ EventTime eventTime2 =
+ createEventTime(timeline, /* windowIndex= */ 1, /* mediaPeriodId= */ null);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture());
+ verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture());
+ verify(mockListener).onSessionActive(eventTime1, sessionId1.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionId1).isNotEqualTo(sessionId2);
+ }
+
+ @Test
+ public void updateSessions_withMediaPeriodId_ofOtherWindow_createsNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
+ MediaPeriodId mediaPeriodId1 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ MediaPeriodId mediaPeriodId2 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1);
+ EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1);
+ EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture());
+ verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture());
+ verify(mockListener).onSessionActive(eventTime1, sessionId1.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionId1).isNotEqualTo(sessionId2);
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId1))
+ .isEqualTo(sessionId1.getValue());
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId2))
+ .isEqualTo(sessionId2.getValue());
+ }
+
+ @Test
+ public void updateSessions_ofSameWindow_withNewWindowSequenceNumber_createsNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId1 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ MediaPeriodId mediaPeriodId2 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1);
+ EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1);
+ EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture());
+ verify(mockListener).onSessionActive(eventTime1, sessionId1.getValue());
+ verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionId1).isNotEqualTo(sessionId2);
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId1))
+ .isEqualTo(sessionId1.getValue());
+ assertThat(sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId2))
+ .isEqualTo(sessionId2.getValue());
+ }
+
+ @Test
+ public void
+ updateSessions_withoutMediaPeriodId_andPreviouslyCreatedSessions_doesNotCreateNewSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId1 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ MediaPeriodId mediaPeriodId2 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1);
+ MediaPeriodId mediaPeriodIdWithAd =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0);
+ EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1);
+ EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2);
+ EventTime eventTime3 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodIdWithAd);
+ EventTime eventTime4 =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+ sessionManager.updateSessions(eventTime3);
+ sessionManager.updateSessions(eventTime4);
+
+ verify(mockListener).onSessionCreated(eq(eventTime1), anyString());
+ verify(mockListener).onSessionActive(eq(eventTime1), anyString());
+ verify(mockListener).onSessionCreated(eq(eventTime2), anyString());
+ verify(mockListener).onSessionCreated(eq(eventTime3), anyString());
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void getSessionForMediaPeriodId_returnsValue_butDoesNotCreateSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ String session = sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId);
+
+ assertThat(session).isNotEmpty();
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void updateSessions_afterSessionForMediaPeriodId_withSameMediaPeriodId_returnsSameValue() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ EventTime eventTime = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId);
+
+ String expectedSessionId = sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId);
+ sessionManager.updateSessions(eventTime);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture());
+ verify(mockListener).onSessionActive(eventTime, sessionId.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionId.getValue()).isEqualTo(expectedSessionId);
+ }
+
+ @Test
+ public void updateSessions_withoutMediaPeriodId_afterSessionForMediaPeriodId_returnsSameValue() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ EventTime eventTime =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+
+ String expectedSessionId = sessionManager.getSessionForMediaPeriodId(timeline, mediaPeriodId);
+ sessionManager.updateSessions(eventTime);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture());
+ verify(mockListener).onSessionActive(eventTime, sessionId.getValue());
+ verifyNoMoreInteractions(mockListener);
+ assertThat(sessionId.getValue()).isEqualTo(expectedSessionId);
+ }
+
+ @Test
+ public void belongsToSession_withSameWindowIndex_returnsTrue() {
+ EventTime eventTime =
+ createEventTime(Timeline.EMPTY, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ EventTime eventTimeWithTimeline =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ EventTime eventTimeWithMediaPeriodId =
+ createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId);
+ sessionManager.updateSessions(eventTime);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture());
+ assertThat(sessionManager.belongsToSession(eventTime, sessionId.getValue())).isTrue();
+ assertThat(sessionManager.belongsToSession(eventTimeWithTimeline, sessionId.getValue()))
+ .isTrue();
+ assertThat(sessionManager.belongsToSession(eventTimeWithMediaPeriodId, sessionId.getValue()))
+ .isTrue();
+ }
+
+ @Test
+ public void belongsToSession_withOtherWindowIndex_returnsFalse() {
+ EventTime eventTime =
+ createEventTime(Timeline.EMPTY, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ EventTime eventTimeOtherWindow =
+ createEventTime(Timeline.EMPTY, /* windowIndex= */ 1, /* mediaPeriodId= */ null);
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
+ MediaPeriodId mediaPeriodId =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1);
+ EventTime eventTimeWithOtherMediaPeriodId =
+ createEventTime(timeline, /* windowIndex= */ 1, mediaPeriodId);
+ sessionManager.updateSessions(eventTime);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime), sessionId.capture());
+ assertThat(sessionManager.belongsToSession(eventTimeOtherWindow, sessionId.getValue()))
+ .isFalse();
+ assertThat(
+ sessionManager.belongsToSession(eventTimeWithOtherMediaPeriodId, sessionId.getValue()))
+ .isFalse();
+ }
+
+ @Test
+ public void belongsToSession_withOtherWindowSequenceNumber_returnsFalse() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId1 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ MediaPeriodId mediaPeriodId2 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1);
+ EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1);
+ EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2);
+ sessionManager.updateSessions(eventTime1);
+
+ ArgumentCaptor sessionId = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId.capture());
+ assertThat(sessionManager.belongsToSession(eventTime2, sessionId.getValue())).isFalse();
+ }
+
+ @Test
+ public void belongsToSession_withAd_returnsFalse() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ MediaPeriodId mediaPeriodId1 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0);
+ MediaPeriodId mediaPeriodId2 =
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 1);
+ EventTime eventTime1 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId1);
+ EventTime eventTime2 = createEventTime(timeline, /* windowIndex= */ 0, mediaPeriodId2);
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture());
+ verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture());
+ assertThat(sessionManager.belongsToSession(eventTime2, sessionId1.getValue())).isFalse();
+ assertThat(sessionManager.belongsToSession(eventTime1, sessionId2.getValue())).isFalse();
+ assertThat(sessionManager.belongsToSession(eventTime2, sessionId2.getValue())).isTrue();
+ }
+
+ @Test
+ public void initialTimelineUpdate_finishesAllSessionsOutsideTimeline() {
+ EventTime eventTime1 =
+ createEventTime(Timeline.EMPTY, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ EventTime eventTime2 =
+ createEventTime(Timeline.EMPTY, /* windowIndex= */ 1, /* mediaPeriodId= */ null);
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ EventTime newTimelineEventTime =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+
+ sessionManager.handleTimelineUpdate(newTimelineEventTime);
+
+ ArgumentCaptor sessionId1 = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor sessionId2 = ArgumentCaptor.forClass(String.class);
+ verify(mockListener).onSessionCreated(eq(eventTime1), sessionId1.capture());
+ verify(mockListener).onSessionCreated(eq(eventTime2), sessionId2.capture());
+ verify(mockListener).onSessionActive(eventTime1, sessionId1.getValue());
+ verify(mockListener)
+ .onSessionFinished(
+ newTimelineEventTime,
+ sessionId2.getValue(),
+ /* automaticTransitionToNextPlayback= */ false);
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void dynamicTimelineUpdate_resolvesWindowIndices() {
+ Timeline initialTimeline =
+ new FakeTimeline(
+ new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 100),
+ new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 200),
+ new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 300));
+ EventTime eventForInitialTimelineId100 =
+ createEventTime(
+ initialTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ initialTimeline.getUidOfPeriod(/* periodIndex= */ 1),
+ /* windowSequenceNumber= */ 0));
+ EventTime eventForInitialTimelineId200 =
+ createEventTime(
+ initialTimeline,
+ /* windowIndex= */ 1,
+ new MediaPeriodId(
+ initialTimeline.getUidOfPeriod(/* periodIndex= */ 2),
+ /* windowSequenceNumber= */ 1));
+ EventTime eventForInitialTimelineId300 =
+ createEventTime(
+ initialTimeline,
+ /* windowIndex= */ 2,
+ new MediaPeriodId(
+ initialTimeline.getUidOfPeriod(/* periodIndex= */ 3),
+ /* windowSequenceNumber= */ 2));
+ sessionManager.handleTimelineUpdate(eventForInitialTimelineId100);
+ sessionManager.updateSessions(eventForInitialTimelineId100);
+ sessionManager.updateSessions(eventForInitialTimelineId200);
+ sessionManager.updateSessions(eventForInitialTimelineId300);
+ String sessionId100 =
+ sessionManager.getSessionForMediaPeriodId(
+ initialTimeline, eventForInitialTimelineId100.mediaPeriodId);
+ String sessionId200 =
+ sessionManager.getSessionForMediaPeriodId(
+ initialTimeline, eventForInitialTimelineId200.mediaPeriodId);
+ String sessionId300 =
+ sessionManager.getSessionForMediaPeriodId(
+ initialTimeline, eventForInitialTimelineId300.mediaPeriodId);
+
+ Timeline timelineUpdate =
+ new FakeTimeline(
+ new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 300),
+ new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 100));
+ EventTime eventForTimelineUpdateId100 =
+ createEventTime(
+ timelineUpdate,
+ /* windowIndex= */ 1,
+ new MediaPeriodId(
+ timelineUpdate.getUidOfPeriod(/* periodIndex= */ 1),
+ /* windowSequenceNumber= */ 0));
+ EventTime eventForTimelineUpdateId300 =
+ createEventTime(
+ timelineUpdate,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ timelineUpdate.getUidOfPeriod(/* periodIndex= */ 0),
+ /* windowSequenceNumber= */ 2));
+
+ sessionManager.handleTimelineUpdate(eventForTimelineUpdateId100);
+ String updatedSessionId100 =
+ sessionManager.getSessionForMediaPeriodId(
+ timelineUpdate, eventForTimelineUpdateId100.mediaPeriodId);
+ String updatedSessionId300 =
+ sessionManager.getSessionForMediaPeriodId(
+ timelineUpdate, eventForTimelineUpdateId300.mediaPeriodId);
+
+ verify(mockListener).onSessionCreated(eventForInitialTimelineId100, sessionId100);
+ verify(mockListener).onSessionActive(eventForInitialTimelineId100, sessionId100);
+ verify(mockListener).onSessionCreated(eventForInitialTimelineId200, sessionId200);
+ verify(mockListener).onSessionCreated(eventForInitialTimelineId300, sessionId300);
+ verify(mockListener)
+ .onSessionFinished(
+ eventForTimelineUpdateId100,
+ sessionId200,
+ /* automaticTransitionToNextPlayback= */ false);
+ verifyNoMoreInteractions(mockListener);
+ assertThat(updatedSessionId100).isEqualTo(sessionId100);
+ assertThat(updatedSessionId300).isEqualTo(sessionId300);
+ }
+
+ @Test
+ public void positionDiscontinuity_withinWindow_doesNotFinishSession() {
+ Timeline timeline =
+ new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 100));
+ EventTime eventTime1 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
+ EventTime eventTime2 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
+ sessionManager.handleTimelineUpdate(eventTime1);
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ sessionManager.handlePositionDiscontinuity(
+ eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
+
+ verify(mockListener).onSessionCreated(eq(eventTime1), anyString());
+ verify(mockListener).onSessionActive(eq(eventTime1), anyString());
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void positionDiscontinuity_toNewWindow_withPeriodTransitionReason_finishesSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
+ EventTime eventTime1 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
+ EventTime eventTime2 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 1,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));
+ sessionManager.handleTimelineUpdate(eventTime1);
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+ String sessionId1 =
+ sessionManager.getSessionForMediaPeriodId(timeline, eventTime1.mediaPeriodId);
+ String sessionId2 =
+ sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId);
+
+ sessionManager.handlePositionDiscontinuity(
+ eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
+
+ verify(mockListener).onSessionCreated(eventTime1, sessionId1);
+ verify(mockListener).onSessionActive(eventTime1, sessionId1);
+ verify(mockListener).onSessionCreated(eq(eventTime2), anyString());
+ verify(mockListener)
+ .onSessionFinished(eventTime2, sessionId1, /* automaticTransitionToNextPlayback= */ true);
+ verify(mockListener).onSessionActive(eventTime2, sessionId2);
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void positionDiscontinuity_toNewWindow_withSeekTransitionReason_finishesSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
+ EventTime eventTime1 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
+ EventTime eventTime2 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 1,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));
+ sessionManager.handleTimelineUpdate(eventTime1);
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+ String sessionId1 =
+ sessionManager.getSessionForMediaPeriodId(timeline, eventTime1.mediaPeriodId);
+ String sessionId2 =
+ sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId);
+
+ sessionManager.handlePositionDiscontinuity(eventTime2, Player.DISCONTINUITY_REASON_SEEK);
+
+ verify(mockListener).onSessionCreated(eventTime1, sessionId1);
+ verify(mockListener).onSessionActive(eventTime1, sessionId1);
+ verify(mockListener).onSessionCreated(eq(eventTime2), anyString());
+ verify(mockListener)
+ .onSessionFinished(eventTime2, sessionId1, /* automaticTransitionToNextPlayback= */ false);
+ verify(mockListener).onSessionActive(eventTime2, sessionId2);
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void positionDiscontinuity_toSameWindow_withoutMediaPeriodId_doesNotFinishSession() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
+ EventTime eventTime1 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
+ EventTime eventTime2 =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ sessionManager.handleTimelineUpdate(eventTime1);
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+
+ sessionManager.handlePositionDiscontinuity(eventTime2, Player.DISCONTINUITY_REASON_SEEK);
+
+ verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean());
+ }
+
+ @Test
+ public void positionDiscontinuity_toNewWindow_finishesOnlyPastSessions() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 4);
+ EventTime eventTime1 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
+ EventTime eventTime2 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 1,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));
+ EventTime eventTime3 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 2,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 2), /* windowSequenceNumber= */ 2));
+ EventTime eventTime4 =
+ createEventTime(
+ timeline,
+ /* windowIndex= */ 3,
+ new MediaPeriodId(
+ timeline.getUidOfPeriod(/* periodIndex= */ 3), /* windowSequenceNumber= */ 3));
+ sessionManager.handleTimelineUpdate(eventTime1);
+ sessionManager.updateSessions(eventTime1);
+ sessionManager.updateSessions(eventTime2);
+ sessionManager.updateSessions(eventTime3);
+ sessionManager.updateSessions(eventTime4);
+ String sessionId1 =
+ sessionManager.getSessionForMediaPeriodId(timeline, eventTime1.mediaPeriodId);
+ String sessionId2 =
+ sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId);
+
+ sessionManager.handlePositionDiscontinuity(eventTime3, Player.DISCONTINUITY_REASON_SEEK);
+
+ verify(mockListener).onSessionCreated(eventTime1, sessionId1);
+ verify(mockListener).onSessionActive(eventTime1, sessionId1);
+ verify(mockListener).onSessionCreated(eventTime2, sessionId2);
+ verify(mockListener).onSessionCreated(eq(eventTime3), anyString());
+ verify(mockListener).onSessionCreated(eq(eventTime4), anyString());
+ verify(mockListener)
+ .onSessionFinished(eventTime3, sessionId1, /* automaticTransitionToNextPlayback= */ false);
+ verify(mockListener)
+ .onSessionFinished(eventTime3, sessionId2, /* automaticTransitionToNextPlayback= */ false);
+ verify(mockListener).onSessionActive(eq(eventTime3), anyString());
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void positionDiscontinuity_fromAdToContent_finishesAd() {
+ Timeline adTimeline =
+ new FakeTimeline(
+ new TimelineWindowDefinition(
+ /* periodCount= */ 1,
+ /* id= */ 0,
+ /* isSeekable= */ true,
+ /* isDynamic= */ false,
+ /* durationUs =*/ 10 * C.MICROS_PER_SECOND,
+ new AdPlaybackState(/* adGroupTimesUs= */ 0, 5 * C.MICROS_PER_SECOND)
+ .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
+ .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)));
+ EventTime adEventTime1 =
+ createEventTime(
+ adTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ adTimeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0));
+ EventTime adEventTime2 =
+ createEventTime(
+ adTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ adTimeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 1,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0));
+ EventTime contentEventTime =
+ createEventTime(
+ adTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ adTimeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* windowSequenceNumber= */ 0,
+ /* nextAdGroupIndex= */ 1));
+ sessionManager.handleTimelineUpdate(adEventTime1);
+ sessionManager.updateSessions(adEventTime1);
+ sessionManager.updateSessions(adEventTime2);
+ String adSessionId1 =
+ sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime1.mediaPeriodId);
+
+ sessionManager.handlePositionDiscontinuity(
+ contentEventTime, Player.DISCONTINUITY_REASON_AD_INSERTION);
+
+ verify(mockListener).onSessionCreated(adEventTime1, adSessionId1);
+ verify(mockListener).onSessionActive(adEventTime1, adSessionId1);
+ verify(mockListener).onSessionCreated(eq(adEventTime2), anyString());
+ verify(mockListener)
+ .onSessionFinished(
+ contentEventTime, adSessionId1, /* automaticTransitionToNextPlayback= */ true);
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void positionDiscontinuity_fromContentToAd_doesNotFinishSessions() {
+ Timeline adTimeline =
+ new FakeTimeline(
+ new TimelineWindowDefinition(
+ /* periodCount= */ 1,
+ /* id= */ 0,
+ /* isSeekable= */ true,
+ /* isDynamic= */ false,
+ /* durationUs =*/ 10 * C.MICROS_PER_SECOND,
+ new AdPlaybackState(
+ /* adGroupTimesUs= */ 2 * C.MICROS_PER_SECOND, 5 * C.MICROS_PER_SECOND)
+ .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
+ .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)));
+ EventTime adEventTime1 =
+ createEventTime(
+ adTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ adTimeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0));
+ EventTime adEventTime2 =
+ createEventTime(
+ adTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ adTimeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 1,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0));
+ EventTime contentEventTime =
+ createEventTime(
+ adTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ adTimeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* windowSequenceNumber= */ 0,
+ /* nextAdGroupIndex= */ 0));
+ sessionManager.handleTimelineUpdate(contentEventTime);
+ sessionManager.updateSessions(contentEventTime);
+ sessionManager.updateSessions(adEventTime1);
+ sessionManager.updateSessions(adEventTime2);
+
+ sessionManager.handlePositionDiscontinuity(
+ adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION);
+
+ verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean());
+ }
+
+ @Test
+ public void positionDiscontinuity_fromAdToAd_finishesPastAds_andNotifiesAdPlaybackStated() {
+ Timeline adTimeline =
+ new FakeTimeline(
+ new TimelineWindowDefinition(
+ /* periodCount= */ 1,
+ /* id= */ 0,
+ /* isSeekable= */ true,
+ /* isDynamic= */ false,
+ /* durationUs =*/ 10 * C.MICROS_PER_SECOND,
+ new AdPlaybackState(/* adGroupTimesUs= */ 0, 5 * C.MICROS_PER_SECOND)
+ .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
+ .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)));
+ EventTime adEventTime1 =
+ createEventTime(
+ adTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ adTimeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0));
+ EventTime adEventTime2 =
+ createEventTime(
+ adTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ adTimeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* adGroupIndex= */ 1,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0));
+ EventTime contentEventTime =
+ createEventTime(
+ adTimeline,
+ /* windowIndex= */ 0,
+ new MediaPeriodId(
+ adTimeline.getUidOfPeriod(/* periodIndex= */ 0),
+ /* windowSequenceNumber= */ 0,
+ /* nextAdGroupIndex= */ 1));
+ sessionManager.handleTimelineUpdate(contentEventTime);
+ sessionManager.updateSessions(contentEventTime);
+ sessionManager.updateSessions(adEventTime1);
+ sessionManager.updateSessions(adEventTime2);
+ String contentSessionId =
+ sessionManager.getSessionForMediaPeriodId(adTimeline, contentEventTime.mediaPeriodId);
+ String adSessionId1 =
+ sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime1.mediaPeriodId);
+ String adSessionId2 =
+ sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime2.mediaPeriodId);
+
+ sessionManager.handlePositionDiscontinuity(
+ adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION);
+ sessionManager.handlePositionDiscontinuity(adEventTime2, Player.DISCONTINUITY_REASON_SEEK);
+
+ verify(mockListener).onSessionCreated(eq(contentEventTime), anyString());
+ verify(mockListener).onSessionActive(eq(contentEventTime), anyString());
+ verify(mockListener).onSessionCreated(adEventTime1, adSessionId1);
+ verify(mockListener).onSessionCreated(adEventTime2, adSessionId2);
+ verify(mockListener).onAdPlaybackStarted(adEventTime1, contentSessionId, adSessionId1);
+ verify(mockListener).onSessionActive(adEventTime1, adSessionId1);
+ verify(mockListener)
+ .onSessionFinished(
+ adEventTime2, adSessionId1, /* automaticTransitionToNextPlayback= */ false);
+ verify(mockListener).onAdPlaybackStarted(adEventTime2, contentSessionId, adSessionId2);
+ verify(mockListener).onSessionActive(adEventTime2, adSessionId2);
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ private static EventTime createEventTime(
+ Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ return new EventTime(
+ /* realtimeMs = */ 0,
+ timeline,
+ windowIndex,
+ mediaPeriodId,
+ /* eventPlaybackPositionMs= */ 0,
+ /* currentPlaybackPositionMs= */ 0,
+ /* totalBufferedDurationMs= */ 0);
+ }
+}