Commit 68cbf6dd by tonihei Committed by Oliver Woodman

Move listener handling to common util class.

ExoPlayerImpl and CastPlayer repeat the same logic. Moving the listener
and event handling to a common util class allows to reuse the same code
and add unit tests for this logic.

The change is a functional no-op.

PiperOrigin-RevId: 337812358
parent 9398f4db
...@@ -249,58 +249,4 @@ public abstract class BasePlayer implements Player { ...@@ -249,58 +249,4 @@ public abstract class BasePlayer implements Player {
@RepeatMode int repeatMode = getRepeatMode(); @RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode; return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;
} }
/** Holds a listener reference. */
protected static final class ListenerHolder {
/**
* The listener on which {link #invoke} will execute {@link ListenerInvocation listener
* invocations}.
*/
public final Player.EventListener listener;
private boolean released;
public ListenerHolder(Player.EventListener listener) {
this.listener = listener;
}
/** Prevents any further {@link ListenerInvocation} to be executed on {@link #listener}. */
public void release() {
released = true;
}
/**
* Executes the given {@link ListenerInvocation} on {@link #listener}. Does nothing if {@link
* #release} has been called on this instance.
*/
public void invoke(ListenerInvocation listenerInvocation) {
if (!released) {
listenerInvocation.invokeListener(listener);
}
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
return listener.equals(((ListenerHolder) other).listener);
}
@Override
public int hashCode() {
return listener.hashCode();
}
}
/** Parameterized invocation of a {@link Player.EventListener} method. */
protected interface ListenerInvocation {
/** Executes the invocation on the given {@link Player.EventListener}. */
void invokeListener(Player.EventListener listener);
}
} }
/*
* 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 androidx.annotation.Nullable;
import java.util.ArrayDeque;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nonnull;
/**
* A set of listeners.
*
* <p>Events are guaranteed to arrive in the order in which they happened even if a new event is
* triggered recursively from another listener.
*
* <p>Events are also guaranteed to be only sent to the listeners registered at the time the event
* was enqueued and haven't been removed since.
*
* @param <T> The listener type.
*/
public final class ListenerSet<T> {
/**
* An event sent to a listener.
*
* @param <T> The listener type.
*/
public interface Event<T> {
/** Invokes the event notification on the given listener. */
void invoke(T listener);
}
private final CopyOnWriteArraySet<ListenerHolder<T>> listeners;
private final ArrayDeque<Runnable> flushingEvents;
private final ArrayDeque<Runnable> queuedEvents;
/** Creates the listener set. */
public ListenerSet() {
listeners = new CopyOnWriteArraySet<>();
flushingEvents = new ArrayDeque<>();
queuedEvents = new ArrayDeque<>();
}
/**
* Adds a listener to the set.
*
* <p>If a listener is already present, it will not be added again.
*
* @param listener The listener to be added.
*/
public void add(T listener) {
Assertions.checkNotNull(listener);
listeners.add(new ListenerHolder<T>(listener));
}
/**
* Removes a listener from the set.
*
* <p>If the listener is not present, nothing happens.
*
* @param listener The listener to be removed.
*/
public void remove(T listener) {
for (ListenerHolder<T> listenerHolder : listeners) {
if (listenerHolder.listener.equals(listener)) {
listenerHolder.release();
listeners.remove(listenerHolder);
}
}
}
/**
* Adds an event that is sent to the listeners when {@link #flushEvents} is called.
*
* @param event The event.
*/
public void queueEvent(Event<T> event) {
CopyOnWriteArraySet<ListenerHolder<T>> listenerSnapshot = new CopyOnWriteArraySet<>(listeners);
queuedEvents.add(
() -> {
for (ListenerHolder<T> holder : listenerSnapshot) {
holder.invoke(event);
}
});
}
/** Notifies listeners of events previously enqueued with {@link #queueEvent(Event)}. */
public void flushEvents() {
boolean recursiveFlushInProgress = !flushingEvents.isEmpty();
flushingEvents.addAll(queuedEvents);
queuedEvents.clear();
if (recursiveFlushInProgress) {
// Recursive call to flush. Let the outer call handle the flush queue.
return;
}
while (!flushingEvents.isEmpty()) {
flushingEvents.peekFirst().run();
flushingEvents.removeFirst();
}
}
/**
* {@link #queueEvent(Event) Queues} a single event and immediately {@link #flushEvents() flushes}
* the event queue to notify all listeners.
*
* @param event The event.
*/
public void sendEvent(Event<T> event) {
queueEvent(event);
flushEvents();
}
private static final class ListenerHolder<T> {
@Nonnull public final T listener;
private boolean released;
public ListenerHolder(@Nonnull T listener) {
this.listener = listener;
}
public void release() {
released = true;
}
public void invoke(Event<T> event) {
if (!released) {
event.invoke(listener);
}
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
return listener.equals(((ListenerHolder<?>) other).listener);
}
@Override
public int hashCode() {
return listener.hashCode();
}
}
}
/*
* 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.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mockito;
/** Unit test for {@link ListenerSet}. */
@RunWith(AndroidJUnit4.class)
public class ListenerSetTest {
@Test
public void queueEvent_isNotSentWithoutFlush() {
ListenerSet<TestListener> listenerSet = new ListenerSet<>();
TestListener listener = mock(TestListener.class);
listenerSet.add(listener);
listenerSet.queueEvent(TestListener::callback1);
listenerSet.queueEvent(TestListener::callback2);
verifyNoMoreInteractions(listener);
}
@Test
public void flushEvents_sendsPreviouslyQueuedEventsToAllListeners() {
ListenerSet<TestListener> listenerSet = new ListenerSet<>();
TestListener listener1 = mock(TestListener.class);
TestListener listener2 = mock(TestListener.class);
listenerSet.add(listener1);
listenerSet.add(listener2);
listenerSet.queueEvent(TestListener::callback1);
listenerSet.queueEvent(TestListener::callback2);
listenerSet.queueEvent(TestListener::callback1);
listenerSet.flushEvents();
InOrder inOrder = Mockito.inOrder(listener1, listener2);
inOrder.verify(listener1).callback1();
inOrder.verify(listener2).callback1();
inOrder.verify(listener1).callback2();
inOrder.verify(listener2).callback2();
inOrder.verify(listener1).callback1();
inOrder.verify(listener2).callback1();
inOrder.verifyNoMoreInteractions();
}
@Test
public void flushEvents_recursive_sendsEventsInCorrectOrder() {
ListenerSet<TestListener> listenerSet = new ListenerSet<>();
// Listener1 sends callback3 recursively when receiving callback1.
TestListener listener1 =
spy(
new TestListener() {
@Override
public void callback1() {
listenerSet.queueEvent(TestListener::callback3);
listenerSet.flushEvents();
}
});
TestListener listener2 = mock(TestListener.class);
listenerSet.add(listener1);
listenerSet.add(listener2);
listenerSet.queueEvent(TestListener::callback1);
listenerSet.queueEvent(TestListener::callback2);
listenerSet.flushEvents();
InOrder inOrder = Mockito.inOrder(listener1, listener2);
inOrder.verify(listener1).callback1();
inOrder.verify(listener2).callback1();
inOrder.verify(listener1).callback2();
inOrder.verify(listener2).callback2();
inOrder.verify(listener1).callback3();
inOrder.verify(listener2).callback3();
inOrder.verifyNoMoreInteractions();
}
@Test
public void add_withRecursion_onlyReceivesUpdatesForFutureEvents() {
ListenerSet<TestListener> listenerSet = new ListenerSet<>();
TestListener listener2 = mock(TestListener.class);
// Listener1 adds listener2 recursively.
TestListener listener1 =
spy(
new TestListener() {
@Override
public void callback1() {
listenerSet.add(listener2);
}
});
listenerSet.sendEvent(TestListener::callback1);
listenerSet.add(listener1);
// This should add listener2, but the event should not be received yet as it happened before
// listener2 was added.
listenerSet.sendEvent(TestListener::callback1);
listenerSet.sendEvent(TestListener::callback1);
verify(listener1, times(2)).callback1();
verify(listener2).callback1();
}
@Test
public void add_withQueueing_onlyReceivesUpdatesForFutureEvents() {
ListenerSet<TestListener> listenerSet = new ListenerSet<>();
TestListener listener1 = mock(TestListener.class);
TestListener listener2 = mock(TestListener.class);
// This event is flushed after listener2 was added, but shouldn't be sent to listener2 because
// the event itself occurred before the listener was added.
listenerSet.add(listener1);
listenerSet.queueEvent(TestListener::callback2);
listenerSet.add(listener2);
listenerSet.queueEvent(TestListener::callback2);
listenerSet.flushEvents();
verify(listener1, times(2)).callback2();
verify(listener2).callback2();
}
@Test
public void remove_withRecursion_stopsReceivingEventsImmediately() {
ListenerSet<TestListener> listenerSet = new ListenerSet<>();
TestListener listener2 = mock(TestListener.class);
// Listener1 removes listener2 recursively.
TestListener listener1 =
spy(
new TestListener() {
@Override
public void callback1() {
listenerSet.remove(listener2);
}
});
listenerSet.add(listener1);
listenerSet.add(listener2);
// Listener2 shouldn't even get this event as it's removed before the event can be invoked.
listenerSet.sendEvent(TestListener::callback1);
listenerSet.remove(listener1);
listenerSet.sendEvent(TestListener::callback1);
verify(listener1).callback1();
verify(listener2, never()).callback1();
}
@Test
public void remove_withQueueing_stopsReceivingEventsImmediately() {
ListenerSet<TestListener> listenerSet = new ListenerSet<>();
TestListener listener1 = mock(TestListener.class);
TestListener listener2 = mock(TestListener.class);
listenerSet.add(listener1);
listenerSet.add(listener2);
// Listener1 shouldn't even get this event as it's removed before the event can be invoked.
listenerSet.queueEvent(TestListener::callback1);
listenerSet.remove(listener1);
listenerSet.queueEvent(TestListener::callback1);
listenerSet.flushEvents();
verify(listener1, never()).callback1();
verify(listener2, times(2)).callback1();
}
private interface TestListener {
default void callback1() {}
default void callback2() {}
default void callback3() {}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment