[go: nahoru, domu]

blob: b56e4b367e229dca42822552b8e5c8f95acc07a9 [file] [log] [blame]
/*
* Copyright 2019 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 androidx.media2.session;
import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_ID;
import static androidx.media2.common.BaseResult.RESULT_ERROR_BAD_VALUE;
import static androidx.media2.common.SessionPlayer.BUFFERING_STATE_UNKNOWN;
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_IDLE;
import static androidx.media2.common.SessionPlayer.UNKNOWN_TIME;
import static androidx.media2.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED;
import static androidx.media2.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
import static androidx.media2.session.SessionResult.RESULT_SUCCESS;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.ResultReceiver;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.MediaSessionCompat.QueueItem;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.Surface;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.util.ObjectsCompat;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.Rating;
import androidx.media2.common.SessionPlayer;
import androidx.media2.common.SessionPlayer.BuffState;
import androidx.media2.common.SessionPlayer.RepeatMode;
import androidx.media2.common.SessionPlayer.ShuffleMode;
import androidx.media2.common.SessionPlayer.TrackInfo;
import androidx.media2.common.VideoSize;
import androidx.media2.session.MediaController.ControllerCallback;
import androidx.media2.session.MediaController.ControllerCallbackRunnable;
import androidx.media2.session.MediaController.PlaybackInfo;
import androidx.media2.session.MediaController.VolumeDirection;
import androidx.media2.session.MediaController.VolumeFlags;
import androidx.media2.session.MediaSession.CommandButton;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// TODO: Find better way to return listenable future.
@SuppressLint("ObsoleteSdkInt") // TODO: Remove once the minSdkVersion is lowered enough.
class MediaControllerImplLegacy implements MediaController.MediaControllerImpl {
private static final String TAG = "MC2ImplLegacy";
private static final int ITEM_NONE = -1;
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final long POSITION_DIFF_TOLERANCE = 100;
static final String SESSION_COMMAND_ON_EXTRAS_CHANGED =
"android.media.session.command.ON_EXTRAS_CHANGED";
static final String SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED =
"android.media.session.command.ON_CAPTIONING_ENALBED_CHANGED";
final Context mContext;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final SessionToken mToken;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final HandlerThread mHandlerThread;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Handler mHandler;
final Object mLock = new Object();
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaController mInstance;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaBrowserCompat mBrowserCompat;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mClosed;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
List<MediaItem> mPlaylist;
@SuppressWarnings("WeakerAccess") /* synthetic access */
List<QueueItem> mQueue;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaMetadata mPlaylistMetadata;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
@RepeatMode int mRepeatMode;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
@ShuffleMode int mShuffleMode;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mPlayerState;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaItem mCurrentMediaItem;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mBufferingState;
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mCurrentMediaItemIndex;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mSkipToPlaylistIndex = -1;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
long mBufferedPosition;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
PlaybackInfo mPlaybackInfo;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
SessionCommandGroup mAllowedCommands;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
List<CommandButton> mCustomLayout;
// Media 1.0 variables
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaControllerCompat mControllerCompat;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
ControllerCompatCallback mControllerCompatCallback;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
PlaybackStateCompat mPlaybackStateCompat;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaMetadataCompat mMediaMetadataCompat;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mConnected;
MediaControllerImplLegacy(@NonNull Context context, @NonNull MediaController instance,
@NonNull SessionToken token) {
mContext = context;
mInstance = instance;
mHandlerThread = new HandlerThread("MediaController_Thread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mToken = token;
if (mToken.getType() == SessionToken.TYPE_SESSION) {
synchronized (mLock) {
mBrowserCompat = null;
}
connectToSession((MediaSessionCompat.Token) mToken.getBinder());
} else {
connectToService();
}
}
@Override
public void close() {
if (DEBUG) {
Log.d(TAG, "close from " + mToken);
}
synchronized (mLock) {
if (mClosed) {
// Prevent re-enterance from the ControllerCallback.onDisconnected()
return;
}
mHandler.removeCallbacksAndMessages(null);
if (Build.VERSION.SDK_INT >= 18) {
mHandlerThread.quitSafely();
} else {
mHandlerThread.quit();
}
mClosed = true;
if (mBrowserCompat != null) {
mBrowserCompat.disconnect();
mBrowserCompat = null;
}
if (mControllerCompat != null) {
mControllerCompat.unregisterCallback(mControllerCompatCallback);
mControllerCompat = null;
}
mConnected = false;
}
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onDisconnected(mInstance);
}
});
}
@Override
@Nullable
public SessionToken getConnectedToken() {
synchronized (mLock) {
return mConnected ? mToken : null;
}
}
private ListenableFuture<SessionResult> createFutureWithResult(int resultCode) {
final MediaItem item;
synchronized (mLock) {
item = mCurrentMediaItem;
}
ResolvableFuture<SessionResult> result = ResolvableFuture.create();
result.set(new SessionResult(resultCode, null, item));
return result;
}
@Override
public boolean isConnected() {
synchronized (mLock) {
return mConnected;
}
}
@Override
public ListenableFuture<SessionResult> play() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().play();
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> pause() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().pause();
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> prepare() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().prepare();
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> fastForward() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().fastForward();
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> rewind() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().rewind();
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> skipForward() {
// Unsupported action
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
public ListenableFuture<SessionResult> skipBackward() {
// Unsupported action
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
public ListenableFuture<SessionResult> seekTo(long pos) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().seekTo(pos);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> playFromMediaId(@NonNull String mediaId,
@Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().playFromMediaId(mediaId, extras);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> playFromSearch(@NonNull String query,
@Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().playFromSearch(query, extras);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> playFromUri(@NonNull Uri uri,
@Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().playFromUri(uri, extras);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> prepareFromMediaId(@NonNull String mediaId,
@Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().prepareFromMediaId(mediaId, extras);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> prepareFromSearch(@NonNull String query,
@Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().prepareFromSearch(query, extras);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> prepareFromUri(@NonNull Uri uri,
@Nullable Bundle extras) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().prepareFromUri(uri, extras);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> setVolumeTo(int value, @VolumeFlags int flags) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.setVolumeTo(value, flags);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> adjustVolume(@VolumeDirection int direction,
@VolumeFlags int flags) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.adjustVolume(direction, flags);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
@Nullable
public PendingIntent getSessionActivity() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return null;
}
return mControllerCompat.getSessionActivity();
}
}
@Override
public int getPlayerState() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return SessionPlayer.PLAYER_STATE_ERROR;
}
return mPlayerState;
}
}
@Override
public long getDuration() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return UNKNOWN_TIME;
}
if (mMediaMetadataCompat != null
&& mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) {
return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION);
}
}
return UNKNOWN_TIME;
}
@Override
public long getCurrentPosition() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return UNKNOWN_TIME;
}
if (mPlaybackStateCompat != null) {
return mPlaybackStateCompat.getCurrentPosition(mInstance.mTimeDiff);
}
return UNKNOWN_TIME;
}
}
@Override
public float getPlaybackSpeed() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return 0f;
}
return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed();
}
}
@Override
@BuffState
public int getBufferingState() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return BUFFERING_STATE_UNKNOWN;
}
return mPlaybackStateCompat == null ? SessionPlayer.BUFFERING_STATE_UNKNOWN
: MediaUtils.toBufferingState(mPlaybackStateCompat.getState());
}
}
@Override
public long getBufferedPosition() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return UNKNOWN_TIME;
}
return (mPlaybackStateCompat == null) ? UNKNOWN_TIME
: mPlaybackStateCompat.getBufferedPosition();
}
}
@Override
@Nullable
public PlaybackInfo getPlaybackInfo() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return null;
}
return mPlaybackInfo;
}
}
@Override
public ListenableFuture<SessionResult> setRating(@NonNull String mediaId,
@NonNull Rating rating) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
if (mCurrentMediaItem != null && mediaId.equals(mCurrentMediaItem.getMediaId())) {
mControllerCompat.getTransportControls().setRating(
MediaUtils.convertToRatingCompat(rating));
}
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> setPlaybackSpeed(float speed) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().setPlaybackSpeed(speed);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> sendCustomCommand(@NonNull SessionCommand command,
@Nullable Bundle args) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
if (mAllowedCommands.hasCommand(command)) {
mControllerCompat.getTransportControls().sendCustomAction(
command.getCustomAction(), args);
return createFutureWithResult(RESULT_SUCCESS);
}
final ResolvableFuture<SessionResult> result = ResolvableFuture.create();
ResultReceiver cb = new ResultReceiver(mHandler) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
result.set(new SessionResult(resultCode, resultData));
}
};
mControllerCompat.sendCommand(command.getCustomAction(), args, cb);
return result;
}
}
@Override
@Nullable
public List<MediaItem> getPlaylist() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return null;
}
return (mPlaylist == null || mPlaylist.size() == 0) ? null : new ArrayList<>(mPlaylist);
}
}
@Override
public ListenableFuture<SessionResult> setPlaylist(@NonNull List<String> list,
@Nullable MediaMetadata metadata) {
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
public ListenableFuture<SessionResult> setMediaItem(@NonNull String mediaId) {
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
public ListenableFuture<SessionResult> setMediaUri(@NonNull Uri uri,
@Nullable Bundle extras) {
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
public ListenableFuture<SessionResult> updatePlaylistMetadata(
@Nullable MediaMetadata metadata) {
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
@Nullable
public MediaMetadata getPlaylistMetadata() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return null;
}
return mPlaylistMetadata;
}
}
@Override
public ListenableFuture<SessionResult> addPlaylistItem(int index, @NonNull String mediaId) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.addQueueItem(
MediaUtils.createMediaDescriptionCompat(mediaId), index);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> removePlaylistItem(int index) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
if (mQueue == null || index < 0 || index >= mQueue.size()) {
return createFutureWithResult(RESULT_ERROR_BAD_VALUE);
}
mControllerCompat.removeQueueItem(mQueue.get(index).getDescription());
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> replacePlaylistItem(int index, @NonNull String mediaId) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
if (mQueue == null || index < 0 || index >= mQueue.size()) {
return createFutureWithResult(RESULT_ERROR_BAD_VALUE);
}
mControllerCompat.removeQueueItem(mQueue.get(index).getDescription());
mControllerCompat.addQueueItem(MediaUtils.createMediaDescriptionCompat(mediaId), index);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> movePlaylistItem(int fromIndex, int toIndex) {
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
public MediaItem getCurrentMediaItem() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return null;
}
return mCurrentMediaItem;
}
}
@Override
public int getCurrentMediaItemIndex() {
return mCurrentMediaItemIndex;
}
@Override
public int getPreviousMediaItemIndex() {
return ITEM_NONE;
}
@Override
public int getNextMediaItemIndex() {
return ITEM_NONE;
}
@Override
public ListenableFuture<SessionResult> skipToPreviousItem() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().skipToPrevious();
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> skipToNextItem() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mControllerCompat.getTransportControls().skipToNext();
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
public ListenableFuture<SessionResult> skipToPlaylistItem(int index) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
mSkipToPlaylistIndex = index;
mControllerCompat.getTransportControls().skipToQueueItem(
mQueue.get(index).getQueueId());
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
@RepeatMode
public int getRepeatMode() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return SessionPlayer.REPEAT_MODE_NONE;
}
return mRepeatMode;
}
}
@Override
public ListenableFuture<SessionResult> setRepeatMode(@RepeatMode int repeatMode) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
// SessionPlayer.RepeatMode has the same values with
// PlaybackStateCompat.RepeatMode.
mControllerCompat.getTransportControls().setRepeatMode(repeatMode);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
@ShuffleMode
public int getShuffleMode() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return SessionPlayer.SHUFFLE_MODE_NONE;
}
return mShuffleMode;
}
}
@Override
public ListenableFuture<SessionResult> setShuffleMode(@ShuffleMode int shuffleMode) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
// SessionPlayer.ShuffleMode has the same values with
// PlaybackStateCompat.ShuffleMode.
mControllerCompat.getTransportControls().setShuffleMode(shuffleMode);
}
return createFutureWithResult(RESULT_SUCCESS);
}
@Override
@NonNull
public VideoSize getVideoSize() {
Log.w(TAG, "Session doesn't support getting VideoSize");
return new VideoSize(0, 0);
}
@Override
public ListenableFuture<SessionResult> setSurface(@Nullable Surface surface) {
Log.w(TAG, "Session doesn't support setting Surface");
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
@NonNull
public List<TrackInfo> getTracks() {
Log.w(TAG, "Session doesn't support getting TrackInfo");
return Collections.emptyList();
}
@Override
@NonNull
public ListenableFuture<SessionResult> selectTrack(@NonNull TrackInfo trackInfo) {
Log.w(TAG, "Session doesn't support selecting track");
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
@NonNull
public ListenableFuture<SessionResult> deselectTrack(
@NonNull TrackInfo trackInfo) {
Log.w(TAG, "Session doesn't support deselecting track");
return createFutureWithResult(RESULT_ERROR_NOT_SUPPORTED);
}
@Override
@Nullable
public TrackInfo getSelectedTrack(int trackType) {
Log.w(TAG, "Session doesn't support getting selected track");
return null;
}
@Override
public SessionCommandGroup getAllowedCommands() {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
return null;
}
return mAllowedCommands;
}
}
@Override
@NonNull
public Context getContext() {
return mContext;
}
@Override
@Nullable
public MediaBrowserCompat getBrowserCompat() {
synchronized (mLock) {
return mBrowserCompat;
}
}
// Should be used without a lock to prevent potential deadlock.
void onConnectedNotLocked() {
if (DEBUG) {
Log.d(TAG, "onConnectedNotLocked token=" + mToken);
}
final SessionCommandGroup allowedCommands;
final List<CommandButton> customLayout;
synchronized (mLock) {
if (mClosed || mConnected) {
return;
}
mPlaybackStateCompat = mControllerCompat.getPlaybackState();
mAllowedCommands = MediaUtils.convertToSessionCommandGroup(
mControllerCompat.getFlags(), mPlaybackStateCompat);
mPlayerState = MediaUtils.convertToPlayerState(mPlaybackStateCompat);
mBufferedPosition = mPlaybackStateCompat == null
? UNKNOWN_TIME : mPlaybackStateCompat.getBufferedPosition();
mCustomLayout = MediaUtils.convertToCustomLayout(mPlaybackStateCompat);
allowedCommands = mAllowedCommands;
customLayout = mCustomLayout;
mPlaybackInfo = MediaUtils.toPlaybackInfo2(mControllerCompat.getPlaybackInfo());
mRepeatMode = mControllerCompat.getRepeatMode();
mShuffleMode = mControllerCompat.getShuffleMode();
mQueue = MediaUtils.removeNullElements(mControllerCompat.getQueue());
if (mQueue == null || mQueue.size() == 0) {
// MediaSessionCompat can set queue as null or empty. However, SessionPlayer should
// not set playlist as null or empty. Therefore, we treat them as the same.
mQueue = null;
mPlaylist = null;
} else {
mPlaylist = MediaUtils.convertQueueItemListToMediaItemList(mQueue);
}
mPlaylistMetadata = MediaUtils.convertToMediaMetadata(
mControllerCompat.getQueueTitle());
// Call this after set playlist.
setCurrentMediaItemLocked(mControllerCompat.getMetadata());
mConnected = true;
}
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onConnected(mInstance, allowedCommands);
}
});
if (!customLayout.isEmpty()) {
mInstance.notifyPrimaryControllerCallback(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onSetCustomLayout(mInstance, customLayout);
}
});
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void connectToSession(MediaSessionCompat.Token sessionCompatToken) {
boolean isSessionReady;
MediaControllerCompat controllerCompat = new MediaControllerCompat(mContext,
sessionCompatToken);
synchronized (mLock) {
mControllerCompat = controllerCompat;
mControllerCompatCallback = new ControllerCompatCallback();
isSessionReady = mControllerCompat.isSessionReady();
mControllerCompat.registerCallback(mControllerCompatCallback, mHandler);
}
if (!isSessionReady) {
// If the session not ready here, then call onConnectedNotLocked() immediately. The
// session would be higly likely framework MediaSession, so wouldn't be ready forever.
// Just FYI, previous attempt to make session ready (i.e. request extra binder) had been
// failed already in MediaController's constructor via SessionToken#createSessionToken.
onConnectedNotLocked();
}
}
private void connectToService() {
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mLock) {
mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(),
new ConnectionCallback(), null);
mBrowserCompat.connect();
}
}
});
}
@SuppressWarnings({"GuardedBy", "WeakerAccess"}) /* WeakerAccess for synthetic access */
void setCurrentMediaItemLocked(MediaMetadataCompat metadata) {
mMediaMetadataCompat = metadata;
if (metadata == null) {
mCurrentMediaItemIndex = -1;
mCurrentMediaItem = null;
return;
}
if (mQueue == null) {
mCurrentMediaItemIndex = -1;
mCurrentMediaItem = MediaUtils.convertToMediaItem(metadata);
return;
}
if (mPlaybackStateCompat != null) {
// If playback state is updated before, compare use queue id and media id.
long queueId = mPlaybackStateCompat.getActiveQueueItemId();
for (int i = 0; i < mQueue.size(); ++i) {
QueueItem item = mQueue.get(i);
if (item.getQueueId() == queueId) {
mCurrentMediaItem = MediaUtils.convertToMediaItem(metadata);
mCurrentMediaItemIndex = i;
return;
}
}
}
String mediaId = metadata.getString(METADATA_KEY_MEDIA_ID);
if (mediaId == null) {
mCurrentMediaItemIndex = -1;
mCurrentMediaItem = MediaUtils.convertToMediaItem(metadata);
return;
}
// Need to find the media item in the playlist using mediaId.
// Note that there can be multiple media items with the same media id.
if (mSkipToPlaylistIndex >= 0 && mSkipToPlaylistIndex < mQueue.size()
&& TextUtils.equals(mediaId,
mQueue.get(mSkipToPlaylistIndex).getDescription().getMediaId())) {
// metadata changed after skipToPlaylistIItem() was called.
mCurrentMediaItem = MediaUtils.convertToMediaItem(metadata);
mCurrentMediaItemIndex = mSkipToPlaylistIndex;
mSkipToPlaylistIndex = -1;
return;
}
// Find mediaId from the playlist.
for (int i = 0; i < mQueue.size(); ++i) {
QueueItem item = mQueue.get(i);
if (TextUtils.equals(mediaId, item.getDescription().getMediaId())) {
mCurrentMediaItemIndex = i;
mCurrentMediaItem = MediaUtils.convertToMediaItem(metadata);
return;
}
}
// Failed to find media item from the playlist.
mCurrentMediaItemIndex = -1;
mCurrentMediaItem = MediaUtils.convertToMediaItem(mMediaMetadataCompat);
}
private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
ConnectionCallback() {
}
@Override
public void onConnected() {
MediaBrowserCompat browser = getBrowserCompat();
if (browser != null) {
connectToSession(browser.getSessionToken());
} else if (DEBUG) {
Log.d(TAG, "Controller is closed prematually", new IllegalStateException());
}
}
@Override
public void onConnectionSuspended() {
close();
}
@Override
public void onConnectionFailed() {
close();
}
}
private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
ControllerCompatCallback() {
}
@Override
public void onSessionReady() {
boolean connected;
synchronized (mLock) {
connected = mConnected;
}
if (!connected) {
onConnectedNotLocked();
} else {
// Handle rare occasion that extra binder becomes available lately.
// See connectToSession() for detail.
PlaybackStateCompat state;
int shuffleMode;
int repeatMode;
boolean isCaptioningEnabled;
synchronized (mLock) {
state = mControllerCompat.getPlaybackState();
shuffleMode = mControllerCompat.getShuffleMode();
repeatMode = mControllerCompat.getRepeatMode();
isCaptioningEnabled = mControllerCompat.isCaptioningEnabled();
}
onPlaybackStateChanged(state);
onShuffleModeChanged(shuffleMode);
onRepeatModeChanged(repeatMode);
onCaptioningEnabledChanged(isCaptioningEnabled);
}
}
@Override
public void onSessionDestroyed() {
close();
}
@Override
public void onSessionEvent(final String event, final Bundle extras) {
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
}
mInstance.notifyPrimaryControllerCallback(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
// Ignore return because legacy session cannot get result back.
callback.onCustomCommand(mInstance, new SessionCommand(event, null), extras);
}
});
}
@Override
public void onPlaybackStateChanged(final PlaybackStateCompat state) {
final PlaybackStateCompat prevState;
final MediaItem prevItem;
final MediaItem currentItem;
final List<CommandButton> prevLayout;
final List<CommandButton> currentLayout;
final SessionCommandGroup prevAllowedCommands;
final SessionCommandGroup currentAllowedCommands;
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
prevItem = mCurrentMediaItem;
prevState = mPlaybackStateCompat;
mPlaybackStateCompat = state;
mPlayerState = MediaUtils.convertToPlayerState(state);
mBufferedPosition = state == null ? UNKNOWN_TIME
: state.getBufferedPosition();
if (mQueue != null && state != null) {
for (int i = 0; i < mQueue.size(); ++i) {
if (mQueue.get(i).getQueueId() == state.getActiveQueueItemId()) {
mCurrentMediaItemIndex = i;
mCurrentMediaItem = mPlaylist.get(i);
}
}
}
currentItem = mCurrentMediaItem;
prevLayout = mCustomLayout;
mCustomLayout = MediaUtils.convertToCustomLayout(state);
currentLayout = mCustomLayout;
prevAllowedCommands = mAllowedCommands;
mAllowedCommands = MediaUtils.convertToSessionCommandGroup(
mControllerCompat.getFlags(), mPlaybackStateCompat);
currentAllowedCommands = mAllowedCommands;
}
if (prevItem != currentItem) {
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onCurrentMediaItemChanged(mInstance, currentItem);
}
});
}
if (state == null) {
if (prevState != null) {
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onPlayerStateChanged(mInstance, PLAYER_STATE_IDLE);
}
});
}
return;
}
if (prevState == null || prevState.getState() != state.getState()) {
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onPlayerStateChanged(
mInstance, MediaUtils.convertToPlayerState(state));
}
});
}
if (prevState == null || prevState.getPlaybackSpeed() != state.getPlaybackSpeed()) {
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onPlaybackSpeedChanged(mInstance, state.getPlaybackSpeed());
}
});
}
if (prevState != null) {
final long currentPosition = state.getCurrentPosition(mInstance.mTimeDiff);
long positionDiff = Math.abs(currentPosition
- prevState.getCurrentPosition(mInstance.mTimeDiff));
if (positionDiff > POSITION_DIFF_TOLERANCE) {
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onSeekCompleted(mInstance, currentPosition);
}
});
}
}
if (!prevAllowedCommands.equals(currentAllowedCommands)) {
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onAllowedCommandsChanged(mInstance, currentAllowedCommands);
}
});
}
boolean layoutChanged;
if (prevLayout.size() == currentLayout.size()) {
layoutChanged = false;
for (int i = 0; i < currentLayout.size(); i++) {
if (!ObjectsCompat.equals(prevLayout.get(i).getCommand(),
currentLayout.get(i).getCommand())) {
layoutChanged = true;
break;
}
}
} else {
layoutChanged = true;
}
if (layoutChanged) {
mInstance.notifyPrimaryControllerCallback(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onSetCustomLayout(mInstance, currentLayout);
}
});
}
if (currentItem == null) {
return;
}
// Update buffering state if needed
final int bufferingState = MediaUtils.toBufferingState(state.getState());
final int prevBufferingState = prevState == null
? SessionPlayer.BUFFERING_STATE_UNKNOWN
: MediaUtils.toBufferingState(prevState.getState());
if (bufferingState != prevBufferingState) {
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onBufferingStateChanged(mInstance, currentItem, bufferingState);
}
});
}
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
final MediaItem prevItem;
final MediaItem currentItem;
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
prevItem = mCurrentMediaItem;
setCurrentMediaItemLocked(metadata);
currentItem = mCurrentMediaItem;
}
if (prevItem != currentItem) {
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onCurrentMediaItemChanged(mInstance, currentItem);
}
});
}
}
@Override
public void onQueueChanged(List<QueueItem> queue) {
final List<MediaItem> playlist;
final MediaMetadata playlistMetadata;
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
mQueue = MediaUtils.removeNullElements(queue);
if (mQueue == null || mQueue.size() == 0) {
// MediaSessionCompat can set queue as null or empty. However, SessionPlayer
// should not set playlist as null or empty. Therefore, we treat them as the
// same.
mQueue = null;
mPlaylist = null;
} else {
mPlaylist = MediaUtils.convertQueueItemListToMediaItemList(mQueue);
}
playlist = mPlaylist;
playlistMetadata = mPlaylistMetadata;
}
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onPlaylistChanged(mInstance, playlist, playlistMetadata);
}
});
}
@Override
public void onQueueTitleChanged(CharSequence title) {
final MediaMetadata playlistMetadata;
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
mPlaylistMetadata = MediaUtils.convertToMediaMetadata(title);
playlistMetadata = mPlaylistMetadata;
}
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onPlaylistMetadataChanged(mInstance, playlistMetadata);
}
});
}
@Override
public void onExtrasChanged(final Bundle extras) {
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
}
mInstance.notifyPrimaryControllerCallback(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onCustomCommand(mInstance, new SessionCommand(
SESSION_COMMAND_ON_EXTRAS_CHANGED, null), extras);
}
});
}
@Override
public void onAudioInfoChanged(final MediaControllerCompat.PlaybackInfo infoCompat) {
final PlaybackInfo info = MediaUtils.toPlaybackInfo2(infoCompat);
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
mPlaybackInfo = info;
}
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onPlaybackInfoChanged(mInstance, info);
}
});
}
@Override
public void onCaptioningEnabledChanged(final boolean enabled) {
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
}
mInstance.notifyPrimaryControllerCallback(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
Bundle args = new Bundle();
args.putBoolean(MediaConstants.ARGUMENT_CAPTIONING_ENABLED, enabled);
callback.onCustomCommand(mInstance, new SessionCommand(
SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED, null), args);
}
});
}
@Override
public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode final int repeatMode) {
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
mRepeatMode = repeatMode;
}
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onRepeatModeChanged(mInstance, repeatMode);
}
});
}
@Override
public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode final int shuffleMode) {
synchronized (mLock) {
if (mClosed || !mConnected) {
return;
}
mShuffleMode = shuffleMode;
}
mInstance.notifyAllControllerCallbacks(new ControllerCallbackRunnable() {
@Override
public void run(@NonNull ControllerCallback callback) {
callback.onShuffleModeChanged(mInstance, shuffleMode);
}
});
}
}
}