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