mirror of
https://github.com/androidx/media.git
synced 2025-05-06 23:20:42 +08:00
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
This commit is contained in:
parent
9398f4db03
commit
68cbf6ddf3
@ -35,6 +35,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.ListenerSet;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
@ -50,12 +51,8 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
|
||||
import com.google.android.gms.common.api.PendingResult;
|
||||
import com.google.android.gms.common.api.ResultCallback;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
@ -99,9 +96,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
private final SeekResultCallback seekResultCallback;
|
||||
|
||||
// Listeners and notification.
|
||||
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
||||
private final ArrayList<ListenerNotificationTask> notificationsBatch;
|
||||
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
|
||||
private final ListenerSet<Player.EventListener> listeners;
|
||||
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
|
||||
|
||||
// Internal state.
|
||||
@ -140,9 +135,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
period = new Timeline.Period();
|
||||
statusListener = new StatusListener();
|
||||
seekResultCallback = new SeekResultCallback();
|
||||
listeners = new CopyOnWriteArrayList<>();
|
||||
notificationsBatch = new ArrayList<>();
|
||||
ongoingNotificationsTasks = new ArrayDeque<>();
|
||||
listeners = new ListenerSet<>();
|
||||
|
||||
playWhenReady = new StateHolder<>(false);
|
||||
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
|
||||
@ -295,18 +288,12 @@ public final class CastPlayer extends BasePlayer {
|
||||
|
||||
@Override
|
||||
public void addListener(EventListener listener) {
|
||||
Assertions.checkNotNull(listener);
|
||||
listeners.addIfAbsent(new ListenerHolder(listener));
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(EventListener listener) {
|
||||
for (ListenerHolder listenerHolder : listeners) {
|
||||
if (listenerHolder.listener.equals(listener)) {
|
||||
listenerHolder.release();
|
||||
listeners.remove(listenerHolder);
|
||||
}
|
||||
}
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -418,7 +405,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
// the local state will be updated to reflect the state reported by the Cast SDK.
|
||||
setPlayerStateAndNotifyIfChanged(
|
||||
playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, playbackState);
|
||||
flushNotifications();
|
||||
listeners.flushEvents();
|
||||
PendingResult<MediaChannelResult> pendingResult =
|
||||
playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
|
||||
this.playWhenReady.pendingResultCallback =
|
||||
@ -427,7 +414,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
public void onResult(MediaChannelResult mediaChannelResult) {
|
||||
if (remoteMediaClient != null) {
|
||||
updatePlayerStateAndNotifyIfChanged(this);
|
||||
flushNotifications();
|
||||
listeners.flushEvents();
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -458,13 +445,11 @@ public final class CastPlayer extends BasePlayer {
|
||||
pendingSeekCount++;
|
||||
pendingSeekWindowIndex = windowIndex;
|
||||
pendingSeekPositionMs = positionMs;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
|
||||
listeners.queueEvent(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
|
||||
} else if (pendingSeekCount == 0) {
|
||||
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||
listeners.queueEvent(EventListener::onSeekProcessed);
|
||||
}
|
||||
flushNotifications();
|
||||
listeners.flushEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -528,7 +513,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
// operation to be perceived as synchronous by the user. When the operation reports a result,
|
||||
// the local state will be updated to reflect the state reported by the Cast SDK.
|
||||
setRepeatModeAndNotifyIfChanged(repeatMode);
|
||||
flushNotifications();
|
||||
listeners.flushEvents();
|
||||
PendingResult<MediaChannelResult> pendingResult =
|
||||
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), /* jsonObject= */ null);
|
||||
this.repeatMode.pendingResultCallback =
|
||||
@ -537,7 +522,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
public void onResult(MediaChannelResult mediaChannelResult) {
|
||||
if (remoteMediaClient != null) {
|
||||
updateRepeatModeAndNotifyIfChanged(this);
|
||||
flushNotifications();
|
||||
listeners.flushEvents();
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -662,8 +647,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
|
||||
if (wasPlaying != isPlaying) {
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(listener -> listener.onIsPlayingChanged(isPlaying)));
|
||||
listeners.queueEvent(listener -> listener.onIsPlayingChanged(isPlaying));
|
||||
}
|
||||
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
updateTimelineAndNotifyIfChanged();
|
||||
@ -679,17 +663,14 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
|
||||
this.currentWindowIndex = currentWindowIndex;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener ->
|
||||
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
|
||||
listeners.queueEvent(
|
||||
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION));
|
||||
}
|
||||
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
|
||||
listeners.queueEvent(
|
||||
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection));
|
||||
}
|
||||
flushNotifications();
|
||||
listeners.flushEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -728,11 +709,10 @@ public final class CastPlayer extends BasePlayer {
|
||||
if (updateTimeline()) {
|
||||
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
|
||||
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener ->
|
||||
listener.onTimelineChanged(
|
||||
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
|
||||
listeners.queueEvent(
|
||||
listener ->
|
||||
listener.onTimelineChanged(
|
||||
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
|
||||
}
|
||||
}
|
||||
|
||||
@ -851,8 +831,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
|
||||
if (this.repeatMode.value != repeatMode) {
|
||||
this.repeatMode.value = repeatMode;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode)));
|
||||
listeners.queueEvent(listener -> listener.onRepeatModeChanged(repeatMode));
|
||||
}
|
||||
}
|
||||
|
||||
@ -866,17 +845,16 @@ public final class CastPlayer extends BasePlayer {
|
||||
if (playWhenReadyChanged || playbackStateChanged) {
|
||||
this.playbackState = playbackState;
|
||||
this.playWhenReady.value = playWhenReady;
|
||||
notificationsBatch.add(
|
||||
new ListenerNotificationTask(
|
||||
listener -> {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||
if (playbackStateChanged) {
|
||||
listener.onPlaybackStateChanged(playbackState);
|
||||
}
|
||||
if (playWhenReadyChanged) {
|
||||
listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason);
|
||||
}
|
||||
}));
|
||||
listeners.queueEvent(
|
||||
listener -> {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||
if (playbackStateChanged) {
|
||||
listener.onPlaybackStateChanged(playbackState);
|
||||
}
|
||||
if (playWhenReadyChanged) {
|
||||
listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -984,20 +962,6 @@ public final class CastPlayer extends BasePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
private void flushNotifications() {
|
||||
boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
|
||||
ongoingNotificationsTasks.addAll(notificationsBatch);
|
||||
notificationsBatch.clear();
|
||||
if (recursiveNotification) {
|
||||
// This will be handled once the current notification task is finished.
|
||||
return;
|
||||
}
|
||||
while (!ongoingNotificationsTasks.isEmpty()) {
|
||||
ongoingNotificationsTasks.peekFirst().execute();
|
||||
ongoingNotificationsTasks.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
private MediaQueueItem[] toMediaQueueItems(List<MediaItem> mediaItems) {
|
||||
MediaQueueItem[] mediaQueueItems = new MediaQueueItem[mediaItems.size()];
|
||||
for (int i = 0; i < mediaItems.size(); i++) {
|
||||
@ -1108,8 +1072,7 @@ public final class CastPlayer extends BasePlayer {
|
||||
if (--pendingSeekCount == 0) {
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||
flushNotifications();
|
||||
listeners.sendEvent(EventListener::onSeekProcessed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1149,21 +1112,4 @@ public final class CastPlayer extends BasePlayer {
|
||||
return pendingResultCallback == resultCallback;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ListenerNotificationTask {
|
||||
|
||||
private final Iterator<ListenerHolder> listenersSnapshot;
|
||||
private final ListenerInvocation listenerInvocation;
|
||||
|
||||
private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
|
||||
this.listenersSnapshot = listeners.iterator();
|
||||
this.listenerInvocation = listenerInvocation;
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
while (listenersSnapshot.hasNext()) {
|
||||
listenersSnapshot.next().invoke(listenerInvocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -249,58 +249,4 @@ public abstract class BasePlayer implements Player {
|
||||
@RepeatMode int repeatMode = getRepeatMode();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -42,14 +42,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Clock;
|
||||
import com.google.android.exoplayer2.util.ListenerSet;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
@ -74,9 +73,8 @@ import java.util.concurrent.TimeoutException;
|
||||
private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener;
|
||||
private final ExoPlayerImplInternal internalPlayer;
|
||||
private final Handler internalPlayerHandler;
|
||||
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
||||
private final ListenerSet<Player.EventListener> listeners;
|
||||
private final Timeline.Period period;
|
||||
private final ArrayDeque<Runnable> pendingListenerNotifications;
|
||||
private final List<MediaSourceHolderSnapshot> mediaSourceHolderSnapshots;
|
||||
private final boolean useLazyPreparation;
|
||||
private final MediaSourceFactory mediaSourceFactory;
|
||||
@ -152,7 +150,7 @@ import java.util.concurrent.TimeoutException;
|
||||
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
|
||||
this.applicationLooper = applicationLooper;
|
||||
repeatMode = Player.REPEAT_MODE_OFF;
|
||||
listeners = new CopyOnWriteArrayList<>();
|
||||
listeners = new ListenerSet<>();
|
||||
mediaSourceHolderSnapshots = new ArrayList<>();
|
||||
shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
|
||||
emptyTrackSelectorResult =
|
||||
@ -167,7 +165,6 @@ import java.util.concurrent.TimeoutException;
|
||||
playbackInfoUpdate ->
|
||||
playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate));
|
||||
playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult);
|
||||
pendingListenerNotifications = new ArrayDeque<>();
|
||||
if (analyticsCollector != null) {
|
||||
analyticsCollector.setPlayer(this);
|
||||
addListener(analyticsCollector);
|
||||
@ -255,18 +252,12 @@ import java.util.concurrent.TimeoutException;
|
||||
|
||||
@Override
|
||||
public void addListener(Player.EventListener listener) {
|
||||
Assertions.checkNotNull(listener);
|
||||
listeners.addIfAbsent(new ListenerHolder(listener));
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(Player.EventListener listener) {
|
||||
for (ListenerHolder listenerHolder : listeners) {
|
||||
if (listenerHolder.listener.equals(listener)) {
|
||||
listenerHolder.release();
|
||||
listeners.remove(listenerHolder);
|
||||
}
|
||||
}
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -560,7 +551,7 @@ import java.util.concurrent.TimeoutException;
|
||||
if (this.repeatMode != repeatMode) {
|
||||
this.repeatMode = repeatMode;
|
||||
internalPlayer.setRepeatMode(repeatMode);
|
||||
notifyListeners(listener -> listener.onRepeatModeChanged(repeatMode));
|
||||
listeners.sendEvent(listener -> listener.onRepeatModeChanged(repeatMode));
|
||||
}
|
||||
}
|
||||
|
||||
@ -574,7 +565,7 @@ import java.util.concurrent.TimeoutException;
|
||||
if (this.shuffleModeEnabled != shuffleModeEnabled) {
|
||||
this.shuffleModeEnabled = shuffleModeEnabled;
|
||||
internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);
|
||||
notifyListeners(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
|
||||
listeners.sendEvent(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
@ -723,7 +714,7 @@ import java.util.concurrent.TimeoutException;
|
||||
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
|
||||
+ ExoPlayerLibraryInfo.registeredModules() + "]");
|
||||
if (!internalPlayer.release()) {
|
||||
notifyListeners(
|
||||
listeners.sendEvent(
|
||||
listener ->
|
||||
listener.onPlayerError(
|
||||
ExoPlaybackException.createForTimeout(
|
||||
@ -942,6 +933,8 @@ import java.util.concurrent.TimeoutException;
|
||||
}
|
||||
}
|
||||
|
||||
// Calling deprecated listeners.
|
||||
@SuppressWarnings("deprecation")
|
||||
private void updatePlaybackInfo(
|
||||
PlaybackInfo playbackInfo,
|
||||
boolean positionDiscontinuity,
|
||||
@ -949,39 +942,106 @@ import java.util.concurrent.TimeoutException;
|
||||
@TimelineChangeReason int timelineChangeReason,
|
||||
@PlayWhenReadyChangeReason int playWhenReadyChangeReason,
|
||||
boolean seekProcessed) {
|
||||
// Assign playback info immediately such that all getters return the right values.
|
||||
// Assign playback info immediately such that all getters return the right values, but keep
|
||||
// snapshot of previous and new state so that listener invocations are triggered correctly.
|
||||
PlaybackInfo previousPlaybackInfo = this.playbackInfo;
|
||||
PlaybackInfo newPlaybackInfo = playbackInfo;
|
||||
this.playbackInfo = playbackInfo;
|
||||
|
||||
Pair<Boolean, Integer> mediaItemTransitionInfo =
|
||||
evaluateMediaItemTransitionReason(
|
||||
playbackInfo,
|
||||
newPlaybackInfo,
|
||||
previousPlaybackInfo,
|
||||
positionDiscontinuity,
|
||||
positionDiscontinuityReason,
|
||||
!previousPlaybackInfo.timeline.equals(playbackInfo.timeline));
|
||||
!previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline));
|
||||
boolean mediaItemTransitioned = mediaItemTransitionInfo.first;
|
||||
int mediaItemTransitionReason = mediaItemTransitionInfo.second;
|
||||
@Nullable MediaItem newMediaItem = null;
|
||||
if (mediaItemTransitioned && !playbackInfo.timeline.isEmpty()) {
|
||||
int windowIndex =
|
||||
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period).windowIndex;
|
||||
newMediaItem = playbackInfo.timeline.getWindow(windowIndex, window).mediaItem;
|
||||
if (!previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline)) {
|
||||
listeners.queueEvent(
|
||||
listener -> listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason));
|
||||
}
|
||||
notifyListeners(
|
||||
new PlaybackInfoUpdate(
|
||||
playbackInfo,
|
||||
previousPlaybackInfo,
|
||||
listeners,
|
||||
trackSelector,
|
||||
positionDiscontinuity,
|
||||
positionDiscontinuityReason,
|
||||
timelineChangeReason,
|
||||
mediaItemTransitioned,
|
||||
mediaItemTransitionReason,
|
||||
newMediaItem,
|
||||
playWhenReadyChangeReason,
|
||||
seekProcessed));
|
||||
if (positionDiscontinuity) {
|
||||
listeners.queueEvent(
|
||||
listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));
|
||||
}
|
||||
if (mediaItemTransitioned) {
|
||||
@Nullable final MediaItem mediaItem;
|
||||
if (!newPlaybackInfo.timeline.isEmpty()) {
|
||||
int windowIndex =
|
||||
newPlaybackInfo.timeline.getPeriodByUid(newPlaybackInfo.periodId.periodUid, period)
|
||||
.windowIndex;
|
||||
mediaItem = newPlaybackInfo.timeline.getWindow(windowIndex, window).mediaItem;
|
||||
} else {
|
||||
mediaItem = null;
|
||||
}
|
||||
listeners.queueEvent(
|
||||
listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason));
|
||||
}
|
||||
if (previousPlaybackInfo.playbackError != newPlaybackInfo.playbackError
|
||||
&& newPlaybackInfo.playbackError != null) {
|
||||
listeners.queueEvent(listener -> listener.onPlayerError(newPlaybackInfo.playbackError));
|
||||
}
|
||||
if (previousPlaybackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult) {
|
||||
trackSelector.onSelectionActivated(newPlaybackInfo.trackSelectorResult.info);
|
||||
listeners.queueEvent(
|
||||
listener ->
|
||||
listener.onTracksChanged(
|
||||
newPlaybackInfo.trackGroups, newPlaybackInfo.trackSelectorResult.selections));
|
||||
}
|
||||
if (!previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
|
||||
listeners.queueEvent(
|
||||
listener -> listener.onStaticMetadataChanged(newPlaybackInfo.staticMetadata));
|
||||
}
|
||||
if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) {
|
||||
listeners.queueEvent(listener -> listener.onIsLoadingChanged(newPlaybackInfo.isLoading));
|
||||
}
|
||||
if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState
|
||||
|| previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) {
|
||||
listeners.queueEvent(
|
||||
listener ->
|
||||
listener.onPlayerStateChanged(
|
||||
newPlaybackInfo.playWhenReady, newPlaybackInfo.playbackState));
|
||||
}
|
||||
if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState) {
|
||||
listeners.queueEvent(
|
||||
listener -> listener.onPlaybackStateChanged(newPlaybackInfo.playbackState));
|
||||
}
|
||||
if (previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) {
|
||||
listeners.queueEvent(
|
||||
listener ->
|
||||
listener.onPlayWhenReadyChanged(
|
||||
newPlaybackInfo.playWhenReady, playWhenReadyChangeReason));
|
||||
}
|
||||
if (previousPlaybackInfo.playbackSuppressionReason
|
||||
!= newPlaybackInfo.playbackSuppressionReason) {
|
||||
listeners.queueEvent(
|
||||
listener ->
|
||||
listener.onPlaybackSuppressionReasonChanged(
|
||||
newPlaybackInfo.playbackSuppressionReason));
|
||||
}
|
||||
if (isPlaying(previousPlaybackInfo) != isPlaying(newPlaybackInfo)) {
|
||||
listeners.queueEvent(listener -> listener.onIsPlayingChanged(isPlaying(newPlaybackInfo)));
|
||||
}
|
||||
if (!previousPlaybackInfo.playbackParameters.equals(newPlaybackInfo.playbackParameters)) {
|
||||
listeners.queueEvent(
|
||||
listener -> listener.onPlaybackParametersChanged(newPlaybackInfo.playbackParameters));
|
||||
}
|
||||
if (seekProcessed) {
|
||||
listeners.queueEvent(EventListener::onSeekProcessed);
|
||||
}
|
||||
if (previousPlaybackInfo.offloadSchedulingEnabled != newPlaybackInfo.offloadSchedulingEnabled) {
|
||||
listeners.queueEvent(
|
||||
listener ->
|
||||
listener.onExperimentalOffloadSchedulingEnabledChanged(
|
||||
newPlaybackInfo.offloadSchedulingEnabled));
|
||||
}
|
||||
if (previousPlaybackInfo.sleepingForOffload != newPlaybackInfo.sleepingForOffload) {
|
||||
listeners.queueEvent(
|
||||
listener ->
|
||||
listener.onExperimentalSleepingForOffloadChanged(newPlaybackInfo.sleepingForOffload));
|
||||
}
|
||||
listeners.flushEvents();
|
||||
}
|
||||
|
||||
private Pair<Boolean, Integer> evaluateMediaItemTransitionReason(
|
||||
@ -1332,23 +1392,6 @@ import java.util.concurrent.TimeoutException;
|
||||
return timeline.getPeriodPosition(window, period, windowIndex, C.msToUs(windowPositionMs));
|
||||
}
|
||||
|
||||
private void notifyListeners(ListenerInvocation listenerInvocation) {
|
||||
CopyOnWriteArrayList<ListenerHolder> listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
|
||||
notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation));
|
||||
}
|
||||
|
||||
private void notifyListeners(Runnable listenerNotificationRunnable) {
|
||||
boolean isRunningRecursiveListenerNotification = !pendingListenerNotifications.isEmpty();
|
||||
pendingListenerNotifications.addLast(listenerNotificationRunnable);
|
||||
if (isRunningRecursiveListenerNotification) {
|
||||
return;
|
||||
}
|
||||
while (!pendingListenerNotifications.isEmpty()) {
|
||||
pendingListenerNotifications.peekFirst().run();
|
||||
pendingListenerNotifications.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) {
|
||||
long positionMs = C.usToMs(positionUs);
|
||||
playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);
|
||||
@ -1356,183 +1399,10 @@ import java.util.concurrent.TimeoutException;
|
||||
return positionMs;
|
||||
}
|
||||
|
||||
private static final class PlaybackInfoUpdate implements Runnable {
|
||||
|
||||
private final PlaybackInfo playbackInfo;
|
||||
private final CopyOnWriteArrayList<ListenerHolder> listenerSnapshot;
|
||||
private final TrackSelector trackSelector;
|
||||
private final boolean positionDiscontinuity;
|
||||
@DiscontinuityReason private final int positionDiscontinuityReason;
|
||||
@TimelineChangeReason private final int timelineChangeReason;
|
||||
private final boolean mediaItemTransitioned;
|
||||
private final int mediaItemTransitionReason;
|
||||
@Nullable private final MediaItem mediaItem;
|
||||
@PlayWhenReadyChangeReason private final int playWhenReadyChangeReason;
|
||||
private final boolean seekProcessed;
|
||||
private final boolean playbackStateChanged;
|
||||
private final boolean playbackErrorChanged;
|
||||
private final boolean isLoadingChanged;
|
||||
private final boolean timelineChanged;
|
||||
private final boolean trackSelectorResultChanged;
|
||||
private final boolean staticMetadataChanged;
|
||||
private final boolean playWhenReadyChanged;
|
||||
private final boolean playbackSuppressionReasonChanged;
|
||||
private final boolean isPlayingChanged;
|
||||
private final boolean playbackParametersChanged;
|
||||
private final boolean offloadSchedulingEnabledChanged;
|
||||
private final boolean sleepingForOffloadChanged;
|
||||
|
||||
public PlaybackInfoUpdate(
|
||||
PlaybackInfo playbackInfo,
|
||||
PlaybackInfo previousPlaybackInfo,
|
||||
CopyOnWriteArrayList<ListenerHolder> listeners,
|
||||
TrackSelector trackSelector,
|
||||
boolean positionDiscontinuity,
|
||||
@DiscontinuityReason int positionDiscontinuityReason,
|
||||
@TimelineChangeReason int timelineChangeReason,
|
||||
boolean mediaItemTransitioned,
|
||||
@MediaItemTransitionReason int mediaItemTransitionReason,
|
||||
@Nullable MediaItem mediaItem,
|
||||
@PlayWhenReadyChangeReason int playWhenReadyChangeReason,
|
||||
boolean seekProcessed) {
|
||||
this.playbackInfo = playbackInfo;
|
||||
this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
|
||||
this.trackSelector = trackSelector;
|
||||
this.positionDiscontinuity = positionDiscontinuity;
|
||||
this.positionDiscontinuityReason = positionDiscontinuityReason;
|
||||
this.timelineChangeReason = timelineChangeReason;
|
||||
this.mediaItemTransitioned = mediaItemTransitioned;
|
||||
this.mediaItemTransitionReason = mediaItemTransitionReason;
|
||||
this.mediaItem = mediaItem;
|
||||
this.playWhenReadyChangeReason = playWhenReadyChangeReason;
|
||||
this.seekProcessed = seekProcessed;
|
||||
playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState;
|
||||
playbackErrorChanged =
|
||||
previousPlaybackInfo.playbackError != playbackInfo.playbackError
|
||||
&& playbackInfo.playbackError != null;
|
||||
isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
|
||||
timelineChanged = !previousPlaybackInfo.timeline.equals(playbackInfo.timeline);
|
||||
trackSelectorResultChanged =
|
||||
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
|
||||
staticMetadataChanged =
|
||||
!previousPlaybackInfo.staticMetadata.equals(playbackInfo.staticMetadata);
|
||||
playWhenReadyChanged = previousPlaybackInfo.playWhenReady != playbackInfo.playWhenReady;
|
||||
playbackSuppressionReasonChanged =
|
||||
previousPlaybackInfo.playbackSuppressionReason != playbackInfo.playbackSuppressionReason;
|
||||
isPlayingChanged = isPlaying(previousPlaybackInfo) != isPlaying(playbackInfo);
|
||||
playbackParametersChanged =
|
||||
!previousPlaybackInfo.playbackParameters.equals(playbackInfo.playbackParameters);
|
||||
offloadSchedulingEnabledChanged =
|
||||
previousPlaybackInfo.offloadSchedulingEnabled != playbackInfo.offloadSchedulingEnabled;
|
||||
sleepingForOffloadChanged =
|
||||
previousPlaybackInfo.sleepingForOffload != playbackInfo.sleepingForOffload;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void run() {
|
||||
if (timelineChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));
|
||||
}
|
||||
if (positionDiscontinuity) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));
|
||||
}
|
||||
if (mediaItemTransitioned) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason));
|
||||
}
|
||||
if (playbackErrorChanged) {
|
||||
invokeAll(listenerSnapshot, listener -> listener.onPlayerError(playbackInfo.playbackError));
|
||||
}
|
||||
if (trackSelectorResultChanged) {
|
||||
trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener ->
|
||||
listener.onTracksChanged(
|
||||
playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections));
|
||||
}
|
||||
if (staticMetadataChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener -> listener.onStaticMetadataChanged(playbackInfo.staticMetadata));
|
||||
}
|
||||
if (isLoadingChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot, listener -> listener.onIsLoadingChanged(playbackInfo.isLoading));
|
||||
}
|
||||
if (playbackStateChanged || playWhenReadyChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener ->
|
||||
listener.onPlayerStateChanged(
|
||||
playbackInfo.playWhenReady, playbackInfo.playbackState));
|
||||
}
|
||||
if (playbackStateChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener -> listener.onPlaybackStateChanged(playbackInfo.playbackState));
|
||||
}
|
||||
if (playWhenReadyChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener ->
|
||||
listener.onPlayWhenReadyChanged(
|
||||
playbackInfo.playWhenReady, playWhenReadyChangeReason));
|
||||
}
|
||||
if (playbackSuppressionReasonChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener ->
|
||||
listener.onPlaybackSuppressionReasonChanged(
|
||||
playbackInfo.playbackSuppressionReason));
|
||||
}
|
||||
if (isPlayingChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot, listener -> listener.onIsPlayingChanged(isPlaying(playbackInfo)));
|
||||
}
|
||||
if (playbackParametersChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener -> {
|
||||
listener.onPlaybackParametersChanged(playbackInfo.playbackParameters);
|
||||
});
|
||||
}
|
||||
if (seekProcessed) {
|
||||
invokeAll(listenerSnapshot, EventListener::onSeekProcessed);
|
||||
}
|
||||
if (offloadSchedulingEnabledChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener ->
|
||||
listener.onExperimentalOffloadSchedulingEnabledChanged(
|
||||
playbackInfo.offloadSchedulingEnabled));
|
||||
}
|
||||
if (sleepingForOffloadChanged) {
|
||||
invokeAll(
|
||||
listenerSnapshot,
|
||||
listener ->
|
||||
listener.onExperimentalSleepingForOffloadChanged(playbackInfo.sleepingForOffload));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPlaying(PlaybackInfo playbackInfo) {
|
||||
return playbackInfo.playbackState == Player.STATE_READY
|
||||
&& playbackInfo.playWhenReady
|
||||
&& playbackInfo.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private static void invokeAll(
|
||||
CopyOnWriteArrayList<ListenerHolder> listeners, ListenerInvocation listenerInvocation) {
|
||||
for (ListenerHolder listenerHolder : listeners) {
|
||||
listenerHolder.invoke(listenerInvocation);
|
||||
}
|
||||
private static boolean isPlaying(PlaybackInfo playbackInfo) {
|
||||
return playbackInfo.playbackState == Player.STATE_READY
|
||||
&& playbackInfo.playWhenReady
|
||||
&& playbackInfo.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE;
|
||||
}
|
||||
|
||||
private static final class MediaSourceHolderSnapshot implements MediaSourceInfoHolder {
|
||||
|
@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
/*
|
||||
* 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() {}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user