/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ui;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.Formatter;
import java.util.Locale;
/**
* A view for controlling {@link ExoPlayer} instances.
* <p>
* A PlaybackControlView can be customized by setting attributes (or calling corresponding methods),
* overriding the view's layout file or by specifying a custom view layout file, as outlined below.
*
* <h3>Attributes</h3>
* The following attributes can be set on a PlaybackControlView when used in a layout XML file:
* <p>
* <ul>
* <li><b>{@code show_timeout}</b> - The time between the last user interaction and the controls
* being automatically hidden, in milliseconds. Use zero if the controls should not
* automatically timeout.
* <ul>
* <li>Corresponding method: {@link #setShowTimeoutMs(int)}</li>
* <li>Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}</li>
* </ul>
* </li>
* <li><b>{@code rewind_increment}</b> - The duration of the rewind applied when the user taps the
* rewind button, in milliseconds. Use zero to disable the rewind button.
* <ul>
* <li>Corresponding method: {@link #setRewindIncrementMs(int)}</li>
* <li>Default: {@link #DEFAULT_REWIND_MS}</li>
* </ul>
* </li>
* <li><b>{@code fastforward_increment}</b> - Like {@code rewind_increment}, but for fast forward.
* <ul>
* <li>Corresponding method: {@link #setFastForwardIncrementMs(int)}</li>
* <li>Default: {@link #DEFAULT_FAST_FORWARD_MS}</li>
* </ul>
* </li>
* <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout to be inflated. See
* below for more details.
* <ul>
* <li>Corresponding method: None</li>
* <li>Default: {@code R.id.exo_playback_control_view}</li>
* </ul>
* </li>
* </ul>
*
* <h3>Overriding the layout file</h3>
* To customize the layout of PlaybackControlView throughout your app, or just for certain
* configurations, you can define {@code exo_playback_control_view.xml} layout files in your
* application {@code res/layout*} directories. These layouts will override the one provided by the
* ExoPlayer library, and will be inflated for use by PlaybackControlView. The view identifies and
* binds its children by looking for the following ids:
* <p>
* <ul>
* <li><b>{@code exo_play}</b> - The play button.
* <ul>
* <li>Type: {@link View}</li>
* </ul>
* </li>
* <li><b>{@code exo_pause}</b> - The pause button.
* <ul>
* <li>Type: {@link View}</li>
* </ul>
* </li>
* <li><b>{@code exo_ffwd}</b> - The fast forward button.
* <ul>
* <li>Type: {@link View}</li>
* </ul>
* </li>
* <li><b>{@code exo_rew}</b> - The rewind button.
* <ul>
* <li>Type: {@link View}</li>
* </ul>
* </li>
* <li><b>{@code exo_prev}</b> - The previous track button.
* <ul>
* <li>Type: {@link View}</li>
* </ul>
* </li>
* <li><b>{@code exo_next}</b> - The next track button.
* <ul>
* <li>Type: {@link View}</li>
* </ul>
* </li>
* <li><b>{@code exo_position}</b> - Text view displaying the current playback position.
* <ul>
* <li>Type: {@link TextView}</li>
* </ul>
* </li>
* <li><b>{@code exo_duration}</b> - Text view displaying the current media duration.
* <ul>
* <li>Type: {@link TextView}</li>
* </ul>
* </li>
* <li><b>{@code exo_progress}</b> - Time bar that's updated during playback and allows seeking.
* <ul>
* <li>Type: {@link TimeBar}</li>
* </ul>
* </li>
* </ul>
* <p>
* All child views are optional and so can be omitted if not required, however where defined they
* must be of the expected type.
*
* <h3>Specifying a custom layout file</h3>
* Defining your own {@code exo_playback_control_view.xml} is useful to customize the layout of
* PlaybackControlView throughout your application. It's also possible to customize the layout for a
* single instance in a layout file. This is achieved by setting the {@code controller_layout_id}
* attribute on a PlaybackControlView. This will cause the specified layout to be inflated instead
* of {@code exo_playback_control_view.xml} for only the instance on which the attribute is set.
*/
public class PlaybackControlView extends FrameLayout {
/**
* Listener to be notified about changes of the visibility of the UI control.
*/
public interface VisibilityListener {
/**
* Called when the visibility changes.
*
* @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}.
*/
void onVisibilityChange(int visibility);
}
/**
* Dispatches operations to the player.
* <p>
* Implementations may choose to suppress (e.g. prevent playback from resuming if audio focus is
* denied) or modify (e.g. change the seek position to prevent a user from seeking past a
* non-skippable advert) operations.
*/
public interface ControlDispatcher {
/**
* Dispatches a {@link ExoPlayer#setPlayWhenReady(boolean)} operation.
*
* @param player The player to which the operation should be dispatched.
* @param playWhenReady Whether playback should proceed when ready.
* @return True if the operation was dispatched. False if suppressed.
*/
boolean dispatchSetPlayWhenReady(ExoPlayer player, boolean playWhenReady);
/**
* Dispatches a {@link ExoPlayer#seekTo(int, long)} operation.
*
* @param player The player to which the operation should be dispatched.
* @param windowIndex The index of the window.
* @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek
* to the window's default position.
* @return True if the operation was dispatched. False if suppressed.
*/
boolean dispatchSeekTo(ExoPlayer player, int windowIndex, long positionMs);
}
/**
* Default {@link ControlDispatcher} that dispatches operations to the player without
* modification.
*/
public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new ControlDispatcher() {
@Override
public boolean dispatchSetPlayWhenReady(ExoPlayer player, boolean playWhenReady) {
player.setPlayWhenReady(playWhenReady);
return true;
}
@Override
public boolean dispatchSeekTo(ExoPlayer player, int windowIndex, long positionMs) {
player.seekTo(windowIndex, positionMs);
return true;
}
};
public static final int DEFAULT_FAST_FORWARD_MS = 15000;
public static final int DEFAULT_REWIND_MS = 5000;
public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000;
/**
* The maximum number of windows that can be shown in a multi-window time bar.
*/
public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100;
private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;
private final ComponentListener componentListener;
private final View previousButton;
private final View nextButton;
private final View playButton;
private final View pauseButton;
private final View fastForwardButton;
private final View rewindButton;
private final TextView durationView;
private final TextView positionView;
private final TimeBar timeBar;
private final StringBuilder formatBuilder;
private final Formatter formatter;
private final Timeline.Period period;
private final Timeline.Window window;
private ExoPlayer player;
private ControlDispatcher controlDispatcher;
private VisibilityListener visibilityListener;
private boolean isAttachedToWindow;
private boolean showMultiWindowTimeBar;
private boolean multiWindowTimeBar;
private boolean scrubbing;
private int rewindMs;
private int fastForwardMs;
private int showTimeoutMs;
private long hideAtMs;
private long[] adBreakTimesMs;
private final Runnable updateProgressAction = new Runnable() {
@Override
public void run() {
updateProgress();
}
};
private final Runnable hideAction = new Runnable() {
@Override
public void run() {
hide();
}
};
public PlaybackControlView(Context context) {
this(context, null);
}
public PlaybackControlView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
int controllerLayoutId = R.layout.exo_playback_control_view;
rewindMs = DEFAULT_REWIND_MS;
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.PlaybackControlView, 0, 0);
try {
rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs);
fastForwardMs = a.getInt(R.styleable.PlaybackControlView_fastforward_increment,
fastForwardMs);
showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs);
controllerLayoutId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id,
controllerLayoutId);
} finally {
a.recycle();
}
}
period = new Timeline.Period();
window = new Timeline.Window();
formatBuilder = new StringBuilder();
formatter = new Formatter(formatBuilder, Locale.getDefault());
adBreakTimesMs = new long[0];
componentListener = new ComponentListener();
controlDispatcher = DEFAULT_CONTROL_DISPATCHER;
LayoutInflater.from(context).inflate(controllerLayoutId, this);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
durationView = (TextView) findViewById(R.id.exo_duration);
positionView = (TextView) findViewById(R.id.exo_position);
timeBar = (TimeBar) findViewById(R.id.exo_progress);
if (timeBar != null) {
timeBar.setListener(componentListener);
}
playButton = findViewById(R.id.exo_play);
if (playButton != null) {
playButton.setOnClickListener(componentListener);
}
pauseButton = findViewById(R.id.exo_pause);
if (pauseButton != null) {
pauseButton.setOnClickListener(componentListener);
}
previousButton = findViewById(R.id.exo_prev);
if (previousButton != null) {
previousButton.setOnClickListener(componentListener);
}
nextButton = findViewById(R.id.exo_next);
if (nextButton != null) {
nextButton.setOnClickListener(componentListener);
}
rewindButton = findViewById(R.id.exo_rew);
if (rewindButton != null) {
rewindButton.setOnClickListener(componentListener);
}
fastForwardButton = findViewById(R.id.exo_ffwd);
if (fastForwardButton != null) {
fastForwardButton.setOnClickListener(componentListener);
}
}
/**
* Returns the player currently being controlled by this view, or null if no player is set.
*/
public ExoPlayer getPlayer() {
return player;
}
/**
* Sets the {@link ExoPlayer} to control.
*
* @param player The {@code ExoPlayer} to control.
*/
public void setPlayer(ExoPlayer player) {
if (this.player == player) {
return;
}
if (this.player != null) {
this.player.removeListener(componentListener);
}
this.player = player;
if (player != null) {
player.addListener(componentListener);
}
updateAll();
}
/**
* Sets whether the time bar should show all windows, as opposed to just the current one. If the
* timeline has a period with unknown duration or more than
* {@link #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a
* single window.
*
* @param showMultiWindowTimeBar Whether the time bar should show all windows.
*/
public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
this.showMultiWindowTimeBar = showMultiWindowTimeBar;
updateTimeBarMode();
}
/**
* Sets the {@link VisibilityListener}.
*
* @param listener The listener to be notified about visibility changes.
*/
public void setVisibilityListener(VisibilityListener listener) {
this.visibilityListener = listener;
}
/**
* Sets the {@link ControlDispatcher}.
*
* @param controlDispatcher The {@link ControlDispatcher}, or null to use
* {@link #DEFAULT_CONTROL_DISPATCHER}.
*/
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
this.controlDispatcher = controlDispatcher == null ? DEFAULT_CONTROL_DISPATCHER
: controlDispatcher;
}
/**
* Sets the rewind increment in milliseconds.
*
* @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the
* rewind button to be disabled.
*/
public void setRewindIncrementMs(int rewindMs) {
this.rewindMs = rewindMs;
updateNavigation();
}
/**
* Sets the fast forward increment in milliseconds.
*
* @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will
* cause the fast forward button to be disabled.
*/
public void setFastForwardIncrementMs(int fastForwardMs) {
this.fastForwardMs = fastForwardMs;
updateNavigation();
}
/**
* Returns the playback controls timeout. The playback controls are automatically hidden after
* this duration of time has elapsed without user input.
*
* @return The duration in milliseconds. A non-positive value indicates that the controls will
* remain visible indefinitely.
*/
public int getShowTimeoutMs() {
return showTimeoutMs;
}
/**
* Sets the playback controls timeout. The playback controls are automatically hidden after this
* duration of time has elapsed without user input.
*
* @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls
* to remain visible indefinitely.
*/
public void setShowTimeoutMs(int showTimeoutMs) {
this.showTimeoutMs = showTimeoutMs;
}
/**
* Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will
* be automatically hidden after this duration of time has elapsed without user input.
*/
public void show() {
if (!isVisible()) {
setVisibility(VISIBLE);
if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
}
updateAll();
requestPlayPauseFocus();
}
// Call hideAfterTimeout even if already visible to reset the timeout.
hideAfterTimeout();
}
/**
* Hides the controller.
*/
public void hide() {
if (isVisible()) {
setVisibility(GONE);
if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
}
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
hideAtMs = C.TIME_UNSET;
}
}
/**
* Returns whether the controller is currently visible.
*/
public boolean isVisible() {
return getVisibility() == VISIBLE;
}
private void hideAfterTimeout() {
removeCallbacks(hideAction);
if (showTimeoutMs > 0) {
hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs;
if (isAttachedToWindow) {
postDelayed(hideAction, showTimeoutMs);
}
} else {
hideAtMs = C.TIME_UNSET;
}
}
private void updateAll() {
updatePlayPauseButton();
updateNavigation();
updateProgress();
}
private void updatePlayPauseButton() {
if (!isVisible() || !isAttachedToWindow) {
return;
}
boolean requestPlayPauseFocus = false;
boolean playing = player != null && player.getPlayWhenReady();
if (playButton != null) {
requestPlayPauseFocus |= playing && playButton.isFocused();
playButton.setVisibility(playing ? View.GONE : View.VISIBLE);
}
if (pauseButton != null) {
requestPlayPauseFocus |= !playing && pauseButton.isFocused();
pauseButton.setVisibility(!playing ? View.GONE : View.VISIBLE);
}
if (requestPlayPauseFocus) {
requestPlayPauseFocus();
}
}
private void updateNavigation() {
if (!isVisible() || !isAttachedToWindow) {
return;
}
Timeline timeline = player != null ? player.getCurrentTimeline() : null;
boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty();
boolean isSeekable = false;
boolean enablePrevious = false;
boolean enableNext = false;
if (haveNonEmptyTimeline) {
int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window);
isSeekable = window.isSeekable;
enablePrevious = windowIndex > 0 || isSeekable || !window.isDynamic;
enableNext = (windowIndex < timeline.getWindowCount() - 1) || window.isDynamic;
if (timeline.getPeriod(player.getCurrentPeriodIndex(), period).isAd) {
// Always hide player controls during ads.
hide();
}
}
setButtonEnabled(enablePrevious, previousButton);
setButtonEnabled(enableNext, nextButton);
setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton);
setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton);
if (timeBar != null) {
timeBar.setEnabled(isSeekable);
}
}
private void updateTimeBarMode() {
if (player == null) {
return;
}
multiWindowTimeBar = showMultiWindowTimeBar
&& canShowMultiWindowTimeBar(player.getCurrentTimeline(), period);
}
private void updateProgress() {
if (!isVisible() || !isAttachedToWindow) {
return;
}
long position = 0;
long bufferedPosition = 0;
long duration = 0;
if (player != null) {
if (multiWindowTimeBar) {
Timeline timeline = player.getCurrentTimeline();
int windowCount = timeline.getWindowCount();
int periodIndex = player.getCurrentPeriodIndex();
long positionUs = 0;
long bufferedPositionUs = 0;
long durationUs = 0;
boolean isInAdBreak = false;
boolean isPlayingAd = false;
int adBreakCount = 0;
for (int i = 0; i < windowCount; i++) {
timeline.getWindow(i, window);
for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) {
if (timeline.getPeriod(j, period).isAd) {
isPlayingAd |= j == periodIndex;
if (!isInAdBreak) {
isInAdBreak = true;
if (adBreakCount == adBreakTimesMs.length) {
adBreakTimesMs = Arrays.copyOf(adBreakTimesMs,
adBreakTimesMs.length == 0 ? 1 : adBreakTimesMs.length * 2);
}
adBreakTimesMs[adBreakCount++] = C.usToMs(durationUs);
}
} else {
isInAdBreak = false;
long periodDurationUs = period.getDurationUs();
Assertions.checkState(periodDurationUs != C.TIME_UNSET);
long periodDurationInWindowUs = periodDurationUs;
if (j == window.firstPeriodIndex) {
periodDurationInWindowUs -= window.positionInFirstPeriodUs;
}
if (i < periodIndex) {
positionUs += periodDurationInWindowUs;
bufferedPositionUs += periodDurationInWindowUs;
}
durationUs += periodDurationInWindowUs;
}
}
}
position = C.usToMs(positionUs);
bufferedPosition = C.usToMs(bufferedPositionUs);
duration = C.usToMs(durationUs);
if (!isPlayingAd) {
position += player.getCurrentPosition();
bufferedPosition += player.getBufferedPosition();
}
if (timeBar != null) {
timeBar.setAdBreakTimesMs(adBreakTimesMs, adBreakCount);
}
} else {
position = player.getCurrentPosition();
bufferedPosition = player.getBufferedPosition();
duration = player.getDuration();
}
}
if (durationView != null) {
durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration));
}
if (positionView != null && !scrubbing) {
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
}
if (timeBar != null) {
timeBar.setPosition(position);
timeBar.setBufferedPosition(bufferedPosition);
timeBar.setDuration(duration);
}
// Cancel any pending updates and schedule a new one if necessary.
removeCallbacks(updateProgressAction);
int playbackState = player == null ? ExoPlayer.STATE_IDLE : player.getPlaybackState();
if (playbackState != ExoPlayer.STATE_IDLE && playbackState != ExoPlayer.STATE_ENDED) {
long delayMs;
if (player.getPlayWhenReady() && playbackState == ExoPlayer.STATE_READY) {
delayMs = 1000 - (position % 1000);
if (delayMs < 200) {
delayMs += 1000;
}
} else {
delayMs = 1000;
}
postDelayed(updateProgressAction, delayMs);
}
}
private void requestPlayPauseFocus() {
boolean playing = player != null && player.getPlayWhenReady();
if (!playing && playButton != null) {
playButton.requestFocus();
} else if (playing && pauseButton != null) {
pauseButton.requestFocus();
}
}
private void setButtonEnabled(boolean enabled, View view) {
if (view == null) {
return;
}
view.setEnabled(enabled);
if (Util.SDK_INT >= 11) {
setViewAlphaV11(view, enabled ? 1f : 0.3f);
view.setVisibility(VISIBLE);
} else {
view.setVisibility(enabled ? VISIBLE : INVISIBLE);
}
}
@TargetApi(11)
private void setViewAlphaV11(View view, float alpha) {
view.setAlpha(alpha);
}
private void previous() {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return;
}
int windowIndex = player.getCurrentWindowIndex();
timeline.getWindow(windowIndex, window);
if (windowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|| (window.isDynamic && !window.isSeekable))) {
seekTo(windowIndex - 1, C.TIME_UNSET);
} else {
seekTo(0);
}
}
private void next() {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return;
}
int windowIndex = player.getCurrentWindowIndex();
if (windowIndex < timeline.getWindowCount() - 1) {
seekTo(windowIndex + 1, C.TIME_UNSET);
} else if (timeline.getWindow(windowIndex, window, false).isDynamic) {
seekTo(windowIndex, C.TIME_UNSET);
}
}
private void rewind() {
if (rewindMs <= 0) {
return;
}
seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0));
}
private void fastForward() {
if (fastForwardMs <= 0) {
return;
}
seekTo(Math.min(player.getCurrentPosition() + fastForwardMs, player.getDuration()));
}
private void seekTo(long positionMs) {
seekTo(player.getCurrentWindowIndex(), positionMs);
}
private void seekTo(int windowIndex, long positionMs) {
boolean dispatched = controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
if (!dispatched) {
// The seek wasn't dispatched. If the progress bar was dragged by the user to perform the
// seek then it'll now be in the wrong position. Trigger a progress update to snap it back.
updateProgress();
}
}
private void seekToTimebarPosition(long timebarPositionMs) {
if (multiWindowTimeBar) {
Timeline timeline = player.getCurrentTimeline();
int windowCount = timeline.getWindowCount();
long remainingMs = timebarPositionMs;
for (int i = 0; i < windowCount; i++) {
timeline.getWindow(i, window);
for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) {
if (!timeline.getPeriod(j, period).isAd) {
long periodDurationMs = period.getDurationMs();
if (periodDurationMs == C.TIME_UNSET) {
// Should never happen as canShowMultiWindowTimeBar is true.
throw new IllegalStateException();
}
if (j == window.firstPeriodIndex) {
periodDurationMs -= window.getPositionInFirstPeriodMs();
}
if (i == windowCount - 1 && j == window.lastPeriodIndex
&& remainingMs >= periodDurationMs) {
// Seeking past the end of the last window should seek to the end of the timeline.
seekTo(i, window.getDurationMs());
return;
}
if (remainingMs < periodDurationMs) {
seekTo(i, period.getPositionInWindowMs() + remainingMs);
return;
}
remainingMs -= periodDurationMs;
}
}
}
} else {
seekTo(timebarPositionMs);
}
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
isAttachedToWindow = true;
if (hideAtMs != C.TIME_UNSET) {
long delayMs = hideAtMs - SystemClock.uptimeMillis();
if (delayMs <= 0) {
hide();
} else {
postDelayed(hideAction, delayMs);
}
}
updateAll();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
isAttachedToWindow = false;
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
boolean handled = dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
if (handled) {
show();
}
return handled;
}
/**
* Called to process media key events. Any {@link KeyEvent} can be passed but only media key
* events will be handled.
*
* @param event A key event.
* @return Whether the key event was handled.
*/
public boolean dispatchMediaKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
if (player == null || !isHandledMediaKey(keyCode)) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
fastForward();
break;
case KeyEvent.KEYCODE_MEDIA_REWIND:
rewind();
break;
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
controlDispatcher.dispatchSetPlayWhenReady(player, true);
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
controlDispatcher.dispatchSetPlayWhenReady(player, false);
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
next();
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
previous();
break;
default:
break;
}
}
show();
return true;
}
@SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|| keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
|| keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
|| keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
|| keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS;
}
/**
* Returns whether the specified {@code timeline} can be shown on a multi-window time bar.
*
* @param timeline The {@link Timeline} to check.
* @param period A scratch {@link Timeline.Period} instance.
* @return Whether the specified timeline can be shown on a multi-window time bar.
*/
private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Period period) {
if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) {
return false;
}
int periodCount = timeline.getPeriodCount();
for (int i = 0; i < periodCount; i++) {
timeline.getPeriod(i, period);
if (!period.isAd && period.durationUs == C.TIME_UNSET) {
return false;
}
}
return true;
}
private final class ComponentListener implements ExoPlayer.EventListener, TimeBar.OnScrubListener,
OnClickListener {
@Override
public void onScrubStart(TimeBar timeBar) {
removeCallbacks(hideAction);
scrubbing = true;
}
@Override
public void onScrubMove(TimeBar timeBar, long position) {
if (positionView != null) {
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
}
}
@Override
public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
scrubbing = false;
if (!canceled && player != null) {
seekToTimebarPosition(position);
}
hideAfterTimeout();
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
updatePlayPauseButton();
updateProgress();
}
@Override
public void onPositionDiscontinuity() {
updateNavigation();
updateProgress();
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
// Do nothing.
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
updateNavigation();
updateTimeBarMode();
updateProgress();
}
@Override
public void onLoadingChanged(boolean isLoading) {
// Do nothing.
}
@Override
public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {
// Do nothing.
}
@Override
public void onPlayerError(ExoPlaybackException error) {
// Do nothing.
}
@Override
public void onClick(View view) {
if (player != null) {
if (nextButton == view) {
next();
} else if (previousButton == view) {
previous();
} else if (fastForwardButton == view) {
fastForward();
} else if (rewindButton == view) {
rewind();
} else if (playButton == view) {
controlDispatcher.dispatchSetPlayWhenReady(player, true);
} else if (pauseButton == view) {
controlDispatcher.dispatchSetPlayWhenReady(player, false);
}
}
hideAfterTimeout();
}
}
}