diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index dcc28f974e..1636275109 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -721,6 +721,7 @@ import java.util.concurrent.TimeoutException; new TimeoutException("Player release timed out."), ExoPlaybackException.TIMEOUT_OPERATION_RELEASE))); } + listeners.release(); playbackInfoUpdateHandler.removeCallbacksAndMessages(null); if (analyticsCollector != null) { bandwidthMeter.removeEventListener(analyticsCollector); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java index 12b73ec94f..499e05acf0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java @@ -48,6 +48,8 @@ public final class ListenerSet { private final ArrayDeque flushingEvents; private final ArrayDeque queuedEvents; + private boolean released; + /** Creates the listener set. */ public ListenerSet() { listeners = new CopyOnWriteArraySet<>(); @@ -63,6 +65,9 @@ public final class ListenerSet { * @param listener The listener to be added. */ public void add(T listener) { + if (released) { + return; + } Assertions.checkNotNull(listener); listeners.add(new ListenerHolder(listener)); } @@ -124,6 +129,19 @@ public final class ListenerSet { flushEvents(); } + /** + * Releases the set of listeners. + * + *

This will ensure no events are sent to any listener after this method has been called. + */ + public void release() { + for (ListenerHolder listenerHolder : listeners) { + listenerHolder.release(); + } + listeners.clear(); + released = true; + } + private static final class ListenerHolder { @Nonnull public final T listener; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java index 36f20c065e..aaa299ecd7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java @@ -184,6 +184,44 @@ public class ListenerSetTest { verify(listener2, times(2)).callback1(); } + @Test + public void release_stopsForwardingEventsImmediately() { + ListenerSet listenerSet = new ListenerSet<>(); + TestListener listener2 = mock(TestListener.class); + // Listener1 releases the set from within the callback. + TestListener listener1 = + spy( + new TestListener() { + @Override + public void callback1() { + listenerSet.release(); + } + }); + listenerSet.add(listener1); + listenerSet.add(listener2); + + // Listener2 shouldn't even get this event as it's released before the event can be invoked. + listenerSet.sendEvent(TestListener::callback1); + listenerSet.sendEvent(TestListener::callback2); + + verify(listener1).callback1(); + verify(listener2, never()).callback1(); + verify(listener1, never()).callback2(); + verify(listener2, never()).callback2(); + } + + @Test + public void release_preventsRegisteringNewListeners() { + ListenerSet listenerSet = new ListenerSet<>(); + TestListener listener = mock(TestListener.class); + + listenerSet.release(); + listenerSet.add(listener); + listenerSet.sendEvent(TestListener::callback1); + + verify(listener, never()).callback1(); + } + private interface TestListener { default void callback1() {}