diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java index a71dac47c5..a10bd038d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -143,12 +143,12 @@ public abstract class BaseMediaSource implements MediaSource { @Override public final void addDrmEventListener(Handler handler, DrmSessionEventListener eventListener) { - eventDispatcher.addEventListener(handler, eventListener); + eventDispatcher.addEventListener(handler, eventListener, DrmSessionEventListener.class); } @Override public final void removeDrmEventListener(DrmSessionEventListener eventListener) { - eventDispatcher.removeEventListener(eventListener); + eventDispatcher.removeEventListener(eventListener, DrmSessionEventListener.class); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 70892ee972..61bb55d8d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import android.net.Uri; +import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -174,7 +175,7 @@ public interface MediaSourceEventListener { } private EventDispatcher( - CopyOnWriteMultiset listeners, + CopyOnWriteMultiset listeners, int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { @@ -184,8 +185,34 @@ public interface MediaSourceEventListener { @Override public EventDispatcher withParameters( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { - return new EventDispatcher( - listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs); + return new EventDispatcher(listenerInfos, windowIndex, mediaPeriodId, mediaTimeOffsetMs); + } + + /** + * Adds a {@link MediaSourceEventListener} to the event dispatcher. + * + *

This is equivalent to {@link #addEventListener(Handler, Object, Class)} with {@code + * listenerClass = MediaSourceEventListener.class} and is intended to ease the transition to + * using {@link MediaSourceEventDispatcher} everywhere. + * + * @param handler A handler on the which listener events will be posted. + * @param eventListener The listener to be added. + */ + public void addEventListener(Handler handler, MediaSourceEventListener eventListener) { + addEventListener(handler, eventListener, MediaSourceEventListener.class); + } + + /** + * Removes a {@link MediaSourceEventListener} from the event dispatcher. + * + *

This is equivalent to {@link #removeEventListener(Object, Class)} with {@code + * listenerClass = MediaSourceEventListener.class} and is intended to ease the transition to + * using {@link MediaSourceEventDispatcher} everywhere. + * + * @param eventListener The listener to be removed. + */ + public void removeEventListener(MediaSourceEventListener eventListener) { + removeEventListener(eventListener, MediaSourceEventListener.class); } public void mediaPeriodCreated() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcher.java index 3b9086f4e6..b0b349a86e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcher.java @@ -50,25 +50,25 @@ public class MediaSourceEventDispatcher { @Nullable public final MediaPeriodId mediaPeriodId; // TODO: Make these private when MediaSourceEventListener.EventDispatcher is deleted. - protected final CopyOnWriteMultiset listenerAndHandlers; + protected final CopyOnWriteMultiset listenerInfos; // TODO: Define exactly what this means, and check it's always set correctly. protected final long mediaTimeOffsetMs; /** Creates an event dispatcher. */ public MediaSourceEventDispatcher() { this( - /* listenerAndHandlers= */ new CopyOnWriteMultiset<>(), + /* listenerInfos= */ new CopyOnWriteMultiset<>(), /* windowIndex= */ 0, /* mediaPeriodId= */ null, /* mediaTimeOffsetMs= */ 0); } protected MediaSourceEventDispatcher( - CopyOnWriteMultiset listenerAndHandlers, + CopyOnWriteMultiset listenerInfos, int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { - this.listenerAndHandlers = listenerAndHandlers; + this.listenerInfos = listenerInfos; this.windowIndex = windowIndex; this.mediaPeriodId = mediaPeriodId; this.mediaTimeOffsetMs = mediaTimeOffsetMs; @@ -87,30 +87,45 @@ public class MediaSourceEventDispatcher { public MediaSourceEventDispatcher withParameters( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { return new MediaSourceEventDispatcher( - listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs); + listenerInfos, windowIndex, mediaPeriodId, mediaTimeOffsetMs); } /** * Adds a listener to the event dispatcher. * + *

Calls to {@link #dispatch(EventWithPeriodId, Class)} will propagate to {@code eventListener} + * if the {@code listenerClass} types are equal. + * + *

The same listener instance can be added multiple times with different {@code listenerClass} + * values (i.e. if the instance implements multiple listener interfaces). + * + *

Duplicate {@code {eventListener, listenerClass}} pairs are also permitted. In this case an + * event dispatched to {@code listenerClass} will only be passed to the {@code eventListener} + * once. + * * @param handler A handler on the which listener events will be posted. * @param eventListener The listener to be added. + * @param listenerClass The type used to register the listener. Can be a superclass of {@code + * eventListener}. */ - public void addEventListener(Handler handler, Object eventListener) { + public void addEventListener(Handler handler, T eventListener, Class listenerClass) { Assertions.checkNotNull(handler); Assertions.checkNotNull(eventListener); - listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener)); + listenerInfos.add(new ListenerInfo(handler, eventListener, listenerClass)); } /** * Removes a listener from the event dispatcher. * * @param eventListener The listener to be removed. + * @param listenerClass The listener type passed to {@link #addEventListener(Handler, Object, + * Class)}. */ - public void removeEventListener(Object eventListener) { - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { - if (listenerAndHandler.listener == eventListener) { - listenerAndHandlers.remove(listenerAndHandler); + public void removeEventListener(T eventListener, Class listenerClass) { + for (ListenerInfo listenerInfo : listenerInfos) { + if (listenerInfo.listener == eventListener + && listenerInfo.listenerClass.equals(listenerClass)) { + listenerInfos.remove(listenerInfo); } } } @@ -118,11 +133,11 @@ public class MediaSourceEventDispatcher { /** Dispatches {@code event} to all registered listeners of type {@code listenerClass}. */ @SuppressWarnings("unchecked") // The cast is gated with listenerClass.isInstance() public void dispatch(EventWithPeriodId event, Class listenerClass) { - for (ListenerAndHandler listenerAndHandler : listenerAndHandlers.elementSet()) { - if (listenerClass.isInstance(listenerAndHandler.listener)) { + for (ListenerInfo listenerInfo : listenerInfos.elementSet()) { + if (listenerInfo.listenerClass.equals(listenerClass)) { postOrRun( - listenerAndHandler.handler, - () -> event.sendTo((T) listenerAndHandler.listener, windowIndex, mediaPeriodId)); + listenerInfo.handler, + () -> event.sendTo((T) listenerInfo.listener, windowIndex, mediaPeriodId)); } } } @@ -140,15 +155,17 @@ public class MediaSourceEventDispatcher { return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; } - /** Container class for a {@link Handler} and {@code listener} object. */ - protected static final class ListenerAndHandler { + /** Container class for a {@link Handler}, {@code listener} and {@code listenerClass}. */ + protected static final class ListenerInfo { public final Handler handler; public final Object listener; + public final Class listenerClass; - public ListenerAndHandler(Handler handler, Object listener) { + public ListenerInfo(Handler handler, Object listener, Class listenerClass) { this.handler = handler; this.listener = listener; + this.listenerClass = listenerClass; } @Override @@ -156,19 +173,21 @@ public class MediaSourceEventDispatcher { if (this == o) { return true; } - if (!(o instanceof ListenerAndHandler)) { + if (!(o instanceof ListenerInfo)) { return false; } - // We deliberately only consider listener (and not handler) in equals() and hashcode() - // because the handler used to process the callbacks is an implementation detail. - ListenerAndHandler that = (ListenerAndHandler) o; - return listener.equals(that.listener); + ListenerInfo that = (ListenerInfo) o; + + // We deliberately only consider listener and listenerClass (and not handler) in equals() and + // hashcode() because the handler used to process the callbacks is an implementation detail. + return listener.equals(that.listener) && listenerClass.equals(that.listenerClass); } @Override public int hashCode() { - return listener.hashCode(); + int result = 31 * listener.hashCode(); + return result + 31 * listenerClass.hashCode(); } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcherTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcherTest.java new file mode 100644 index 0000000000..7fa55d3128 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/MediaSourceEventDispatcherTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2020 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.util; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.os.Handler; +import android.os.Looper; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Tests for {@link MediaSourceEventDispatcher}. */ +@RunWith(AndroidJUnit4.class) +public class MediaSourceEventDispatcherTest { + + private static final MediaSource.MediaPeriodId MEDIA_PERIOD_ID = + new MediaSource.MediaPeriodId("test uid"); + private static final int WINDOW_INDEX = 200; + private static final int MEDIA_TIME_OFFSET_MS = 1_000; + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private MediaSourceEventListener mediaSourceEventListener; + @Mock private MediaAndDrmEventListener mediaAndDrmEventListener; + + private MediaSourceEventDispatcher eventDispatcher; + + @Before + public void setupEventDispatcher() { + eventDispatcher = new MediaSourceEventDispatcher(); + eventDispatcher = + eventDispatcher.withParameters(WINDOW_INDEX, MEDIA_PERIOD_ID, MEDIA_TIME_OFFSET_MS); + } + + @Test + public void listenerReceivesEventPopulatedWithMediaPeriodInfo() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaSourceEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + } + + @Test + public void sameListenerObjectRegisteredTwiceOnlyReceivesEventsOnce() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaSourceEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + } + + @Test + public void sameListenerInstanceCanBeRegisteredWithTwoTypes() { + eventDispatcher.addEventListener( + new Handler(Looper.getMainLooper()), + mediaAndDrmEventListener, + MediaSourceEventListener.class); + eventDispatcher.addEventListener( + new Handler(Looper.getMainLooper()), + mediaAndDrmEventListener, + DrmSessionEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded(), + DrmSessionEventListener.class); + + verify(mediaAndDrmEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + verify(mediaAndDrmEventListener).onDrmKeysLoaded(); + } + + // If a listener is added that implements multiple types, it should only receive events for the + // type specified at registration time. + @Test + public void listenerOnlyReceivesEventsForRegisteredType() { + eventDispatcher.addEventListener( + new Handler(Looper.getMainLooper()), + mediaAndDrmEventListener, + MediaSourceEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + eventDispatcher.dispatch( + (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded(), + DrmSessionEventListener.class); + + verify(mediaAndDrmEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + verify(mediaAndDrmEventListener, never()).onDrmKeysLoaded(); + } + + @Test + public void listenersAreCopiedToNewDispatcher() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + + MediaSource.MediaPeriodId newPeriodId = new MediaSource.MediaPeriodId("different uid"); + MediaSourceEventDispatcher newEventDispatcher = + this.eventDispatcher.withParameters( + /* windowIndex= */ 250, newPeriodId, /* mediaTimeOffsetMs= */ 500); + + newEventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaSourceEventListener).onMediaPeriodCreated(250, newPeriodId); + } + + @Test + public void removingListenerStopsEventDispatch() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class); + eventDispatcher.removeEventListener(mediaSourceEventListener, MediaSourceEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaSourceEventListener, never()).onMediaPeriodCreated(anyInt(), any()); + } + + @Test + public void removingListenerWithDifferentTypeToRegistrationDoesntRemove() { + eventDispatcher.addEventListener( + Util.createHandler(), mediaAndDrmEventListener, MediaSourceEventListener.class); + eventDispatcher.removeEventListener(mediaAndDrmEventListener, DrmSessionEventListener.class); + + eventDispatcher.dispatch( + MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class); + + verify(mediaAndDrmEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID); + } + + private interface MediaAndDrmEventListener + extends MediaSourceEventListener, DrmSessionEventListener {} +}