Make LeanbackPlayerAdapter use a ControlDispatcher + Misc cleanup

1. Make LeanbackPlayerAdapter use a ControlDispatcher. This
   allows apps to suppress control events in some circumstances,
   and is in-line with our mobile controls.
2. Misc simplifications and cleanup to LeanbackPlayerAdapter.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=166852816
This commit is contained in:
olly 2017-08-29 08:50:17 -07:00 committed by Oliver Woodman
parent 44dc3c3ab3
commit 55e928f75a
5 changed files with 215 additions and 142 deletions

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.leanback;
import android.content.Context;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v17.leanback.R;
import android.support.v17.leanback.media.PlaybackGlueHost;
import android.support.v17.leanback.media.PlayerAdapter;
@ -25,6 +26,8 @@ import android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters;
@ -48,13 +51,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
private final SimpleExoPlayer player;
private final Handler handler;
private final ComponentListener componentListener;
private final Runnable updatePlayerRunnable;
private final Runnable updateProgressRunnable;
private ControlDispatcher controlDispatcher;
private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean initialized;
private boolean hasSurface;
private boolean isBuffering;
private boolean lastNotifiedPreparedState;
/**
* Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the
@ -70,7 +73,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
this.player = player;
handler = new Handler();
componentListener = new ComponentListener();
updatePlayerRunnable = new Runnable() {
controlDispatcher = new DefaultControlDispatcher();
updateProgressRunnable = new Runnable() {
@Override
public void run() {
Callback callback = getCallback();
@ -81,34 +85,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
};
}
@Override
public void onAttachedToHost(PlaybackGlueHost host) {
if (host instanceof SurfaceHolderGlueHost) {
surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host);
surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener);
}
notifyListeners();
player.addListener(componentListener);
player.addVideoListener(componentListener);
}
private void notifyListeners() {
boolean oldIsPrepared = isPrepared();
int playbackState = player.getPlaybackState();
boolean isInitialized = playbackState != Player.STATE_IDLE;
isBuffering = playbackState == Player.STATE_BUFFERING;
boolean hasEnded = playbackState == Player.STATE_ENDED;
initialized = isInitialized;
Callback callback = getCallback();
if (oldIsPrepared != isPrepared()) {
callback.onPreparedStateChanged(this);
}
callback.onPlayStateChanged(this);
callback.onBufferingStateChanged(this, isBuffering || !initialized);
if (hasEnded) {
callback.onPlayCompleted(this);
}
/**
* Sets the {@link ControlDispatcher}.
*
* @param controlDispatcher The {@link ControlDispatcher}, or null to use
* {@link DefaultControlDispatcher}.
*/
public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) {
this.controlDispatcher = controlDispatcher == null ? new DefaultControlDispatcher()
: controlDispatcher;
}
/**
@ -121,6 +106,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
this.errorMessageProvider = errorMessageProvider;
}
// PlayerAdapter implementation.
@Override
public void onAttachedToHost(PlaybackGlueHost host) {
if (host instanceof SurfaceHolderGlueHost) {
surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host);
surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener);
}
notifyStateChanged();
player.addListener(componentListener);
player.addVideoListener(componentListener);
}
@Override
public void onDetachedFromHost() {
player.removeListener(componentListener);
@ -129,56 +127,59 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
surfaceHolderGlueHost = null;
}
initialized = false;
hasSurface = false;
Callback callback = getCallback();
callback.onBufferingStateChanged(this, false);
callback.onPlayStateChanged(this);
callback.onPreparedStateChanged(this);
maybeNotifyPreparedStateChanged(callback);
}
@Override
public void setProgressUpdatingEnabled(final boolean enabled) {
handler.removeCallbacks(updatePlayerRunnable);
public void setProgressUpdatingEnabled(boolean enabled) {
handler.removeCallbacks(updateProgressRunnable);
if (enabled) {
handler.post(updatePlayerRunnable);
handler.post(updateProgressRunnable);
}
}
@Override
public boolean isPlaying() {
return initialized && player.getPlayWhenReady();
int playbackState = player.getPlaybackState();
return playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
&& player.getPlayWhenReady();
}
@Override
public long getDuration() {
long durationMs = player.getDuration();
return durationMs != C.TIME_UNSET ? durationMs : -1;
return durationMs == C.TIME_UNSET ? -1 : durationMs;
}
@Override
public long getCurrentPosition() {
return initialized ? player.getCurrentPosition() : -1;
return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition();
}
@Override
public void play() {
if (player.getPlaybackState() == Player.STATE_ENDED) {
player.seekToDefaultPosition();
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
if (controlDispatcher.dispatchSetPlayWhenReady(player, true)) {
getCallback().onPlayStateChanged(this);
}
player.setPlayWhenReady(true);
getCallback().onPlayStateChanged(this);
}
@Override
public void pause() {
player.setPlayWhenReady(false);
getCallback().onPlayStateChanged(this);
if (controlDispatcher.dispatchSetPlayWhenReady(player, false)) {
getCallback().onPlayStateChanged(this);
}
}
@Override
public void seekTo(long positionMs) {
player.seekTo(positionMs);
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), positionMs);
}
@Override
@ -188,13 +189,35 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
@Override
public boolean isPrepared() {
return initialized && (surfaceHolderGlueHost == null || hasSurface);
return player.getPlaybackState() != Player.STATE_IDLE
&& (surfaceHolderGlueHost == null || hasSurface);
}
private void setVideoSurface(Surface surface) {
// Internal methods.
/* package */ void setVideoSurface(Surface surface) {
hasSurface = surface != null;
player.setVideoSurface(surface);
getCallback().onPreparedStateChanged(this);
maybeNotifyPreparedStateChanged(getCallback());
}
/* package */ void notifyStateChanged() {
int playbackState = player.getPlaybackState();
Callback callback = getCallback();
maybeNotifyPreparedStateChanged(callback);
callback.onPlayStateChanged(this);
callback.onBufferingStateChanged(this, playbackState == Player.STATE_BUFFERING);
if (playbackState == Player.STATE_ENDED) {
callback.onPlayCompleted(this);
}
}
private void maybeNotifyPreparedStateChanged(Callback callback) {
boolean isPrepared = isPrepared();
if (lastNotifiedPreparedState != isPrepared) {
lastNotifiedPreparedState = isPrepared;
callback.onPreparedStateChanged(this);
}
}
private final class ComponentListener implements Player.EventListener,
@ -208,7 +231,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
// Do nothing.
}
@ -221,7 +244,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
notifyListeners();
notifyStateChanged();
}
@Override
@ -292,4 +315,3 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2017 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;
import com.google.android.exoplayer2.Player.RepeatMode;
/**
* Dispatches operations to the {@link 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 Player#setPlayWhenReady(boolean)} operation.
*
* @param player The {@link 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(Player player, boolean playWhenReady);
/**
* Dispatches a {@link Player#seekTo(int, long)} operation.
*
* @param player The {@link 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(Player player, int windowIndex, long positionMs);
/**
* Dispatches a {@link Player#setRepeatMode(int)} operation.
*
* @param player The {@link Player} to which the operation should be dispatched.
* @param repeatMode The repeat mode.
* @return True if the operation was dispatched. False if suppressed.
*/
boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode);
/**
* Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation.
*
* @param player The {@link Player} to which the operation should be dispatched.
* @param shuffleModeEnabled Whether shuffling is enabled.
* @return True if the operation was dispatched. False if suppressed.
*/
boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled);
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2017 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;
import com.google.android.exoplayer2.Player.RepeatMode;
/**
* Default {@link ControlDispatcher} that dispatches all operations to the player without
* modification.
*/
public class DefaultControlDispatcher implements ControlDispatcher {
@Override
public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {
player.setPlayWhenReady(playWhenReady);
return true;
}
@Override
public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) {
player.seekTo(windowIndex, positionMs);
return true;
}
@Override
public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) {
player.setRepeatMode(repeatMode);
return true;
}
@Override
public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) {
player.setShuffleModeEnabled(shuffleModeEnabled);
return true;
}
}

