• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.testutil;
17 
18 import android.os.Looper;
19 import android.view.Surface;
20 import androidx.annotation.Nullable;
21 import com.google.android.exoplayer2.C;
22 import com.google.android.exoplayer2.ExoPlaybackException;
23 import com.google.android.exoplayer2.Player;
24 import com.google.android.exoplayer2.PlayerMessage;
25 import com.google.android.exoplayer2.PlayerMessage.Target;
26 import com.google.android.exoplayer2.SimpleExoPlayer;
27 import com.google.android.exoplayer2.Timeline;
28 import com.google.android.exoplayer2.audio.AudioAttributes;
29 import com.google.android.exoplayer2.source.MediaSource;
30 import com.google.android.exoplayer2.source.ShuffleOrder;
31 import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface;
32 import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable;
33 import com.google.android.exoplayer2.testutil.Action.PlayUntilPosition;
34 import com.google.android.exoplayer2.testutil.Action.Seek;
35 import com.google.android.exoplayer2.testutil.Action.SendMessages;
36 import com.google.android.exoplayer2.testutil.Action.SetAudioAttributes;
37 import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady;
38 import com.google.android.exoplayer2.testutil.Action.SetPlaybackSpeed;
39 import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled;
40 import com.google.android.exoplayer2.testutil.Action.SetRepeatMode;
41 import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled;
42 import com.google.android.exoplayer2.testutil.Action.SetShuffleOrder;
43 import com.google.android.exoplayer2.testutil.Action.SetVideoSurface;
44 import com.google.android.exoplayer2.testutil.Action.Stop;
45 import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException;
46 import com.google.android.exoplayer2.testutil.Action.WaitForIsLoading;
47 import com.google.android.exoplayer2.testutil.Action.WaitForMessage;
48 import com.google.android.exoplayer2.testutil.Action.WaitForPendingPlayerCommands;
49 import com.google.android.exoplayer2.testutil.Action.WaitForPlayWhenReady;
50 import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState;
51 import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity;
52 import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged;
53 import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
54 import com.google.android.exoplayer2.util.Assertions;
55 import com.google.android.exoplayer2.util.HandlerWrapper;
56 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
57 
58 /**
59  * Schedules a sequence of {@link Action}s for execution during a test.
60  */
61 public final class ActionSchedule {
62 
63   /**
64    * Callback to notify listener that the action schedule has finished.
65    */
66   public interface Callback {
67 
68     /**
69      * Called when action schedule finished executing all its actions.
70      */
onActionScheduleFinished()71     void onActionScheduleFinished();
72 
73   }
74 
75   private final ActionNode rootNode;
76   private final CallbackAction callbackAction;
77 
78   /**
79    * @param rootNode The first node in the sequence.
80    * @param callbackAction The final action which can be used to trigger a callback.
81    */
ActionSchedule(ActionNode rootNode, CallbackAction callbackAction)82   private ActionSchedule(ActionNode rootNode, CallbackAction callbackAction) {
83     this.rootNode = rootNode;
84     this.callbackAction = callbackAction;
85   }
86 
87   /**
88    * Starts execution of the schedule.
89    *
90    * @param player The player to which actions should be applied.
91    * @param trackSelector The track selector to which actions should be applied.
92    * @param surface The surface to use when applying actions, or {@code null} if no surface is
93    *     needed.
94    * @param mainHandler A handler associated with the main thread of the host activity.
95    * @param callback A {@link Callback} to notify when the action schedule finishes, or null if no
96    *     notification is needed.
97    */
start( SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface, HandlerWrapper mainHandler, @Nullable Callback callback)98   /* package */ void start(
99       SimpleExoPlayer player,
100       DefaultTrackSelector trackSelector,
101       @Nullable Surface surface,
102       HandlerWrapper mainHandler,
103       @Nullable Callback callback) {
104     callbackAction.setCallback(callback);
105     rootNode.schedule(player, trackSelector, surface, mainHandler);
106   }
107 
108   /**
109    * A builder for {@link ActionSchedule} instances.
110    */
111   public static final class Builder {
112 
113     private final String tag;
114     private final ActionNode rootNode;
115 
116     private long currentDelayMs;
117     private ActionNode previousNode;
118 
119     /**
120      * @param tag A tag to use for logging.
121      */
Builder(String tag)122     public Builder(String tag) {
123       this.tag = tag;
124       rootNode = new ActionNode(new RootAction(tag), 0);
125       previousNode = rootNode;
126     }
127 
128     /**
129      * Schedules a delay between executing any previous actions and any subsequent ones.
130      *
131      * @param delayMs The delay in milliseconds.
132      * @return The builder, for convenience.
133      */
delay(long delayMs)134     public Builder delay(long delayMs) {
135       currentDelayMs += delayMs;
136       return this;
137     }
138 
139     /**
140      * Schedules an action.
141      *
142      * @param action The action to schedule.
143      * @return The builder, for convenience.
144      */
apply(Action action)145     public Builder apply(Action action) {
146       return appendActionNode(new ActionNode(action, currentDelayMs));
147     }
148 
149     /**
150      * Schedules an action repeatedly.
151      *
152      * @param action The action to schedule.
153      * @param intervalMs The interval between each repetition in milliseconds.
154      * @return The builder, for convenience.
155      */
repeat(Action action, long intervalMs)156     public Builder repeat(Action action, long intervalMs) {
157       return appendActionNode(new ActionNode(action, currentDelayMs, intervalMs));
158     }
159 
160     /**
161      * Schedules a seek action.
162      *
163      * @param positionMs The seek position.
164      * @return The builder, for convenience.
165      */
seek(long positionMs)166     public Builder seek(long positionMs) {
167       return apply(new Seek(tag, positionMs));
168     }
169 
170     /**
171      * Schedules a seek action.
172      *
173      * @param windowIndex The window to seek to.
174      * @param positionMs The seek position.
175      * @return The builder, for convenience.
176      */
seek(int windowIndex, long positionMs)177     public Builder seek(int windowIndex, long positionMs) {
178       return apply(new Seek(tag, windowIndex, positionMs, /* catchIllegalSeekException= */ false));
179     }
180 
181     /**
182      * Schedules a seek action to be executed.
183      *
184      * @param windowIndex The window to seek to.
185      * @param positionMs The seek position.
186      * @param catchIllegalSeekException Whether an illegal seek position should be caught or not.
187      * @return The builder, for convenience.
188      */
seek(int windowIndex, long positionMs, boolean catchIllegalSeekException)189     public Builder seek(int windowIndex, long positionMs, boolean catchIllegalSeekException) {
190       return apply(new Seek(tag, windowIndex, positionMs, catchIllegalSeekException));
191     }
192 
193     /**
194      * Schedules a seek action and waits until playback resumes after the seek.
195      *
196      * @param positionMs The seek position.
197      * @return The builder, for convenience.
198      */
seekAndWait(long positionMs)199     public Builder seekAndWait(long positionMs) {
200       return apply(new Seek(tag, positionMs))
201           .apply(new WaitForPlaybackState(tag, Player.STATE_READY));
202     }
203 
204     /**
205      * Schedules a delay until all pending player commands have been handled.
206      *
207      * <p>A command is considered as having been handled if it arrived on the playback thread and
208      * the player acknowledged that it received the command back to the app thread.
209      *
210      * @return The builder, for convenience.
211      */
waitForPendingPlayerCommands()212     public Builder waitForPendingPlayerCommands() {
213       return apply(new WaitForPendingPlayerCommands(tag));
214     }
215 
216     /**
217      * Schedules a playback speed setting action.
218      *
219      * @param playbackSpeed The playback speed to set.
220      * @return The builder, for convenience.
221      * @see Player#setPlaybackSpeed(float)
222      */
setPlaybackSpeed(float playbackSpeed)223     public Builder setPlaybackSpeed(float playbackSpeed) {
224       return apply(new SetPlaybackSpeed(tag, playbackSpeed));
225     }
226 
227     /**
228      * Schedules a stop action.
229      *
230      * @return The builder, for convenience.
231      */
stop()232     public Builder stop() {
233       return apply(new Stop(tag));
234     }
235 
236     /**
237      * Schedules a stop action.
238      *
239      * @param reset Whether the player should be reset.
240      * @return The builder, for convenience.
241      */
stop(boolean reset)242     public Builder stop(boolean reset) {
243       return apply(new Stop(tag, reset));
244     }
245 
246     /**
247      * Schedules a play action.
248      *
249      * @return The builder, for convenience.
250      */
play()251     public Builder play() {
252       return apply(new SetPlayWhenReady(tag, true));
253     }
254 
255     /**
256      * Schedules a play action, waits until the player reaches the specified position, and pauses
257      * the player again.
258      *
259      * @param windowIndex The window index at which the player should be paused again.
260      * @param positionMs The position in that window at which the player should be paused again.
261      * @return The builder, for convenience.
262      */
playUntilPosition(int windowIndex, long positionMs)263     public Builder playUntilPosition(int windowIndex, long positionMs) {
264       return apply(new PlayUntilPosition(tag, windowIndex, positionMs));
265     }
266 
267     /**
268      * Schedules a play action, waits until the player reaches the start of the specified window,
269      * and pauses the player again.
270      *
271      * @param windowIndex The window index at which the player should be paused again.
272      * @return The builder, for convenience.
273      */
playUntilStartOfWindow(int windowIndex)274     public Builder playUntilStartOfWindow(int windowIndex) {
275       return apply(new PlayUntilPosition(tag, windowIndex, /* positionMs= */ 0));
276     }
277 
278     /**
279      * Schedules a pause action.
280      *
281      * @return The builder, for convenience.
282      */
pause()283     public Builder pause() {
284       return apply(new SetPlayWhenReady(tag, false));
285     }
286 
287     /**
288      * Schedules a renderer enable action.
289      *
290      * @return The builder, for convenience.
291      */
enableRenderer(int index)292     public Builder enableRenderer(int index) {
293       return apply(new SetRendererDisabled(tag, index, false));
294     }
295 
296     /**
297      * Schedules a renderer disable action.
298      *
299      * @return The builder, for convenience.
300      */
disableRenderer(int index)301     public Builder disableRenderer(int index) {
302       return apply(new SetRendererDisabled(tag, index, true));
303     }
304 
305     /**
306      * Schedules a clear video surface action.
307      *
308      * @return The builder, for convenience.
309      */
clearVideoSurface()310     public Builder clearVideoSurface() {
311       return apply(new ClearVideoSurface(tag));
312     }
313 
314     /**
315      * Schedules a set video surface action.
316      *
317      * @return The builder, for convenience.
318      */
setVideoSurface()319     public Builder setVideoSurface() {
320       return apply(new SetVideoSurface(tag));
321     }
322 
323     /**
324      * Schedules application of audio attributes.
325      *
326      * @return The builder, for convenience.
327      */
setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus)328     public Builder setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) {
329       return apply(new SetAudioAttributes(tag, audioAttributes, handleAudioFocus));
330     }
331 
332     /**
333      * Schedules a set media items action to be executed.
334      *
335      * @param windowIndex The window index to start playback from or {@link C#INDEX_UNSET} if the
336      *     playback position should not be reset.
337      * @param positionMs The position in milliseconds from where playback should start. If {@link
338      *     C#TIME_UNSET} is passed the default position is used. In any case, if {@code windowIndex}
339      *     is set to {@link C#INDEX_UNSET} the position is not reset at all and this parameter is
340      *     ignored.
341      * @return The builder, for convenience.
342      */
setMediaSources(int windowIndex, long positionMs, MediaSource... sources)343     public Builder setMediaSources(int windowIndex, long positionMs, MediaSource... sources) {
344       return apply(new Action.SetMediaItems(tag, windowIndex, positionMs, sources));
345     }
346 
347     /**
348      * Schedules a set media items action to be executed.
349      *
350      * @param resetPosition Whether the playback position should be reset.
351      * @return The builder, for convenience.
352      */
setMediaSources(boolean resetPosition, MediaSource... sources)353     public Builder setMediaSources(boolean resetPosition, MediaSource... sources) {
354       return apply(new Action.SetMediaItemsResetPosition(tag, resetPosition, sources));
355     }
356 
357     /**
358      * Schedules a set media items action to be executed.
359      *
360      * @param mediaSources The media sources to add.
361      * @return The builder, for convenience.
362      */
setMediaSources(MediaSource... mediaSources)363     public Builder setMediaSources(MediaSource... mediaSources) {
364       return apply(
365           new Action.SetMediaItems(
366               tag, /* windowIndex= */ C.INDEX_UNSET, /* positionMs= */ C.TIME_UNSET, mediaSources));
367     }
368     /**
369      * Schedules a add media items action to be executed.
370      *
371      * @param mediaSources The media sources to add.
372      * @return The builder, for convenience.
373      */
addMediaSources(MediaSource... mediaSources)374     public Builder addMediaSources(MediaSource... mediaSources) {
375       return apply(new Action.AddMediaItems(tag, mediaSources));
376     }
377 
378     /**
379      * Schedules a move media item action to be executed.
380      *
381      * @param currentIndex The current index of the item to move.
382      * @param newIndex The index after the item has been moved.
383      * @return The builder, for convenience.
384      */
moveMediaItem(int currentIndex, int newIndex)385     public Builder moveMediaItem(int currentIndex, int newIndex) {
386       return apply(new Action.MoveMediaItem(tag, currentIndex, newIndex));
387     }
388 
389     /**
390      * Schedules a remove media item action to be executed.
391      *
392      * @param index The index of the media item to be removed.
393      * @return The builder, for convenience.
394      */
removeMediaItem(int index)395     public Builder removeMediaItem(int index) {
396       return apply(new Action.RemoveMediaItem(tag, index));
397     }
398 
399     /**
400      * Schedules a remove media items action to be executed.
401      *
402      * @param fromIndex The start of the range of media items to be removed.
403      * @param toIndex The end of the range of media items to be removed (exclusive).
404      * @return The builder, for convenience.
405      */
removeMediaItems(int fromIndex, int toIndex)406     public Builder removeMediaItems(int fromIndex, int toIndex) {
407       return apply(new Action.RemoveMediaItems(tag, fromIndex, toIndex));
408     }
409 
410     /**
411      * Schedules a prepare action to be executed.
412      *
413      * @return The builder, for convenience.
414      */
prepare()415     public Builder prepare() {
416       return apply(new Action.Prepare(tag));
417     }
418 
419     /**
420      * Schedules a clear media items action to be created.
421      *
422      * @return The builder. for convenience,
423      */
clearMediaItems()424     public Builder clearMediaItems() {
425       return apply(new Action.ClearMediaItems(tag));
426     }
427 
428     /**
429      * Schedules a repeat mode setting action.
430      *
431      * @return The builder, for convenience.
432      */
setRepeatMode(@layer.RepeatMode int repeatMode)433     public Builder setRepeatMode(@Player.RepeatMode int repeatMode) {
434       return apply(new SetRepeatMode(tag, repeatMode));
435     }
436 
437     /**
438      * Schedules a set shuffle order action to be executed.
439      *
440      * @param shuffleOrder The shuffle order.
441      * @return The builder, for convenience.
442      */
setShuffleOrder(ShuffleOrder shuffleOrder)443     public Builder setShuffleOrder(ShuffleOrder shuffleOrder) {
444       return apply(new SetShuffleOrder(tag, shuffleOrder));
445     }
446 
447     /**
448      * Schedules a shuffle setting action to be executed.
449      *
450      * @return The builder, for convenience.
451      */
setShuffleModeEnabled(boolean shuffleModeEnabled)452     public Builder setShuffleModeEnabled(boolean shuffleModeEnabled) {
453       return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled));
454     }
455 
456     /**
457      * Schedules sending a {@link PlayerMessage}.
458      *
459      * @param positionMs The position in the current window at which the message should be sent, in
460      *     milliseconds.
461      * @return The builder, for convenience.
462      */
sendMessage(Target target, long positionMs)463     public Builder sendMessage(Target target, long positionMs) {
464       return apply(new SendMessages(tag, target, positionMs));
465     }
466 
467     /**
468      * Schedules sending a {@link PlayerMessage}.
469      *
470      * @param target A message target.
471      * @param windowIndex The window index at which the message should be sent.
472      * @param positionMs The position at which the message should be sent, in milliseconds.
473      * @return The builder, for convenience.
474      */
sendMessage(Target target, int windowIndex, long positionMs)475     public Builder sendMessage(Target target, int windowIndex, long positionMs) {
476       return apply(
477           new SendMessages(tag, target, windowIndex, positionMs, /* deleteAfterDelivery= */ true));
478     }
479 
480     /**
481      * Schedules to send a {@link PlayerMessage}.
482      *
483      * @param target A message target.
484      * @param windowIndex The window index at which the message should be sent.
485      * @param positionMs The position at which the message should be sent, in milliseconds.
486      * @param deleteAfterDelivery Whether the message will be deleted after delivery.
487      * @return The builder, for convenience.
488      */
sendMessage( Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery)489     public Builder sendMessage(
490         Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) {
491       return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery));
492     }
493 
494     /**
495      * Schedules a delay until any timeline change.
496      *
497      * @return The builder, for convenience.
498      */
waitForTimelineChanged()499     public Builder waitForTimelineChanged() {
500       return apply(new WaitForTimelineChanged(tag));
501     }
502 
503     /**
504      * Schedules a delay until the timeline changed to a specified expected timeline.
505      *
506      * @param expectedTimeline The expected timeline.
507      * @param expectedReason The expected reason of the timeline change.
508      * @return The builder, for convenience.
509      */
waitForTimelineChanged( Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason)510     public Builder waitForTimelineChanged(
511         Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) {
512       return apply(new WaitForTimelineChanged(tag, expectedTimeline, expectedReason));
513     }
514 
515     /**
516      * Schedules a delay until the next position discontinuity.
517      *
518      * @return The builder, for convenience.
519      */
waitForPositionDiscontinuity()520     public Builder waitForPositionDiscontinuity() {
521       return apply(new WaitForPositionDiscontinuity(tag));
522     }
523 
524     /**
525      * Schedules a delay until playWhenReady has the specified value.
526      *
527      * @param targetPlayWhenReady The target playWhenReady value.
528      * @return The builder, for convenience.
529      */
waitForPlayWhenReady(boolean targetPlayWhenReady)530     public Builder waitForPlayWhenReady(boolean targetPlayWhenReady) {
531       return apply(new WaitForPlayWhenReady(tag, targetPlayWhenReady));
532     }
533 
534     /**
535      * Schedules a delay until the playback state changed to the specified state.
536      *
537      * @param targetPlaybackState The target playback state.
538      * @return The builder, for convenience.
539      */
waitForPlaybackState(int targetPlaybackState)540     public Builder waitForPlaybackState(int targetPlaybackState) {
541       return apply(new WaitForPlaybackState(tag, targetPlaybackState));
542     }
543 
544     /**
545      * Schedules a delay until {@code player.isLoading()} changes to the specified value.
546      *
547      * @param targetIsLoading The target value of {@code player.isLoading()}.
548      * @return The builder, for convenience.
549      */
waitForIsLoading(boolean targetIsLoading)550     public Builder waitForIsLoading(boolean targetIsLoading) {
551       return apply(new WaitForIsLoading(tag, targetIsLoading));
552     }
553 
554     /**
555      * Schedules a delay until a message arrives at the {@link PlayerMessage.Target}.
556      *
557      * @param playerTarget The target to observe.
558      * @return The builder, for convenience.
559      */
waitForMessage(PlayerTarget playerTarget)560     public Builder waitForMessage(PlayerTarget playerTarget) {
561       return apply(new WaitForMessage(tag, playerTarget));
562     }
563 
564     /**
565      * Schedules a {@link Runnable}.
566      *
567      * @return The builder, for convenience.
568      */
executeRunnable(Runnable runnable)569     public Builder executeRunnable(Runnable runnable) {
570       return apply(new ExecuteRunnable(tag, runnable));
571     }
572 
573     /**
574      * Schedules to throw a playback exception on the playback thread.
575      *
576      * @param exception The exception to throw.
577      * @return The builder, for convenience.
578      */
throwPlaybackException(ExoPlaybackException exception)579     public Builder throwPlaybackException(ExoPlaybackException exception) {
580       return apply(new ThrowPlaybackException(tag, exception));
581     }
582 
583     /** Builds the schedule. */
build()584     public ActionSchedule build() {
585       CallbackAction callbackAction = new CallbackAction(tag);
586       apply(callbackAction);
587       return new ActionSchedule(rootNode, callbackAction);
588     }
589 
appendActionNode(ActionNode actionNode)590     private Builder appendActionNode(ActionNode actionNode) {
591       previousNode.setNext(actionNode);
592       previousNode = actionNode;
593       currentDelayMs = 0;
594       return this;
595     }
596   }
597 
598   /**
599    * Provides a wrapper for a {@link Target} which has access to the player when handling messages.
600    * Can be used with {@link Builder#sendMessage(Target, long)}.
601    *
602    * <p>The target can be passed to {@link ActionSchedule.Builder#waitForMessage(PlayerTarget)} to
603    * wait for a message to arrive at the target.
604    */
605   public abstract static class PlayerTarget implements Target {
606 
607     /** Callback to be called when message arrives. */
608     public interface Callback {
609       /** Notifies about the arrival of the message. */
onMessageArrived()610       void onMessageArrived();
611     }
612 
613     @Nullable private SimpleExoPlayer player;
614     private boolean hasArrived;
615     @Nullable private Callback callback;
616 
setCallback(Callback callback)617     public void setCallback(Callback callback) {
618       this.callback = callback;
619       if (hasArrived) {
620         callback.onMessageArrived();
621       }
622     }
623 
624     /** Handles the message send to the component and additionally provides access to the player. */
handleMessage( SimpleExoPlayer player, int messageType, @Nullable Object message)625     public abstract void handleMessage(
626         SimpleExoPlayer player, int messageType, @Nullable Object message);
627 
628     /** Sets the player to be passed to {@link #handleMessage(SimpleExoPlayer, int, Object)}. */
setPlayer(SimpleExoPlayer player)629     /* package */ void setPlayer(SimpleExoPlayer player) {
630       this.player = player;
631     }
632 
633     @Override
handleMessage(int messageType, @Nullable Object message)634     public final void handleMessage(int messageType, @Nullable Object message) {
635       handleMessage(Assertions.checkStateNotNull(player), messageType, message);
636       if (callback != null) {
637         hasArrived = true;
638         callback.onMessageArrived();
639       }
640     }
641   }
642 
643   /**
644    * Provides a wrapper for a {@link Runnable} which has access to the player. Can be used with
645    * {@link Builder#executeRunnable(Runnable)}.
646    */
647   public abstract static class PlayerRunnable implements Runnable {
648 
649     @Nullable private SimpleExoPlayer player;
650 
651     /** Executes Runnable with reference to player. */
run(SimpleExoPlayer player)652     public abstract void run(SimpleExoPlayer player);
653 
654     /** Sets the player to be passed to {@link #run(SimpleExoPlayer)} . */
setPlayer(SimpleExoPlayer player)655     /* package */ void setPlayer(SimpleExoPlayer player) {
656       this.player = player;
657     }
658 
659     @Override
run()660     public final void run() {
661       run(Assertions.checkStateNotNull(player));
662     }
663   }
664 
665   /** Wraps an {@link Action}, allowing a delay and a next {@link Action} to be specified. */
666   /* package */ static final class ActionNode implements Runnable {
667 
668     private final Action action;
669     private final long delayMs;
670     private final long repeatIntervalMs;
671 
672     @Nullable private ActionNode next;
673 
674     private @MonotonicNonNull SimpleExoPlayer player;
675     private @MonotonicNonNull DefaultTrackSelector trackSelector;
676     @Nullable private Surface surface;
677     private @MonotonicNonNull HandlerWrapper mainHandler;
678 
679     /**
680      * @param action The wrapped action.
681      * @param delayMs The delay between the node being scheduled and the action being executed.
682      */
ActionNode(Action action, long delayMs)683     public ActionNode(Action action, long delayMs) {
684       this(action, delayMs, C.TIME_UNSET);
685     }
686 
687     /**
688      * @param action The wrapped action.
689      * @param delayMs The delay between the node being scheduled and the action being executed.
690      * @param repeatIntervalMs The interval between one execution and the next repetition. If set to
691      *     {@link C#TIME_UNSET}, the action is executed once only.
692      */
ActionNode(Action action, long delayMs, long repeatIntervalMs)693     public ActionNode(Action action, long delayMs, long repeatIntervalMs) {
694       this.action = action;
695       this.delayMs = delayMs;
696       this.repeatIntervalMs = repeatIntervalMs;
697     }
698 
699     /**
700      * Sets the next action.
701      *
702      * @param next The next {@link Action}.
703      */
setNext(ActionNode next)704     public void setNext(ActionNode next) {
705       this.next = next;
706     }
707 
708     /**
709      * Schedules {@link #action} after {@link #delayMs}. The {@link #next} node will be scheduled
710      * immediately after {@link #action} is executed.
711      *
712      * @param player The player to which actions should be applied.
713      * @param trackSelector The track selector to which actions should be applied.
714      * @param surface The surface to use when applying actions, or {@code null}.
715      * @param mainHandler A handler associated with the main thread of the host activity.
716      */
schedule( SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface, HandlerWrapper mainHandler)717     public void schedule(
718         SimpleExoPlayer player,
719         DefaultTrackSelector trackSelector,
720         @Nullable Surface surface,
721         HandlerWrapper mainHandler) {
722       this.player = player;
723       this.trackSelector = trackSelector;
724       this.surface = surface;
725       this.mainHandler = mainHandler;
726       if (delayMs == 0 && Looper.myLooper() == mainHandler.getLooper()) {
727         run();
728       } else {
729         mainHandler.postDelayed(this, delayMs);
730       }
731     }
732 
733     @Override
run()734     public void run() {
735       action.doActionAndScheduleNext(
736           Assertions.checkStateNotNull(player),
737           Assertions.checkStateNotNull(trackSelector),
738           surface,
739           Assertions.checkStateNotNull(mainHandler),
740           next);
741       if (repeatIntervalMs != C.TIME_UNSET) {
742         mainHandler.postDelayed(
743             new Runnable() {
744               @Override
745               public void run() {
746                 action.doActionAndScheduleNext(
747                     player, trackSelector, surface, mainHandler, /* nextAction= */ null);
748                 mainHandler.postDelayed(/* runnable= */ this, repeatIntervalMs);
749               }
750             },
751             repeatIntervalMs);
752       }
753     }
754 
755   }
756 
757   /**
758    * A no-op root action.
759    */
760   private static final class RootAction extends Action {
761 
RootAction(String tag)762     public RootAction(String tag) {
763       super(tag, "Root");
764     }
765 
766     @Override
doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface)767     protected void doActionImpl(
768         SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface) {
769       // Do nothing.
770     }
771   }
772 
773   /**
774    * An action calling a specified {@link ActionSchedule.Callback}.
775    */
776   private static final class CallbackAction extends Action {
777 
778     @Nullable private Callback callback;
779 
CallbackAction(String tag)780     public CallbackAction(String tag) {
781       super(tag, "FinishedCallback");
782     }
783 
setCallback(@ullable Callback callback)784     public void setCallback(@Nullable Callback callback) {
785       this.callback = callback;
786     }
787 
788     @Override
doActionAndScheduleNextImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface, HandlerWrapper handler, @Nullable ActionNode nextAction)789     protected void doActionAndScheduleNextImpl(
790         SimpleExoPlayer player,
791         DefaultTrackSelector trackSelector,
792         @Nullable Surface surface,
793         HandlerWrapper handler,
794         @Nullable ActionNode nextAction) {
795       Assertions.checkArgument(nextAction == null);
796       @Nullable Callback callback = this.callback;
797       if (callback != null) {
798         handler.post(callback::onActionScheduleFinished);
799       }
800     }
801 
802     @Override
doActionImpl( SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface)803     protected void doActionImpl(
804         SimpleExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface) {
805       // Not triggered.
806     }
807   }
808 
809 }
810