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.ui;
17 
18 import android.annotation.SuppressLint;
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.drawable.Drawable;
23 import android.os.Looper;
24 import android.os.SystemClock;
25 import android.util.AttributeSet;
26 import android.view.KeyEvent;
27 import android.view.LayoutInflater;
28 import android.view.MotionEvent;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.FrameLayout;
32 import android.widget.ImageView;
33 import android.widget.TextView;
34 import androidx.annotation.Nullable;
35 import com.google.android.exoplayer2.C;
36 import com.google.android.exoplayer2.ControlDispatcher;
37 import com.google.android.exoplayer2.DefaultControlDispatcher;
38 import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
39 import com.google.android.exoplayer2.PlaybackPreparer;
40 import com.google.android.exoplayer2.Player;
41 import com.google.android.exoplayer2.Timeline;
42 import com.google.android.exoplayer2.util.Assertions;
43 import com.google.android.exoplayer2.util.RepeatModeUtil;
44 import com.google.android.exoplayer2.util.Util;
45 import java.util.Arrays;
46 import java.util.Formatter;
47 import java.util.Locale;
48 import java.util.concurrent.CopyOnWriteArrayList;
49 
50 /**
51  * A view for controlling {@link Player} instances.
52  *
53  * <p>A PlayerControlView can be customized by setting attributes (or calling corresponding
54  * methods), overriding drawables, overriding the view's layout file, or by specifying a custom view
55  * layout file.
56  *
57  * <h3>Attributes</h3>
58  *
59  * The following attributes can be set on a PlayerControlView when used in a layout XML file:
60  *
61  * <ul>
62  *   <li><b>{@code show_timeout}</b> - The time between the last user interaction and the controls
63  *       being automatically hidden, in milliseconds. Use zero if the controls should not
64  *       automatically timeout.
65  *       <ul>
66  *         <li>Corresponding method: {@link #setShowTimeoutMs(int)}
67  *         <li>Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}
68  *       </ul>
69  *   <li><b>{@code rewind_increment}</b> - The duration of the rewind applied when the user taps the
70  *       rewind button, in milliseconds. Use zero to disable the rewind button.
71  *       <ul>
72  *         <li>Corresponding method: {@link #setControlDispatcher(ControlDispatcher)}
73  *         <li>Default: {@link DefaultControlDispatcher#DEFAULT_REWIND_MS}
74  *       </ul>
75  *   <li><b>{@code fastforward_increment}</b> - Like {@code rewind_increment}, but for fast forward.
76  *       <ul>
77  *         <li>Corresponding method: {@link #setControlDispatcher(ControlDispatcher)}
78  *         <li>Default: {@link DefaultControlDispatcher#DEFAULT_FAST_FORWARD_MS}
79  *       </ul>
80  *   <li><b>{@code repeat_toggle_modes}</b> - A flagged enumeration value specifying which repeat
81  *       mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all},
82  *       or {@code one|all}.
83  *       <ul>
84  *         <li>Corresponding method: {@link #setRepeatToggleModes(int)}
85  *         <li>Default: {@link PlayerControlView#DEFAULT_REPEAT_TOGGLE_MODES}
86  *       </ul>
87  *   <li><b>{@code show_shuffle_button}</b> - Whether the shuffle button is shown.
88  *       <ul>
89  *         <li>Corresponding method: {@link #setShowShuffleButton(boolean)}
90  *         <li>Default: false
91  *       </ul>
92  *   <li><b>{@code time_bar_min_update_interval}</b> - Specifies the minimum interval between time
93  *       bar position updates.
94  *       <ul>
95  *         <li>Corresponding method: {@link #setTimeBarMinUpdateInterval(int)}
96  *         <li>Default: {@link #DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS}
97  *       </ul>
98  *   <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout to be inflated. See
99  *       below for more details.
100  *       <ul>
101  *         <li>Corresponding method: None
102  *         <li>Default: {@code R.layout.exo_player_control_view}
103  *       </ul>
104  *   <li>All attributes that can be set on {@link DefaultTimeBar} can also be set on a
105  *       PlayerControlView, and will be propagated to the inflated {@link DefaultTimeBar} unless the
106  *       layout is overridden to specify a custom {@code exo_progress} (see below).
107  * </ul>
108  *
109  * <h3>Overriding drawables</h3>
110  *
111  * The drawables used by PlayerControlView (with its default layout file) can be overridden by
112  * drawables with the same names defined in your application. The drawables that can be overridden
113  * are:
114  *
115  * <ul>
116  *   <li><b>{@code exo_controls_play}</b> - The play icon.
117  *   <li><b>{@code exo_controls_pause}</b> - The pause icon.
118  *   <li><b>{@code exo_controls_rewind}</b> - The rewind icon.
119  *   <li><b>{@code exo_controls_fastforward}</b> - The fast forward icon.
120  *   <li><b>{@code exo_controls_previous}</b> - The previous icon.
121  *   <li><b>{@code exo_controls_next}</b> - The next icon.
122  *   <li><b>{@code exo_controls_repeat_off}</b> - The repeat icon for {@link
123  *       Player#REPEAT_MODE_OFF}.
124  *   <li><b>{@code exo_controls_repeat_one}</b> - The repeat icon for {@link
125  *       Player#REPEAT_MODE_ONE}.
126  *   <li><b>{@code exo_controls_repeat_all}</b> - The repeat icon for {@link
127  *       Player#REPEAT_MODE_ALL}.
128  *   <li><b>{@code exo_controls_shuffle_off}</b> - The shuffle icon when shuffling is disabled.
129  *   <li><b>{@code exo_controls_shuffle_on}</b> - The shuffle icon when shuffling is enabled.
130  *   <li><b>{@code exo_controls_vr}</b> - The VR icon.
131  * </ul>
132  *
133  * <h3>Overriding the layout file</h3>
134  *
135  * To customize the layout of PlayerControlView throughout your app, or just for certain
136  * configurations, you can define {@code exo_player_control_view.xml} layout files in your
137  * application {@code res/layout*} directories. These layouts will override the one provided by the
138  * ExoPlayer library, and will be inflated for use by PlayerControlView. The view identifies and
139  * binds its children by looking for the following ids:
140  *
141  * <ul>
142  *   <li><b>{@code exo_play}</b> - The play button.
143  *       <ul>
144  *         <li>Type: {@link View}
145  *       </ul>
146  *   <li><b>{@code exo_pause}</b> - The pause button.
147  *       <ul>
148  *         <li>Type: {@link View}
149  *       </ul>
150  *   <li><b>{@code exo_rew}</b> - The rewind button.
151  *       <ul>
152  *         <li>Type: {@link View}
153  *       </ul>
154  *   <li><b>{@code exo_ffwd}</b> - The fast forward button.
155  *       <ul>
156  *         <li>Type: {@link View}
157  *       </ul>
158  *   <li><b>{@code exo_prev}</b> - The previous button.
159  *       <ul>
160  *         <li>Type: {@link View}
161  *       </ul>
162  *   <li><b>{@code exo_next}</b> - The next button.
163  *       <ul>
164  *         <li>Type: {@link View}
165  *       </ul>
166  *   <li><b>{@code exo_repeat_toggle}</b> - The repeat toggle button.
167  *       <ul>
168  *         <li>Type: {@link ImageView}
169  *         <li>Note: PlayerControlView will programmatically set the drawable on the repeat toggle
170  *             button according to the player's current repeat mode. The drawables used are {@code
171  *             exo_controls_repeat_off}, {@code exo_controls_repeat_one} and {@code
172  *             exo_controls_repeat_all}. See the section above for information on overriding these
173  *             drawables.
174  *       </ul>
175  *   <li><b>{@code exo_shuffle}</b> - The shuffle button.
176  *       <ul>
177  *         <li>Type: {@link ImageView}
178  *         <li>Note: PlayerControlView will programmatically set the drawable on the shuffle button
179  *             according to the player's current repeat mode. The drawables used are {@code
180  *             exo_controls_shuffle_off} and {@code exo_controls_shuffle_on}. See the section above
181  *             for information on overriding these drawables.
182  *       </ul>
183  *   <li><b>{@code exo_vr}</b> - The VR mode button.
184  *       <ul>
185  *         <li>Type: {@link View}
186  *       </ul>
187  *   <li><b>{@code exo_position}</b> - Text view displaying the current playback position.
188  *       <ul>
189  *         <li>Type: {@link TextView}
190  *       </ul>
191  *   <li><b>{@code exo_duration}</b> - Text view displaying the current media duration.
192  *       <ul>
193  *         <li>Type: {@link TextView}
194  *       </ul>
195  *   <li><b>{@code exo_progress_placeholder}</b> - A placeholder that's replaced with the inflated
196  *       {@link DefaultTimeBar}. Ignored if an {@code exo_progress} view exists.
197  *       <ul>
198  *         <li>Type: {@link View}
199  *       </ul>
200  *   <li><b>{@code exo_progress}</b> - Time bar that's updated during playback and allows seeking.
201  *       {@link DefaultTimeBar} attributes set on the PlayerControlView will not be automatically
202  *       propagated through to this instance. If a view exists with this id, any {@code
203  *       exo_progress_placeholder} view will be ignored.
204  *       <ul>
205  *         <li>Type: {@link TimeBar}
206  *       </ul>
207  * </ul>
208  *
209  * <p>All child views are optional and so can be omitted if not required, however where defined they
210  * must be of the expected type.
211  *
212  * <h3>Specifying a custom layout file</h3>
213  *
214  * Defining your own {@code exo_player_control_view.xml} is useful to customize the layout of
215  * PlayerControlView throughout your application. It's also possible to customize the layout for a
216  * single instance in a layout file. This is achieved by setting the {@code controller_layout_id}
217  * attribute on a PlayerControlView. This will cause the specified layout to be inflated instead of
218  * {@code exo_player_control_view.xml} for only the instance on which the attribute is set.
219  */
220 public class PlayerControlView extends FrameLayout {
221 
222   static {
223     ExoPlayerLibraryInfo.registerModule("goog.exo.ui");
224   }
225 
226   /** Listener to be notified about changes of the visibility of the UI control. */
227   public interface VisibilityListener {
228 
229     /**
230      * Called when the visibility changes.
231      *
232      * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}.
233      */
onVisibilityChange(int visibility)234     void onVisibilityChange(int visibility);
235   }
236 
237   /** Listener to be notified when progress has been updated. */
238   public interface ProgressUpdateListener {
239 
240     /**
241      * Called when progress needs to be updated.
242      *
243      * @param position The current position.
244      * @param bufferedPosition The current buffered position.
245      */
onProgressUpdate(long position, long bufferedPosition)246     void onProgressUpdate(long position, long bufferedPosition);
247   }
248 
249   /** The default show timeout, in milliseconds. */
250   public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000;
251   /** The default repeat toggle modes. */
252   public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
253       RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE;
254   /** The default minimum interval between time bar position updates. */
255   public static final int DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS = 200;
256   /** The maximum number of windows that can be shown in a multi-window time bar. */
257   public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100;
258 
259   /** The maximum interval between time bar position updates. */
260   private static final int MAX_UPDATE_INTERVAL_MS = 1000;
261 
262   private final ComponentListener componentListener;
263   private final CopyOnWriteArrayList<VisibilityListener> visibilityListeners;
264   @Nullable private final View previousButton;
265   @Nullable private final View nextButton;
266   @Nullable private final View playButton;
267   @Nullable private final View pauseButton;
268   @Nullable private final View fastForwardButton;
269   @Nullable private final View rewindButton;
270   @Nullable private final ImageView repeatToggleButton;
271   @Nullable private final ImageView shuffleButton;
272   @Nullable private final View vrButton;
273   @Nullable private final TextView durationView;
274   @Nullable private final TextView positionView;
275   @Nullable private final TimeBar timeBar;
276   private final StringBuilder formatBuilder;
277   private final Formatter formatter;
278   private final Timeline.Period period;
279   private final Timeline.Window window;
280   private final Runnable updateProgressAction;
281   private final Runnable hideAction;
282 
283   private final Drawable repeatOffButtonDrawable;
284   private final Drawable repeatOneButtonDrawable;
285   private final Drawable repeatAllButtonDrawable;
286   private final String repeatOffButtonContentDescription;
287   private final String repeatOneButtonContentDescription;
288   private final String repeatAllButtonContentDescription;
289   private final Drawable shuffleOnButtonDrawable;
290   private final Drawable shuffleOffButtonDrawable;
291   private final float buttonAlphaEnabled;
292   private final float buttonAlphaDisabled;
293   private final String shuffleOnContentDescription;
294   private final String shuffleOffContentDescription;
295 
296   @Nullable private Player player;
297   private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;
298   @Nullable private ProgressUpdateListener progressUpdateListener;
299   @Nullable private PlaybackPreparer playbackPreparer;
300 
301   private boolean isAttachedToWindow;
302   private boolean showMultiWindowTimeBar;
303   private boolean multiWindowTimeBar;
304   private boolean scrubbing;
305   private int showTimeoutMs;
306   private int timeBarMinUpdateIntervalMs;
307   private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;
308   private boolean showShuffleButton;
309   private long hideAtMs;
310   private long[] adGroupTimesMs;
311   private boolean[] playedAdGroups;
312   private long[] extraAdGroupTimesMs;
313   private boolean[] extraPlayedAdGroups;
314   private long currentWindowOffset;
315 
PlayerControlView(Context context)316   public PlayerControlView(Context context) {
317     this(context, /* attrs= */ null);
318   }
319 
PlayerControlView(Context context, @Nullable AttributeSet attrs)320   public PlayerControlView(Context context, @Nullable AttributeSet attrs) {
321     this(context, attrs, /* defStyleAttr= */ 0);
322   }
323 
PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)324   public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
325     this(context, attrs, defStyleAttr, attrs);
326   }
327 
328   @SuppressWarnings({
329     "nullness:argument.type.incompatible",
330     "nullness:method.invocation.invalid",
331     "nullness:methodref.receiver.bound.invalid"
332   })
PlayerControlView( Context context, @Nullable AttributeSet attrs, int defStyleAttr, @Nullable AttributeSet playbackAttrs)333   public PlayerControlView(
334       Context context,
335       @Nullable AttributeSet attrs,
336       int defStyleAttr,
337       @Nullable AttributeSet playbackAttrs) {
338     super(context, attrs, defStyleAttr);
339     int controllerLayoutId = R.layout.exo_player_control_view;
340     showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
341     repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES;
342     timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS;
343     hideAtMs = C.TIME_UNSET;
344     showShuffleButton = false;
345     int rewindMs = DefaultControlDispatcher.DEFAULT_REWIND_MS;
346     int fastForwardMs = DefaultControlDispatcher.DEFAULT_FAST_FORWARD_MS;
347     if (playbackAttrs != null) {
348       TypedArray a =
349           context
350               .getTheme()
351               .obtainStyledAttributes(playbackAttrs, R.styleable.PlayerControlView, 0, 0);
352       try {
353         rewindMs = a.getInt(R.styleable.PlayerControlView_rewind_increment, rewindMs);
354         fastForwardMs =
355             a.getInt(R.styleable.PlayerControlView_fastforward_increment, fastForwardMs);
356         showTimeoutMs = a.getInt(R.styleable.PlayerControlView_show_timeout, showTimeoutMs);
357         controllerLayoutId =
358             a.getResourceId(R.styleable.PlayerControlView_controller_layout_id, controllerLayoutId);
359         repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes);
360         showShuffleButton =
361             a.getBoolean(R.styleable.PlayerControlView_show_shuffle_button, showShuffleButton);
362         setTimeBarMinUpdateInterval(
363             a.getInt(
364                 R.styleable.PlayerControlView_time_bar_min_update_interval,
365                 timeBarMinUpdateIntervalMs));
366       } finally {
367         a.recycle();
368       }
369     }
370     visibilityListeners = new CopyOnWriteArrayList<>();
371     period = new Timeline.Period();
372     window = new Timeline.Window();
373     formatBuilder = new StringBuilder();
374     formatter = new Formatter(formatBuilder, Locale.getDefault());
375     adGroupTimesMs = new long[0];
376     playedAdGroups = new boolean[0];
377     extraAdGroupTimesMs = new long[0];
378     extraPlayedAdGroups = new boolean[0];
379     componentListener = new ComponentListener();
380     controlDispatcher =
381         new com.google.android.exoplayer2.DefaultControlDispatcher(fastForwardMs, rewindMs);
382     updateProgressAction = this::updateProgress;
383     hideAction = this::hide;
384 
385     LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this);
386     setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
387 
388     TimeBar customTimeBar = findViewById(R.id.exo_progress);
389     View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
390     if (customTimeBar != null) {
391       timeBar = customTimeBar;
392     } else if (timeBarPlaceholder != null) {
393       // Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred,
394       // but standard attributes (e.g. background) are not.
395       DefaultTimeBar defaultTimeBar = new DefaultTimeBar(context, null, 0, playbackAttrs);
396       defaultTimeBar.setId(R.id.exo_progress);
397       defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams());
398       ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent());
399       int timeBarIndex = parent.indexOfChild(timeBarPlaceholder);
400       parent.removeView(timeBarPlaceholder);
401       parent.addView(defaultTimeBar, timeBarIndex);
402       timeBar = defaultTimeBar;
403     } else {
404       timeBar = null;
405     }
406     durationView = findViewById(R.id.exo_duration);
407     positionView = findViewById(R.id.exo_position);
408 
409     if (timeBar != null) {
410       timeBar.addListener(componentListener);
411     }
412     playButton = findViewById(R.id.exo_play);
413     if (playButton != null) {
414       playButton.setOnClickListener(componentListener);
415     }
416     pauseButton = findViewById(R.id.exo_pause);
417     if (pauseButton != null) {
418       pauseButton.setOnClickListener(componentListener);
419     }
420     previousButton = findViewById(R.id.exo_prev);
421     if (previousButton != null) {
422       previousButton.setOnClickListener(componentListener);
423     }
424     nextButton = findViewById(R.id.exo_next);
425     if (nextButton != null) {
426       nextButton.setOnClickListener(componentListener);
427     }
428     rewindButton = findViewById(R.id.exo_rew);
429     if (rewindButton != null) {
430       rewindButton.setOnClickListener(componentListener);
431     }
432     fastForwardButton = findViewById(R.id.exo_ffwd);
433     if (fastForwardButton != null) {
434       fastForwardButton.setOnClickListener(componentListener);
435     }
436     repeatToggleButton = findViewById(R.id.exo_repeat_toggle);
437     if (repeatToggleButton != null) {
438       repeatToggleButton.setOnClickListener(componentListener);
439     }
440     shuffleButton = findViewById(R.id.exo_shuffle);
441     if (shuffleButton != null) {
442       shuffleButton.setOnClickListener(componentListener);
443     }
444     vrButton = findViewById(R.id.exo_vr);
445     setShowVrButton(false);
446 
447     Resources resources = context.getResources();
448 
449     buttonAlphaEnabled =
450         (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100;
451     buttonAlphaDisabled =
452         (float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_disabled) / 100;
453 
454     repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off);
455     repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one);
456     repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all);
457     shuffleOnButtonDrawable = resources.getDrawable(R.drawable.exo_controls_shuffle_on);
458     shuffleOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_shuffle_off);
459     repeatOffButtonContentDescription =
460         resources.getString(R.string.exo_controls_repeat_off_description);
461     repeatOneButtonContentDescription =
462         resources.getString(R.string.exo_controls_repeat_one_description);
463     repeatAllButtonContentDescription =
464         resources.getString(R.string.exo_controls_repeat_all_description);
465     shuffleOnContentDescription = resources.getString(R.string.exo_controls_shuffle_on_description);
466     shuffleOffContentDescription =
467         resources.getString(R.string.exo_controls_shuffle_off_description);
468   }
469 
470   @SuppressWarnings("ResourceType")
getRepeatToggleModes( TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes)471   private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes(
472       TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
473     return a.getInt(R.styleable.PlayerControlView_repeat_toggle_modes, repeatToggleModes);
474   }
475 
476   /**
477    * Returns the {@link Player} currently being controlled by this view, or null if no player is
478    * set.
479    */
480   @Nullable
getPlayer()481   public Player getPlayer() {
482     return player;
483   }
484 
485   /**
486    * Sets the {@link Player} to control.
487    *
488    * @param player The {@link Player} to control, or {@code null} to detach the current player. Only
489    *     players which are accessed on the main thread are supported ({@code
490    *     player.getApplicationLooper() == Looper.getMainLooper()}).
491    */
setPlayer(@ullable Player player)492   public void setPlayer(@Nullable Player player) {
493     Assertions.checkState(Looper.myLooper() == Looper.getMainLooper());
494     Assertions.checkArgument(
495         player == null || player.getApplicationLooper() == Looper.getMainLooper());
496     if (this.player == player) {
497       return;
498     }
499     if (this.player != null) {
500       this.player.removeListener(componentListener);
501     }
502     this.player = player;
503     if (player != null) {
504       player.addListener(componentListener);
505     }
506     updateAll();
507   }
508 
509   /**
510    * Sets whether the time bar should show all windows, as opposed to just the current one. If the
511    * timeline has a period with unknown duration or more than {@link
512    * #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a single
513    * window.
514    *
515    * @param showMultiWindowTimeBar Whether the time bar should show all windows.
516    */
setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar)517   public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
518     this.showMultiWindowTimeBar = showMultiWindowTimeBar;
519     updateTimeline();
520   }
521 
522   /**
523    * Sets the millisecond positions of extra ad markers relative to the start of the window (or
524    * timeline, if in multi-window mode) and whether each extra ad has been played or not. The
525    * markers are shown in addition to any ad markers for ads in the player's timeline.
526    *
527    * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or
528    *     {@code null} to show no extra ad markers.
529    * @param extraPlayedAdGroups Whether each ad has been played. Must be the same length as {@code
530    *     extraAdGroupTimesMs}, or {@code null} if {@code extraAdGroupTimesMs} is {@code null}.
531    */
setExtraAdGroupMarkers( @ullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups)532   public void setExtraAdGroupMarkers(
533       @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) {
534     if (extraAdGroupTimesMs == null) {
535       this.extraAdGroupTimesMs = new long[0];
536       this.extraPlayedAdGroups = new boolean[0];
537     } else {
538       extraPlayedAdGroups = Assertions.checkNotNull(extraPlayedAdGroups);
539       Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length);
540       this.extraAdGroupTimesMs = extraAdGroupTimesMs;
541       this.extraPlayedAdGroups = extraPlayedAdGroups;
542     }
543     updateTimeline();
544   }
545 
546   /**
547    * Adds a {@link VisibilityListener}.
548    *
549    * @param listener The listener to be notified about visibility changes.
550    */
addVisibilityListener(VisibilityListener listener)551   public void addVisibilityListener(VisibilityListener listener) {
552     visibilityListeners.add(listener);
553   }
554 
555   /**
556    * Removes a {@link VisibilityListener}.
557    *
558    * @param listener The listener to be removed.
559    */
removeVisibilityListener(VisibilityListener listener)560   public void removeVisibilityListener(VisibilityListener listener) {
561     visibilityListeners.remove(listener);
562   }
563 
564   /**
565    * Sets the {@link ProgressUpdateListener}.
566    *
567    * @param listener The listener to be notified about when progress is updated.
568    */
setProgressUpdateListener(@ullable ProgressUpdateListener listener)569   public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) {
570     this.progressUpdateListener = listener;
571   }
572 
573   /**
574    * Sets the {@link PlaybackPreparer}.
575    *
576    * @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
577    *     preparer.
578    */
setPlaybackPreparer(@ullable PlaybackPreparer playbackPreparer)579   public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
580     this.playbackPreparer = playbackPreparer;
581   }
582 
583   /**
584    * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}.
585    *
586    * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}.
587    */
setControlDispatcher(ControlDispatcher controlDispatcher)588   public void setControlDispatcher(ControlDispatcher controlDispatcher) {
589     if (this.controlDispatcher != controlDispatcher) {
590       this.controlDispatcher = controlDispatcher;
591       updateNavigation();
592     }
593   }
594 
595   /**
596    * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
597    *     DefaultControlDispatcher#DefaultControlDispatcher(long, long)}.
598    */
599   @SuppressWarnings("deprecation")
600   @Deprecated
setRewindIncrementMs(int rewindMs)601   public void setRewindIncrementMs(int rewindMs) {
602     if (controlDispatcher instanceof DefaultControlDispatcher) {
603       ((DefaultControlDispatcher) controlDispatcher).setRewindIncrementMs(rewindMs);
604       updateNavigation();
605     }
606   }
607 
608   /**
609    * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
610    *     DefaultControlDispatcher#DefaultControlDispatcher(long, long)}.
611    */
612   @SuppressWarnings("deprecation")
613   @Deprecated
setFastForwardIncrementMs(int fastForwardMs)614   public void setFastForwardIncrementMs(int fastForwardMs) {
615     if (controlDispatcher instanceof DefaultControlDispatcher) {
616       ((DefaultControlDispatcher) controlDispatcher).setFastForwardIncrementMs(fastForwardMs);
617       updateNavigation();
618     }
619   }
620 
621   /**
622    * Returns the playback controls timeout. The playback controls are automatically hidden after
623    * this duration of time has elapsed without user input.
624    *
625    * @return The duration in milliseconds. A non-positive value indicates that the controls will
626    *     remain visible indefinitely.
627    */
getShowTimeoutMs()628   public int getShowTimeoutMs() {
629     return showTimeoutMs;
630   }
631 
632   /**
633    * Sets the playback controls timeout. The playback controls are automatically hidden after this
634    * duration of time has elapsed without user input.
635    *
636    * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls
637    *     to remain visible indefinitely.
638    */
setShowTimeoutMs(int showTimeoutMs)639   public void setShowTimeoutMs(int showTimeoutMs) {
640     this.showTimeoutMs = showTimeoutMs;
641     if (isVisible()) {
642       // Reset the timeout.
643       hideAfterTimeout();
644     }
645   }
646 
647   /**
648    * Returns which repeat toggle modes are enabled.
649    *
650    * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}.
651    */
getRepeatToggleModes()652   public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() {
653     return repeatToggleModes;
654   }
655 
656   /**
657    * Sets which repeat toggle modes are enabled.
658    *
659    * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
660    */
setRepeatToggleModes(@epeatModeUtil.RepeatToggleModes int repeatToggleModes)661   public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
662     this.repeatToggleModes = repeatToggleModes;
663     if (player != null) {
664       @Player.RepeatMode int currentMode = player.getRepeatMode();
665       if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
666           && currentMode != Player.REPEAT_MODE_OFF) {
667         controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF);
668       } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE
669           && currentMode == Player.REPEAT_MODE_ALL) {
670         controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE);
671       } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL
672           && currentMode == Player.REPEAT_MODE_ONE) {
673         controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL);
674       }
675     }
676     updateRepeatModeButton();
677   }
678 
679   /** Returns whether the shuffle button is shown. */
getShowShuffleButton()680   public boolean getShowShuffleButton() {
681     return showShuffleButton;
682   }
683 
684   /**
685    * Sets whether the shuffle button is shown.
686    *
687    * @param showShuffleButton Whether the shuffle button is shown.
688    */
setShowShuffleButton(boolean showShuffleButton)689   public void setShowShuffleButton(boolean showShuffleButton) {
690     this.showShuffleButton = showShuffleButton;
691     updateShuffleButton();
692   }
693 
694   /** Returns whether the VR button is shown. */
getShowVrButton()695   public boolean getShowVrButton() {
696     return vrButton != null && vrButton.getVisibility() == VISIBLE;
697   }
698 
699   /**
700    * Sets whether the VR button is shown.
701    *
702    * @param showVrButton Whether the VR button is shown.
703    */
setShowVrButton(boolean showVrButton)704   public void setShowVrButton(boolean showVrButton) {
705     if (vrButton != null) {
706       vrButton.setVisibility(showVrButton ? VISIBLE : GONE);
707     }
708   }
709 
710   /**
711    * Sets listener for the VR button.
712    *
713    * @param onClickListener Listener for the VR button, or null to clear the listener.
714    */
setVrButtonListener(@ullable OnClickListener onClickListener)715   public void setVrButtonListener(@Nullable OnClickListener onClickListener) {
716     if (vrButton != null) {
717       vrButton.setOnClickListener(onClickListener);
718     }
719   }
720 
721   /**
722    * Sets the minimum interval between time bar position updates.
723    *
724    * <p>Note that smaller intervals, e.g. 33ms, will result in a smooth movement but will use more
725    * CPU resources while the time bar is visible, whereas larger intervals, e.g. 200ms, will result
726    * in a step-wise update with less CPU usage.
727    *
728    * @param minUpdateIntervalMs The minimum interval between time bar position updates, in
729    *     milliseconds.
730    */
setTimeBarMinUpdateInterval(int minUpdateIntervalMs)731   public void setTimeBarMinUpdateInterval(int minUpdateIntervalMs) {
732     // Do not accept values below 16ms (60fps) and larger than the maximum update interval.
733     timeBarMinUpdateIntervalMs =
734         Util.constrainValue(minUpdateIntervalMs, 16, MAX_UPDATE_INTERVAL_MS);
735   }
736 
737   /**
738    * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will
739    * be automatically hidden after this duration of time has elapsed without user input.
740    */
show()741   public void show() {
742     if (!isVisible()) {
743       setVisibility(VISIBLE);
744       for (VisibilityListener visibilityListener : visibilityListeners) {
745         visibilityListener.onVisibilityChange(getVisibility());
746       }
747       updateAll();
748       requestPlayPauseFocus();
749     }
750     // Call hideAfterTimeout even if already visible to reset the timeout.
751     hideAfterTimeout();
752   }
753 
754   /** Hides the controller. */
hide()755   public void hide() {
756     if (isVisible()) {
757       setVisibility(GONE);
758       for (VisibilityListener visibilityListener : visibilityListeners) {
759         visibilityListener.onVisibilityChange(getVisibility());
760       }
761       removeCallbacks(updateProgressAction);
762       removeCallbacks(hideAction);
763       hideAtMs = C.TIME_UNSET;
764     }
765   }
766 
767   /** Returns whether the controller is currently visible. */
isVisible()768   public boolean isVisible() {
769     return getVisibility() == VISIBLE;
770   }
771 
hideAfterTimeout()772   private void hideAfterTimeout() {
773     removeCallbacks(hideAction);
774     if (showTimeoutMs > 0) {
775       hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs;
776       if (isAttachedToWindow) {
777         postDelayed(hideAction, showTimeoutMs);
778       }
779     } else {
780       hideAtMs = C.TIME_UNSET;
781     }
782   }
783 
updateAll()784   private void updateAll() {
785     updatePlayPauseButton();
786     updateNavigation();
787     updateRepeatModeButton();
788     updateShuffleButton();
789     updateTimeline();
790   }
791 
updatePlayPauseButton()792   private void updatePlayPauseButton() {
793     if (!isVisible() || !isAttachedToWindow) {
794       return;
795     }
796     boolean requestPlayPauseFocus = false;
797     boolean shouldShowPauseButton = shouldShowPauseButton();
798     if (playButton != null) {
799       requestPlayPauseFocus |= shouldShowPauseButton && playButton.isFocused();
800       playButton.setVisibility(shouldShowPauseButton ? GONE : VISIBLE);
801     }
802     if (pauseButton != null) {
803       requestPlayPauseFocus |= !shouldShowPauseButton && pauseButton.isFocused();
804       pauseButton.setVisibility(shouldShowPauseButton ? VISIBLE : GONE);
805     }
806     if (requestPlayPauseFocus) {
807       requestPlayPauseFocus();
808     }
809   }
810 
updateNavigation()811   private void updateNavigation() {
812     if (!isVisible() || !isAttachedToWindow) {
813       return;
814     }
815 
816     @Nullable Player player = this.player;
817     boolean enableSeeking = false;
818     boolean enablePrevious = false;
819     boolean enableRewind = false;
820     boolean enableFastForward = false;
821     boolean enableNext = false;
822     if (player != null) {
823       Timeline timeline = player.getCurrentTimeline();
824       if (!timeline.isEmpty() && !player.isPlayingAd()) {
825         timeline.getWindow(player.getCurrentWindowIndex(), window);
826         boolean isSeekable = window.isSeekable;
827         enableSeeking = isSeekable;
828         enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious();
829         enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
830         enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
831         enableNext = window.isDynamic || player.hasNext();
832       }
833     }
834 
835     setButtonEnabled(enablePrevious, previousButton);
836     setButtonEnabled(enableRewind, rewindButton);
837     setButtonEnabled(enableFastForward, fastForwardButton);
838     setButtonEnabled(enableNext, nextButton);
839     if (timeBar != null) {
840       timeBar.setEnabled(enableSeeking);
841     }
842   }
843 
updateRepeatModeButton()844   private void updateRepeatModeButton() {
845     if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) {
846       return;
847     }
848 
849     if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
850       repeatToggleButton.setVisibility(GONE);
851       return;
852     }
853 
854     @Nullable Player player = this.player;
855     if (player == null) {
856       setButtonEnabled(false, repeatToggleButton);
857       repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);
858       repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);
859       return;
860     }
861 
862     setButtonEnabled(true, repeatToggleButton);
863     switch (player.getRepeatMode()) {
864       case Player.REPEAT_MODE_OFF:
865         repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);
866         repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);
867         break;
868       case Player.REPEAT_MODE_ONE:
869         repeatToggleButton.setImageDrawable(repeatOneButtonDrawable);
870         repeatToggleButton.setContentDescription(repeatOneButtonContentDescription);
871         break;
872       case Player.REPEAT_MODE_ALL:
873         repeatToggleButton.setImageDrawable(repeatAllButtonDrawable);
874         repeatToggleButton.setContentDescription(repeatAllButtonContentDescription);
875         break;
876       default:
877         // Never happens.
878     }
879     repeatToggleButton.setVisibility(VISIBLE);
880   }
881 
updateShuffleButton()882   private void updateShuffleButton() {
883     if (!isVisible() || !isAttachedToWindow || shuffleButton == null) {
884       return;
885     }
886 
887     @Nullable Player player = this.player;
888     if (!showShuffleButton) {
889       shuffleButton.setVisibility(GONE);
890     } else if (player == null) {
891       setButtonEnabled(false, shuffleButton);
892       shuffleButton.setImageDrawable(shuffleOffButtonDrawable);
893       shuffleButton.setContentDescription(shuffleOffContentDescription);
894     } else {
895       setButtonEnabled(true, shuffleButton);
896       shuffleButton.setImageDrawable(
897           player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable);
898       shuffleButton.setContentDescription(
899           player.getShuffleModeEnabled()
900               ? shuffleOnContentDescription
901               : shuffleOffContentDescription);
902     }
903   }
904 
updateTimeline()905   private void updateTimeline() {
906     @Nullable Player player = this.player;
907     if (player == null) {
908       return;
909     }
910     multiWindowTimeBar =
911         showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window);
912     currentWindowOffset = 0;
913     long durationUs = 0;
914     int adGroupCount = 0;
915     Timeline timeline = player.getCurrentTimeline();
916     if (!timeline.isEmpty()) {
917       int currentWindowIndex = player.getCurrentWindowIndex();
918       int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex;
919       int lastWindowIndex = multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex;
920       for (int i = firstWindowIndex; i <= lastWindowIndex; i++) {
921         if (i == currentWindowIndex) {
922           currentWindowOffset = C.usToMs(durationUs);
923         }
924         timeline.getWindow(i, window);
925         if (window.durationUs == C.TIME_UNSET) {
926           Assertions.checkState(!multiWindowTimeBar);
927           break;
928         }
929         for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) {
930           timeline.getPeriod(j, period);
931           int periodAdGroupCount = period.getAdGroupCount();
932           for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) {
933             long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex);
934             if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) {
935               if (period.durationUs == C.TIME_UNSET) {
936                 // Don't show ad markers for postrolls in periods with unknown duration.
937                 continue;
938               }
939               adGroupTimeInPeriodUs = period.durationUs;
940             }
941             long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs();
942             if (adGroupTimeInWindowUs >= 0) {
943               if (adGroupCount == adGroupTimesMs.length) {
944                 int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2;
945                 adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength);
946                 playedAdGroups = Arrays.copyOf(playedAdGroups, newLength);
947               }
948               adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs);
949               playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex);
950               adGroupCount++;
951             }
952           }
953         }
954         durationUs += window.durationUs;
955       }
956     }
957     long durationMs = C.usToMs(durationUs);
958     if (durationView != null) {
959       durationView.setText(Util.getStringForTime(formatBuilder, formatter, durationMs));
960     }
961     if (timeBar != null) {
962       timeBar.setDuration(durationMs);
963       int extraAdGroupCount = extraAdGroupTimesMs.length;
964       int totalAdGroupCount = adGroupCount + extraAdGroupCount;
965       if (totalAdGroupCount > adGroupTimesMs.length) {
966         adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount);
967         playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount);
968       }
969       System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount);
970       System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount);
971       timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount);
972     }
973     updateProgress();
974   }
975 
updateProgress()976   private void updateProgress() {
977     if (!isVisible() || !isAttachedToWindow) {
978       return;
979     }
980 
981     @Nullable Player player = this.player;
982     long position = 0;
983     long bufferedPosition = 0;
984     if (player != null) {
985       position = currentWindowOffset + player.getContentPosition();
986       bufferedPosition = currentWindowOffset + player.getContentBufferedPosition();
987     }
988     if (positionView != null && !scrubbing) {
989       positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
990     }
991     if (timeBar != null) {
992       timeBar.setPosition(position);
993       timeBar.setBufferedPosition(bufferedPosition);
994     }
995     if (progressUpdateListener != null) {
996       progressUpdateListener.onProgressUpdate(position, bufferedPosition);
997     }
998 
999     // Cancel any pending updates and schedule a new one if necessary.
1000     removeCallbacks(updateProgressAction);
1001     int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState();
1002     if (player != null && player.isPlaying()) {
1003       long mediaTimeDelayMs =
1004           timeBar != null ? timeBar.getPreferredUpdateDelay() : MAX_UPDATE_INTERVAL_MS;
1005 
1006       // Limit delay to the start of the next full second to ensure position display is smooth.
1007       long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000;
1008       mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs);
1009 
1010       // Calculate the delay until the next update in real time, taking playbackSpeed into account.
1011       float playbackSpeed = player.getPlaybackSpeed();
1012       long delayMs =
1013           playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS;
1014 
1015       // Constrain the delay to avoid too frequent / infrequent updates.
1016       delayMs = Util.constrainValue(delayMs, timeBarMinUpdateIntervalMs, MAX_UPDATE_INTERVAL_MS);
1017       postDelayed(updateProgressAction, delayMs);
1018     } else if (playbackState != Player.STATE_ENDED && playbackState != Player.STATE_IDLE) {
1019       postDelayed(updateProgressAction, MAX_UPDATE_INTERVAL_MS);
1020     }
1021   }
1022 
requestPlayPauseFocus()1023   private void requestPlayPauseFocus() {
1024     boolean shouldShowPauseButton = shouldShowPauseButton();
1025     if (!shouldShowPauseButton && playButton != null) {
1026       playButton.requestFocus();
1027     } else if (shouldShowPauseButton && pauseButton != null) {
1028       pauseButton.requestFocus();
1029     }
1030   }
1031 
setButtonEnabled(boolean enabled, @Nullable View view)1032   private void setButtonEnabled(boolean enabled, @Nullable View view) {
1033     if (view == null) {
1034       return;
1035     }
1036     view.setEnabled(enabled);
1037     view.setAlpha(enabled ? buttonAlphaEnabled : buttonAlphaDisabled);
1038     view.setVisibility(VISIBLE);
1039   }
1040 
seekToTimeBarPosition(Player player, long positionMs)1041   private void seekToTimeBarPosition(Player player, long positionMs) {
1042     int windowIndex;
1043     Timeline timeline = player.getCurrentTimeline();
1044     if (multiWindowTimeBar && !timeline.isEmpty()) {
1045       int windowCount = timeline.getWindowCount();
1046       windowIndex = 0;
1047       while (true) {
1048         long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs();
1049         if (positionMs < windowDurationMs) {
1050           break;
1051         } else if (windowIndex == windowCount - 1) {
1052           // Seeking past the end of the last window should seek to the end of the timeline.
1053           positionMs = windowDurationMs;
1054           break;
1055         }
1056         positionMs -= windowDurationMs;
1057         windowIndex++;
1058       }
1059     } else {
1060       windowIndex = player.getCurrentWindowIndex();
1061     }
1062     boolean dispatched = seekTo(player, windowIndex, positionMs);
1063     if (!dispatched) {
1064       // The seek wasn't dispatched then the progress bar scrubber will be in the wrong position.
1065       // Trigger a progress update to snap it back.
1066       updateProgress();
1067     }
1068   }
1069 
seekTo(Player player, int windowIndex, long positionMs)1070   private boolean seekTo(Player player, int windowIndex, long positionMs) {
1071     return controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
1072   }
1073 
1074   @Override
onAttachedToWindow()1075   public void onAttachedToWindow() {
1076     super.onAttachedToWindow();
1077     isAttachedToWindow = true;
1078     if (hideAtMs != C.TIME_UNSET) {
1079       long delayMs = hideAtMs - SystemClock.uptimeMillis();
1080       if (delayMs <= 0) {
1081         hide();
1082       } else {
1083         postDelayed(hideAction, delayMs);
1084       }
1085     } else if (isVisible()) {
1086       hideAfterTimeout();
1087     }
1088     updateAll();
1089   }
1090 
1091   @Override
onDetachedFromWindow()1092   public void onDetachedFromWindow() {
1093     super.onDetachedFromWindow();
1094     isAttachedToWindow = false;
1095     removeCallbacks(updateProgressAction);
1096     removeCallbacks(hideAction);
1097   }
1098 
1099   @Override
dispatchTouchEvent(MotionEvent ev)1100   public final boolean dispatchTouchEvent(MotionEvent ev) {
1101     if (ev.getAction() == MotionEvent.ACTION_DOWN) {
1102       removeCallbacks(hideAction);
1103     } else if (ev.getAction() == MotionEvent.ACTION_UP) {
1104       hideAfterTimeout();
1105     }
1106     return super.dispatchTouchEvent(ev);
1107   }
1108 
1109   @Override
dispatchKeyEvent(KeyEvent event)1110   public boolean dispatchKeyEvent(KeyEvent event) {
1111     return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
1112   }
1113 
1114   /**
1115    * Called to process media key events. Any {@link KeyEvent} can be passed but only media key
1116    * events will be handled.
1117    *
1118    * @param event A key event.
1119    * @return Whether the key event was handled.
1120    */
dispatchMediaKeyEvent(KeyEvent event)1121   public boolean dispatchMediaKeyEvent(KeyEvent event) {
1122     int keyCode = event.getKeyCode();
1123     @Nullable Player player = this.player;
1124     if (player == null || !isHandledMediaKey(keyCode)) {
1125       return false;
1126     }
1127     if (event.getAction() == KeyEvent.ACTION_DOWN) {
1128       if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
1129         controlDispatcher.dispatchFastForward(player);
1130       } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
1131         controlDispatcher.dispatchRewind(player);
1132       } else if (event.getRepeatCount() == 0) {
1133         switch (keyCode) {
1134           case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
1135             controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
1136             break;
1137           case KeyEvent.KEYCODE_MEDIA_PLAY:
1138             controlDispatcher.dispatchSetPlayWhenReady(player, true);
1139             break;
1140           case KeyEvent.KEYCODE_MEDIA_PAUSE:
1141             controlDispatcher.dispatchSetPlayWhenReady(player, false);
1142             break;
1143           case KeyEvent.KEYCODE_MEDIA_NEXT:
1144             controlDispatcher.dispatchNext(player);
1145             break;
1146           case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
1147             controlDispatcher.dispatchPrevious(player);
1148             break;
1149           default:
1150             break;
1151         }
1152       }
1153     }
1154     return true;
1155   }
1156 
shouldShowPauseButton()1157   private boolean shouldShowPauseButton() {
1158     return player != null
1159         && player.getPlaybackState() != Player.STATE_ENDED
1160         && player.getPlaybackState() != Player.STATE_IDLE
1161         && player.getPlayWhenReady();
1162   }
1163 
1164   @SuppressLint("InlinedApi")
isHandledMediaKey(int keyCode)1165   private static boolean isHandledMediaKey(int keyCode) {
1166     return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
1167         || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
1168         || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
1169         || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
1170         || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
1171         || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
1172         || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS;
1173   }
1174 
1175   /**
1176    * Returns whether the specified {@code timeline} can be shown on a multi-window time bar.
1177    *
1178    * @param timeline The {@link Timeline} to check.
1179    * @param window A scratch {@link Timeline.Window} instance.
1180    * @return Whether the specified timeline can be shown on a multi-window time bar.
1181    */
canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window)1182   private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) {
1183     if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) {
1184       return false;
1185     }
1186     int windowCount = timeline.getWindowCount();
1187     for (int i = 0; i < windowCount; i++) {
1188       if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) {
1189         return false;
1190       }
1191     }
1192     return true;
1193   }
1194 
1195   private final class ComponentListener
1196       implements Player.EventListener, TimeBar.OnScrubListener, OnClickListener {
1197 
1198     @Override
onScrubStart(TimeBar timeBar, long position)1199     public void onScrubStart(TimeBar timeBar, long position) {
1200       scrubbing = true;
1201       if (positionView != null) {
1202         positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
1203       }
1204     }
1205 
1206     @Override
onScrubMove(TimeBar timeBar, long position)1207     public void onScrubMove(TimeBar timeBar, long position) {
1208       if (positionView != null) {
1209         positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
1210       }
1211     }
1212 
1213     @Override
onScrubStop(TimeBar timeBar, long position, boolean canceled)1214     public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
1215       scrubbing = false;
1216       if (!canceled && player != null) {
1217         seekToTimeBarPosition(player, position);
1218       }
1219     }
1220 
1221     @Override
onPlaybackStateChanged(@layer.State int playbackState)1222     public void onPlaybackStateChanged(@Player.State int playbackState) {
1223       updatePlayPauseButton();
1224       updateProgress();
1225     }
1226 
1227     @Override
onPlayWhenReadyChanged( boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason)1228     public void onPlayWhenReadyChanged(
1229         boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
1230       updatePlayPauseButton();
1231       updateProgress();
1232     }
1233 
1234     @Override
onIsPlayingChanged(boolean isPlaying)1235     public void onIsPlayingChanged(boolean isPlaying) {
1236       updateProgress();
1237     }
1238 
1239     @Override
onRepeatModeChanged(int repeatMode)1240     public void onRepeatModeChanged(int repeatMode) {
1241       updateRepeatModeButton();
1242       updateNavigation();
1243     }
1244 
1245     @Override
onShuffleModeEnabledChanged(boolean shuffleModeEnabled)1246     public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
1247       updateShuffleButton();
1248       updateNavigation();
1249     }
1250 
1251     @Override
onPositionDiscontinuity(@layer.DiscontinuityReason int reason)1252     public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
1253       updateNavigation();
1254       updateTimeline();
1255     }
1256 
1257     @Override
onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason)1258     public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
1259       updateNavigation();
1260       updateTimeline();
1261     }
1262 
1263     @Override
onClick(View view)1264     public void onClick(View view) {
1265       Player player = PlayerControlView.this.player;
1266       if (player == null) {
1267         return;
1268       }
1269       if (nextButton == view) {
1270         controlDispatcher.dispatchNext(player);
1271       } else if (previousButton == view) {
1272         controlDispatcher.dispatchPrevious(player);
1273       } else if (fastForwardButton == view) {
1274         controlDispatcher.dispatchFastForward(player);
1275       } else if (rewindButton == view) {
1276         controlDispatcher.dispatchRewind(player);
1277       } else if (playButton == view) {
1278         if (player.getPlaybackState() == Player.STATE_IDLE) {
1279           if (playbackPreparer != null) {
1280             playbackPreparer.preparePlayback();
1281           }
1282         } else if (player.getPlaybackState() == Player.STATE_ENDED) {
1283           seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
1284         }
1285         controlDispatcher.dispatchSetPlayWhenReady(player, true);
1286       } else if (pauseButton == view) {
1287         controlDispatcher.dispatchSetPlayWhenReady(player, false);
1288       } else if (repeatToggleButton == view) {
1289         controlDispatcher.dispatchSetRepeatMode(
1290             player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));
1291       } else if (shuffleButton == view) {
1292         controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled());
1293       }
1294     }
1295   }
1296 }
1297