View File

@ -34,7 +34,6 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@ -180,6 +179,12 @@ public class PlaybackControlView extends FrameLayout {
ExoPlayerLibraryInfo.registerModule("goog.exo.ui");
}
/**
* @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}.
*/
@Deprecated
public interface ControlDispatcher extends com.google.android.exoplayer2.ControlDispatcher {}
/**
* Listener to be notified about changes of the visibility of the UI control.
*/
@ -194,86 +199,13 @@ public class PlaybackControlView extends FrameLayout {
}
private static final class DefaultControlDispatcher
extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {}
/**
* Dispatches operations to the {@link 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.
* @deprecated Use {@link com.google.android.exoplayer2.DefaultControlDispatcher}.
*/
public interface ControlDispatcher {
/**
* Dispatches a {@link Player#setPlayWhenReady(boolean)} operation.
*
* @param player The {@link 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(Player player, boolean playWhenReady);
/**
* Dispatches a {@link Player#seekTo(int, long)} operation.
*
* @param player The {@link 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(Player player, int windowIndex, long positionMs);
/**
* Dispatches a {@link Player#setRepeatMode(int)} operation.
*
* @param player The {@link Player} to which the operation should be dispatched.
* @param repeatMode The repeat mode.
* @return True if the operation was dispatched. False if suppressed.
*/
boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode);
/**
* Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation.
*
* @param player The {@link Player} to which the operation should be dispatched.
* @param shuffleModeEnabled Whether shuffling is enabled.
* @return True if the operation was dispatched. False if suppressed.
*/
boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled);
}
/**
* Default {@link ControlDispatcher} that dispatches operations to the player without
* modification.
*/
public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new ControlDispatcher() {
@Override
public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {
player.setPlayWhenReady(playWhenReady);
return true;
}
@Override
public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) {
player.seekTo(windowIndex, positionMs);
return true;
}
@Override
public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) {
player.setRepeatMode(repeatMode);
return true;
}
@Override
public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) {
player.setShuffleModeEnabled(shuffleModeEnabled);
return true;
}
};
@Deprecated
public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher();
/**
* The default fast forward increment, in milliseconds.
@ -325,7 +257,7 @@ public class PlaybackControlView extends FrameLayout {
private final String repeatAllButtonContentDescription;
private Player player;
private ControlDispatcher controlDispatcher;
private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;
private VisibilityListener visibilityListener;
private boolean isAttachedToWindow;
@ -400,7 +332,7 @@ public class PlaybackControlView extends FrameLayout {
extraAdGroupTimesMs = new long[0];
extraPlayedAdGroups = new boolean[0];
componentListener = new ComponentListener();
controlDispatcher = DEFAULT_CONTROL_DISPATCHER;
controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher();
LayoutInflater.from(context).inflate(controllerLayoutId, this);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
@ -534,14 +466,15 @@ public class PlaybackControlView extends FrameLayout {
}
/**
* Sets the {@link ControlDispatcher}.
* Sets the {@link com.google.android.exoplayer2.ControlDispatcher}.
*
* @param controlDispatcher The {@link ControlDispatcher}, or null to use
* {@link #DEFAULT_CONTROL_DISPATCHER}.
* @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null
* to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}.
*/
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
this.controlDispatcher = controlDispatcher == null ? DEFAULT_CONTROL_DISPATCHER
: controlDispatcher;
public void setControlDispatcher(
@Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) {
this.controlDispatcher = controlDispatcher == null
? new com.google.android.exoplayer2.DefaultControlDispatcher() : controlDispatcher;
}
/**

View File

@ -34,6 +34,8 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
@ -47,7 +49,6 @@ import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.ui.PlaybackControlView.ControlDispatcher;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.RepeatModeUtil;
import com.google.android.exoplayer2.util.Util;
@ -616,9 +617,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
* Sets the {@link ControlDispatcher}.
*
* @param controlDispatcher The {@link ControlDispatcher}, or null to use
* {@link PlaybackControlView#DEFAULT_CONTROL_DISPATCHER}.
* {@link DefaultControlDispatcher}.
*/
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) {
Assertions.checkState(controller != null);
controller.setControlDispatcher(controlDispatcher);
}