[go: nahoru, domu]

blob: 325cf141d144d66ab142812b25d9b7bd6970f6a1 [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 androidx.media2.common.BaseResult.RESULT_ERROR_BAD_VALUE;
import static androidx.media2.common.MediaMetadata.METADATA_KEY_DURATION;
import static androidx.media2.common.MediaMetadata.METADATA_KEY_MEDIA_ID;
import static androidx.media2.common.MediaMetadata.METADATA_KEY_PLAYABLE;
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_IDLE;
import static androidx.media2.common.SessionPlayer.UNKNOWN_TIME;
import static androidx.media2.session.MediaUtils.DIRECT_EXECUTOR;
import static androidx.media2.session.SessionResult.RESULT_ERROR_INVALID_STATE;
import static androidx.media2.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
import static androidx.media2.session.SessionResult.RESULT_ERROR_UNKNOWN;
import static androidx.media2.session.SessionResult.RESULT_INFO_SKIPPED;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.MediaSessionCompat.Token;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Surface;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.concurrent.ListenableFuture;
import androidx.concurrent.callback.AbstractResolvableFuture;
import androidx.concurrent.callback.ResolvableFuture;
import androidx.core.util.ObjectsCompat;
import androidx.media.AudioAttributesCompat;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media.VolumeProviderCompat;
import androidx.media2.common.BaseResult;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.SessionPlayer;
import androidx.media2.common.SessionPlayer.PlayerResult;
import androidx.media2.common.SessionPlayer.TrackInfo;
import androidx.media2.common.SubtitleData;
import androidx.media2.common.VideoSize;
import androidx.media2.session.MediaSession.ControllerCb;
import androidx.media2.session.MediaSession.ControllerInfo;
import androidx.media2.session.MediaSession.SessionCallback;
import androidx.media2.session.SequencedFutureManager.SequencedFuture;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
@SuppressLint("ObsoleteSdkInt") // TODO: Remove once the minSdkVersion is lowered enough.
class MediaSessionImplBase implements MediaSession.MediaSessionImpl {
private static final String DEFAULT_MEDIA_SESSION_TAG_PREFIX = "androidx.media2.session.id";
private static final String DEFAULT_MEDIA_SESSION_TAG_DELIM = ".";
private static final int ITEM_NONE = -1;
// Create a static lock for synchronize methods below.
// We'd better not use MediaSessionImplBase.class for synchronized(), which indirectly exposes
// lock object to the outside of the class.
private static final Object STATIC_LOCK = new Object();
@GuardedBy("STATIC_LOCK")
private static boolean sComponentNamesInitialized = false;
@GuardedBy("STATIC_LOCK")
private static ComponentName sServiceComponentName;
static final String TAG = "MSImplBase";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final SessionResult RESULT_WHEN_CLOSED = new SessionResult(RESULT_INFO_SKIPPED);
final Object mLock = new Object();
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Uri mSessionUri;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Executor mCallbackExecutor;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final SessionCallback mCallback;
private final Context mContext;
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private final MediaSessionCompat mSessionCompat;
private final MediaSessionStub mSessionStub;
private final MediaSessionLegacyStub mSessionLegacyStub;
private final String mSessionId;
private final SessionToken mSessionToken;
private final AudioManager mAudioManager;
private final SessionPlayer.PlayerCallback mPlayerCallback;
private final MediaSession mInstance;
private final PendingIntent mSessionActivity;
private final PendingIntent mMediaButtonIntent;
private final BroadcastReceiver mBroadcastReceiver;
@GuardedBy("mLock")
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaController.PlaybackInfo mPlaybackInfo;
@GuardedBy("mLock")
private SessionPlayer mPlayer;
@GuardedBy("mLock")
private MediaBrowserServiceCompat mBrowserServiceLegacyStub;
MediaSessionImplBase(MediaSession instance, Context context, String id, SessionPlayer player,
PendingIntent sessionActivity, Executor callbackExecutor, SessionCallback callback,
Bundle tokenExtras) {
mContext = context;
mInstance = instance;
mHandlerThread = new HandlerThread("MediaSession_Thread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mSessionStub = new MediaSessionStub(this);
mSessionActivity = sessionActivity;
mCallback = callback;
mCallbackExecutor = callbackExecutor;
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mPlayerCallback = new SessionPlayerCallback(this);
mSessionId = id;
// Build Uri that differentiate sessions across the creation/destruction in PendingIntent.
// Here's the reason why Session ID / SessionToken aren't suitable here.
// - Session ID
// PendingIntent from the previously closed session with the same ID can be sent to the
// newly created session.
// - SessionToken
// SessionToken is a Parcelable so we can only put it into the intent extra.
// However, creating two different PendingIntent that only differs extras isn't allowed.
// See {@link PendingIntent} and {@link Intent#filterEquals} for details.
mSessionUri = new Uri.Builder().scheme(MediaSessionImplBase.class.getName()).appendPath(id)
.appendPath(String.valueOf(SystemClock.elapsedRealtime())).build();
mSessionToken = new SessionToken(new SessionTokenImplBase(Process.myUid(),
SessionToken.TYPE_SESSION, context.getPackageName(), mSessionStub, tokenExtras));
String sessionCompatId = TextUtils.join(DEFAULT_MEDIA_SESSION_TAG_DELIM,
new String[] {DEFAULT_MEDIA_SESSION_TAG_PREFIX, id});
ComponentName mbrComponent = null;
synchronized (STATIC_LOCK) {
if (!sComponentNamesInitialized) {
sServiceComponentName = getServiceComponentByAction(
MediaLibraryService.SERVICE_INTERFACE);
if (sServiceComponentName == null) {
sServiceComponentName = getServiceComponentByAction(
MediaSessionService.SERVICE_INTERFACE);
}
sComponentNamesInitialized = true;
}
mbrComponent = sServiceComponentName;
}
if (mbrComponent == null) {
// No service to revive playback after it's dead.
// Create a PendingIntent that points to the runtime broadcast receiver.
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, mSessionUri);
intent.setPackage(context.getPackageName());
mMediaButtonIntent = PendingIntent.getBroadcast(
context, 0 /* requestCode */, intent, 0 /* flags */);
// Creates a dummy ComponentName for MediaSessionCompat in pre-L.
// TODO: Replace this with the MediaButtonReceiver class.
mbrComponent = new ComponentName(context, context.getClass());
// Create and register a BroadcastReceiver for receiving PendingIntent.
// TODO: Introduce MediaButtonReceiver in AndroidManifest instead of this,
// or register only one receiver for all sessions.
mBroadcastReceiver = new MediaButtonReceiver();
IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
filter.addDataScheme(mSessionUri.getScheme());
context.registerReceiver(mBroadcastReceiver, filter);
} else {
// Has MediaSessionService to revive playback after it's dead.
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, mSessionUri);
intent.setComponent(mbrComponent);
if (Build.VERSION.SDK_INT >= 26) {
mMediaButtonIntent = PendingIntent.getForegroundService(mContext, 0, intent, 0);
} else {
mMediaButtonIntent = PendingIntent.getService(mContext, 0, intent, 0);
}
mBroadcastReceiver = null;
}
mSessionCompat = new MediaSessionCompat(context, sessionCompatId, mbrComponent,
mMediaButtonIntent, mSessionToken.getExtras(), mSessionToken);
// NOTE: mSessionLegacyStub should be created after mSessionCompat created.
mSessionLegacyStub = new MediaSessionLegacyStub(this);
mSessionCompat.setSessionActivity(sessionActivity);
mSessionCompat.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
updatePlayer(player);
// Do followings at the last moment. Otherwise commands through framework would be sent to
// this session while initializing, and end up with unexpected situation.
mSessionCompat.setCallback(mSessionLegacyStub, mHandler);
mSessionCompat.setActive(true);
}
@Override
public void updatePlayer(@NonNull SessionPlayer player,
@Nullable SessionPlayer playlistAgent) {
// No-op
}
// TODO(jaewan): Remove SuppressLint when removing duplication session callback.
@Override
@SuppressLint("WrongConstant")
public void updatePlayer(@NonNull SessionPlayer player) {
final boolean isPlaybackInfoChanged;
final SessionPlayer oldPlayer;
final MediaController.PlaybackInfo info = createPlaybackInfo(player, null);
synchronized (mLock) {
isPlaybackInfoChanged = !info.equals(mPlaybackInfo);
oldPlayer = mPlayer;
mPlayer = player;
mPlaybackInfo = info;
if (oldPlayer != mPlayer) {
if (oldPlayer != null) {
oldPlayer.unregisterPlayerCallback(mPlayerCallback);
}
mPlayer.registerPlayerCallback(mCallbackExecutor, mPlayerCallback);
}
}
if (oldPlayer == null) {
// updatePlayerConnector() is called inside of the constructor.
// There's no connected controllers at this moment, so just initialize session compat's
// playback state. Otherwise, framework doesn't know whether this is ready to receive
// media key event.
mSessionCompat.setPlaybackState(createPlaybackStateCompat());
} else {
if (player != oldPlayer) {
final int state = getPlayerState();
mCallbackExecutor.execute(new Runnable() {
@Override
public void run() {
mCallback.onPlayerStateChanged(getInstance(), state);
}
});
notifyPlayerUpdatedNotLocked(oldPlayer);
}
if (isPlaybackInfoChanged) {
notifyPlaybackInfoChangedNotLocked(info);
}
}
if (player instanceof RemoteSessionPlayer) {
final RemoteSessionPlayer remotePlayer = (RemoteSessionPlayer) player;
VolumeProviderCompat volumeProvider =
new VolumeProviderCompat(remotePlayer.getVolumeControlType(),
remotePlayer.getMaxVolume(),
remotePlayer.getVolume()) {
@Override
public void onSetVolumeTo(int volume) {
remotePlayer.setVolume(volume);
}
@Override
public void onAdjustVolume(int direction) {
remotePlayer.adjustVolume(direction);
}
};
mSessionCompat.setPlaybackToRemote(volumeProvider);
} else {
int stream = getLegacyStreamType(player.getAudioAttributes());
mSessionCompat.setPlaybackToLocal(stream);
}
}
@NonNull
MediaController.PlaybackInfo createPlaybackInfo(@NonNull SessionPlayer player,
AudioAttributesCompat audioAttributes) {
final AudioAttributesCompat attrs = audioAttributes != null ? audioAttributes :
player.getAudioAttributes();
if (!(player instanceof RemoteSessionPlayer)) {
int stream = getLegacyStreamType(attrs);
int controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED;
}
return MediaController.PlaybackInfo.createPlaybackInfo(
MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
attrs,
controlType,
mAudioManager.getStreamMaxVolume(stream),
mAudioManager.getStreamVolume(stream));
} else {
RemoteSessionPlayer remotePlayer = (RemoteSessionPlayer) player;
return MediaController.PlaybackInfo.createPlaybackInfo(
MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
attrs,
remotePlayer.getVolumeControlType(),
remotePlayer.getMaxVolume(),
remotePlayer.getVolume());
}
}
private int getLegacyStreamType(@Nullable AudioAttributesCompat attrs) {
int stream;
if (attrs == null) {
stream = AudioManager.STREAM_MUSIC;
} else {
stream = attrs.getLegacyStreamType();
if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
// Usually, AudioAttributesCompat#getLegacyStreamType() does not return
// USE_DEFAULT_STREAM_TYPE unless the developer sets it with
// AudioAttributesCompat.Builder#setLegacyStreamType().
// But for safety, let's convert USE_DEFAULT_STREAM_TYPE to STREAM_MUSIC here.
stream = AudioManager.STREAM_MUSIC;
}
}
return stream;
}
@Override
public void close() {
synchronized (mLock) {
if (isClosed()) {
return;
}
if (DEBUG) {
Log.d(TAG, "Closing session, id=" + getId() + ", token="
+ getToken());
}
mPlayer.unregisterPlayerCallback(mPlayerCallback);
mSessionCompat.release();
mMediaButtonIntent.cancel();
if (mBroadcastReceiver != null) {
mContext.unregisterReceiver(mBroadcastReceiver);
}
mCallback.onSessionClosed(mInstance);
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onDisconnected(seq);
}
});
mHandler.removeCallbacksAndMessages(null);
if (mHandlerThread.isAlive()) {
if (Build.VERSION.SDK_INT >= 18) {
mHandlerThread.quitSafely();
} else {
mHandlerThread.quit();
}
}
}
}
@Override
public @NonNull SessionPlayer getPlayer() {
synchronized (mLock) {
return mPlayer;
}
}
@Override
public String getId() {
return mSessionId;
}
@Override
public Uri getUri() {
return mSessionUri;
}
@Override
public @NonNull SessionToken getToken() {
return mSessionToken;
}
@Override
@SuppressWarnings("unchecked")
public @NonNull List<ControllerInfo> getConnectedControllers() {
List<ControllerInfo> controllers = new ArrayList<>();
controllers.addAll(mSessionStub.getConnectedControllersManager()
.getConnectedControllers());
controllers.addAll(mSessionLegacyStub.getConnectedControllersManager()
.getConnectedControllers());
return controllers;
}
@Override
public boolean isConnected(ControllerInfo controller) {
if (controller == null) {
return false;
}
if (controller.equals(mSessionLegacyStub.getControllersForAll())) {
return true;
}
return mSessionStub.getConnectedControllersManager().isConnected(controller)
|| mSessionLegacyStub.getConnectedControllersManager().isConnected(controller);
}
@Override
public ListenableFuture<SessionResult> setCustomLayout(@NonNull ControllerInfo controller,
@NonNull final List<MediaSession.CommandButton> layout) {
return dispatchRemoteControllerTask(controller, new RemoteControllerTask() {
@Override
public void run(ControllerCb controller, int seq) throws RemoteException {
controller.setCustomLayout(seq, layout);
}
});
}
@Override
public void setAllowedCommands(@NonNull ControllerInfo controller,
@NonNull final SessionCommandGroup commands) {
if (mSessionStub.getConnectedControllersManager().isConnected(controller)) {
mSessionStub.getConnectedControllersManager()
.updateAllowedCommands(controller, commands);
dispatchRemoteControllerTaskWithoutReturn(controller, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onAllowedCommandsChanged(seq, commands);
}
});
} else {
mSessionLegacyStub.getConnectedControllersManager()
.updateAllowedCommands(controller, commands);
}
}
@Override
public void broadcastCustomCommand(@NonNull final SessionCommand command,
@Nullable final Bundle args) {
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb controller, int seq) throws RemoteException {
controller.sendCustomCommand(seq, command, args);
}
});
}
@Override
public ListenableFuture<SessionResult> sendCustomCommand(
@NonNull ControllerInfo controller, @NonNull final SessionCommand command,
@Nullable final Bundle args) {
return dispatchRemoteControllerTask(controller, new RemoteControllerTask() {
@Override
public void run(ControllerCb controller, int seq) throws RemoteException {
controller.sendCustomCommand(seq, command, args);
}
});
}
@Override
@SuppressWarnings("unchecked")
public ListenableFuture<PlayerResult> play() {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
if (player.getPlayerState() != PLAYER_STATE_IDLE) {
return player.play();
}
final ListenableFuture<PlayerResult> prepareFuture = player.prepare();
final ListenableFuture<PlayerResult> playFuture = player.play();
if (prepareFuture == null || playFuture == null) {
// Let dispatchPlayerTask() handle such cases.
return null;
}
return CombinedCommandResultFuture.create(
DIRECT_EXECUTOR, prepareFuture, playFuture);
}
});
}
@Override
public ListenableFuture<PlayerResult> pause() {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.pause();
}
});
}
@Override
public ListenableFuture<PlayerResult> prepare() {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.prepare();
}
});
}
@Override
public ListenableFuture<PlayerResult> seekTo(final long pos) {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.seekTo(pos);
}
});
}
@Override public @SessionPlayer.PlayerState int getPlayerState() {
return dispatchPlayerTask(new PlayerTask<Integer>() {
@Override
public Integer run(SessionPlayer player) throws Exception {
return player.getPlayerState();
}
}, SessionPlayer.PLAYER_STATE_ERROR);
}
@Override
public long getCurrentPosition() {
return dispatchPlayerTask(new PlayerTask<Long>() {
@Override
public Long run(SessionPlayer player) throws Exception {
if (isInPlaybackState(player)) {
return player.getCurrentPosition();
}
return null;
}
}, SessionPlayer.UNKNOWN_TIME);
}
@Override
public long getDuration() {
return dispatchPlayerTask(new PlayerTask<Long>() {
@Override
public Long run(SessionPlayer player) throws Exception {
if (isInPlaybackState(player)) {
return player.getDuration();
}
return null;
}
}, SessionPlayer.UNKNOWN_TIME);
}
@Override
public long getBufferedPosition() {
return dispatchPlayerTask(new PlayerTask<Long>() {
@Override
public Long run(SessionPlayer player) throws Exception {
if (isInPlaybackState(player)) {
return player.getBufferedPosition();
}
return null;
}
}, SessionPlayer.UNKNOWN_TIME);
}
@Override
public @SessionPlayer.BuffState int getBufferingState() {
return dispatchPlayerTask(new PlayerTask<Integer>() {
@Override
public Integer run(SessionPlayer player) throws Exception {
return player.getBufferingState();
}
}, SessionPlayer.BUFFERING_STATE_UNKNOWN);
}
@Override
public float getPlaybackSpeed() {
return dispatchPlayerTask(new PlayerTask<Float>() {
@Override
public Float run(SessionPlayer player) throws Exception {
if (isInPlaybackState(player)) {
return player.getPlaybackSpeed();
}
return null;
}
}, 1.0f);
}
@Override
public ListenableFuture<PlayerResult> setPlaybackSpeed(final float speed) {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.setPlaybackSpeed(speed);
}
});
}
@Override
public List<MediaItem> getPlaylist() {
return dispatchPlayerTask(new PlayerTask<List<MediaItem>>() {
@Override
public List<MediaItem> run(SessionPlayer player) throws Exception {
return player.getPlaylist();
}
}, null);
}
@Override
public ListenableFuture<PlayerResult> setPlaylist(final @NonNull List<MediaItem> list,
final @Nullable MediaMetadata metadata) {
if (list == null) {
throw new NullPointerException("list shouldn't be null");
}
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.setPlaylist(list, metadata);
}
});
}
@Override
public ListenableFuture<PlayerResult> setMediaItem(final @NonNull MediaItem item) {
if (item == null) {
throw new NullPointerException("item shouldn't be null");
}
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.setMediaItem(item);
}
});
}
@Override
public ListenableFuture<PlayerResult> skipToPlaylistItem(final int index) {
if (index < 0) {
throw new IllegalArgumentException("index shouldn't be negative");
}
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
final List<MediaItem> list = player.getPlaylist();
if (index >= list.size()) {
return PlayerResult.createFuture(RESULT_ERROR_BAD_VALUE);
}
return player.skipToPlaylistItem(index);
}
});
}
@Override
public ListenableFuture<PlayerResult> skipToPreviousItem() {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.skipToPreviousPlaylistItem();
}
});
}
@Override
public ListenableFuture<PlayerResult> skipToNextItem() {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.skipToNextPlaylistItem();
}
});
}
@Override
public MediaMetadata getPlaylistMetadata() {
return dispatchPlayerTask(new PlayerTask<MediaMetadata>() {
@Override
public MediaMetadata run(SessionPlayer player) throws Exception {
return player.getPlaylistMetadata();
}
}, null);
}
@Override
public ListenableFuture<PlayerResult> addPlaylistItem(final int index,
final @NonNull MediaItem item) {
if (index < 0) {
throw new IllegalArgumentException("index shouldn't be negative");
}
if (item == null) {
throw new NullPointerException("item shouldn't be null");
}
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.addPlaylistItem(index, item);
}
});
}
@Override
public ListenableFuture<PlayerResult> removePlaylistItem(final int index) {
if (index < 0) {
throw new IllegalArgumentException("index shouldn't be negative");
}
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
final List<MediaItem> list = player.getPlaylist();
if (index >= list.size()) {
return PlayerResult.createFuture(RESULT_ERROR_BAD_VALUE);
}
return player.removePlaylistItem(index);
}
});
}
@Override
public ListenableFuture<PlayerResult> replacePlaylistItem(final int index,
final @NonNull MediaItem item) {
if (index < 0) {
throw new IllegalArgumentException("index shouldn't be negative");
}
if (item == null) {
throw new NullPointerException("item shouldn't be null");
}
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.replacePlaylistItem(index, item);
}
});
}
@Override
public MediaItem getCurrentMediaItem() {
return dispatchPlayerTask(new PlayerTask<MediaItem>() {
@Override
public MediaItem run(SessionPlayer player) throws Exception {
return player.getCurrentMediaItem();
}
}, null);
}
@Override
public int getCurrentMediaItemIndex() {
return dispatchPlayerTask(new PlayerTask<Integer>() {
@Override
public Integer run(SessionPlayer player) throws Exception {
return player.getCurrentMediaItemIndex();
}
}, ITEM_NONE);
}
@Override
public int getPreviousMediaItemIndex() {
return dispatchPlayerTask(new PlayerTask<Integer>() {
@Override
public Integer run(SessionPlayer player) throws Exception {
return player.getPreviousMediaItemIndex();
}
}, ITEM_NONE);
}
@Override
public int getNextMediaItemIndex() {
return dispatchPlayerTask(new PlayerTask<Integer>() {
@Override
public Integer run(SessionPlayer player) throws Exception {
return player.getNextMediaItemIndex();
}
}, ITEM_NONE);
}
@Override
public ListenableFuture<PlayerResult> updatePlaylistMetadata(
final @Nullable MediaMetadata metadata) {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.updatePlaylistMetadata(metadata);
}
});
}
@Override
public @SessionPlayer.RepeatMode int getRepeatMode() {
return dispatchPlayerTask(new PlayerTask<Integer>() {
@Override
public Integer run(SessionPlayer player) throws Exception {
return player.getRepeatMode();
}
}, SessionPlayer.REPEAT_MODE_NONE);
}
@Override
public ListenableFuture<PlayerResult> setRepeatMode(
final @SessionPlayer.RepeatMode int repeatMode) {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.setRepeatMode(repeatMode);
}
});
}
@Override
public @SessionPlayer.ShuffleMode int getShuffleMode() {
return dispatchPlayerTask(new PlayerTask<Integer>() {
@Override
public Integer run(SessionPlayer player) throws Exception {
return player.getShuffleMode();
}
}, SessionPlayer.SHUFFLE_MODE_NONE);
}
@Override
public ListenableFuture<PlayerResult> setShuffleMode(
final @SessionPlayer.ShuffleMode int shuffleMode) {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.setShuffleMode(shuffleMode);
}
});
}
@Override
public VideoSize getVideoSize() {
return dispatchPlayerTask(new PlayerTask<VideoSize>() {
@Override
public VideoSize run(@NonNull SessionPlayer player) {
return player.getVideoSizeInternal();
}
}, new VideoSize(0, 0));
}
@Override
public ListenableFuture<PlayerResult> setSurface(final Surface surface) {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(@NonNull SessionPlayer player) {
return player.setSurfaceInternal(surface);
}
});
}
@Override
public List<TrackInfo> getTrackInfo() {
return dispatchPlayerTask(new PlayerTask<List<TrackInfo>>() {
@Override
public List<TrackInfo> run(SessionPlayer player) throws Exception {
return player.getTrackInfoInternal();
}
}, null);
}
@Override
public ListenableFuture<PlayerResult> selectTrack(final TrackInfo trackInfo) {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.selectTrackInternal(trackInfo);
}
});
}
@Override
public ListenableFuture<PlayerResult> deselectTrack(final TrackInfo trackInfo) {
return dispatchPlayerTask(new PlayerTask<ListenableFuture<PlayerResult>>() {
@Override
public ListenableFuture<PlayerResult> run(SessionPlayer player) throws Exception {
return player.deselectTrackInternal(trackInfo);
}
});
}
@Override
public TrackInfo getSelectedTrack(final int trackType) {
return dispatchPlayerTask(new PlayerTask<TrackInfo>() {
@Override
public TrackInfo run(SessionPlayer player) throws Exception {
return player.getSelectedTrackInternal(trackType);
}
}, null);
}
///////////////////////////////////////////////////
// package private and private methods
///////////////////////////////////////////////////
@Override
public @NonNull MediaSession getInstance() {
return mInstance;
}
@Override
public Context getContext() {
return mContext;
}
@Override
public Executor getCallbackExecutor() {
return mCallbackExecutor;
}
@Override
public SessionCallback getCallback() {
return mCallback;
}
@Override
public MediaSessionCompat getSessionCompat() {
return mSessionCompat;
}
@Override
public boolean isClosed() {
return !mHandlerThread.isAlive();
}
@Override
public PlaybackStateCompat createPlaybackStateCompat() {
synchronized (mLock) {
int state = MediaUtils.convertToPlaybackStateCompatState(getPlayerState(),
getBufferingState());
long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_FAST_FORWARD
| PlaybackStateCompat.ACTION_SET_RATING
| PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
| PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
| PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
| PlaybackStateCompat.ACTION_PLAY_FROM_URI | PlaybackStateCompat.ACTION_PREPARE
| PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
| PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
| PlaybackStateCompat.ACTION_PREPARE_FROM_URI
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
| PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
return new PlaybackStateCompat.Builder()
.setState(state, getCurrentPosition(), getPlaybackSpeed(),
SystemClock.elapsedRealtime())
.setActions(allActions)
.setBufferedPosition(getBufferedPosition())
.build();
}
}
@Override
public MediaController.PlaybackInfo getPlaybackInfo() {
synchronized (mLock) {
return mPlaybackInfo;
}
}
@Override
public PendingIntent getSessionActivity() {
return mSessionActivity;
}
MediaBrowserServiceCompat createLegacyBrowserService(Context context, SessionToken token,
Token sessionToken) {
return new MediaSessionServiceLegacyStub(context, this, sessionToken);
}
@Override
public void connectFromService(IMediaController caller, String packageName, int pid, int uid,
@Nullable Bundle connectionHints) {
mSessionStub.connect(caller, packageName, pid, uid, connectionHints);
}
/**
* Gets the service binder from the MediaBrowserServiceCompat. Should be only called by the
* thread with a Looper.
*
* @return
*/
@Override
public IBinder getLegacyBrowserServiceBinder() {
MediaBrowserServiceCompat legacyStub;
synchronized (mLock) {
if (mBrowserServiceLegacyStub == null) {
mBrowserServiceLegacyStub = createLegacyBrowserService(mContext, mSessionToken,
mSessionCompat.getSessionToken());
}
legacyStub = mBrowserServiceLegacyStub;
}
Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE);
return legacyStub.onBind(intent);
}
MediaBrowserServiceCompat getLegacyBrowserService() {
synchronized (mLock) {
return mBrowserServiceLegacyStub;
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean isInPlaybackState(@NonNull SessionPlayer player) {
return !isClosed()
&& player.getPlayerState() != SessionPlayer.PLAYER_STATE_IDLE
&& player.getPlayerState() != SessionPlayer.PLAYER_STATE_ERROR;
}
private @Nullable MediaItem getCurrentMediaItemOrNull() {
final SessionPlayer player;
synchronized (mLock) {
player = mPlayer;
}
return player != null ? player.getCurrentMediaItem() : null;
}
private @Nullable List<MediaItem> getPlaylistOrNull() {
final SessionPlayer player;
synchronized (mLock) {
player = mPlayer;
}
return player != null ? player.getPlaylist() : null;
}
private ListenableFuture<PlayerResult> dispatchPlayerTask(
@NonNull PlayerTask<ListenableFuture<PlayerResult>> command) {
ResolvableFuture<PlayerResult> result = ResolvableFuture.create();
result.set(new PlayerResult(RESULT_ERROR_INVALID_STATE, null));
return dispatchPlayerTask(command, result);
}
private <T> T dispatchPlayerTask(@NonNull PlayerTask<T> command, T defaultResult) {
final SessionPlayer player;
synchronized (mLock) {
player = mPlayer;
}
try {
if (!isClosed()) {
T result = command.run(player);
if (result != null) {
return result;
}
} else if (DEBUG) {
Log.d(TAG, "API calls after the close()", new IllegalStateException());
}
} catch (Exception e) {
}
return defaultResult;
}
// TODO(jaewan): Remove SuppressLint when removing duplication session callback.
@SuppressLint("WrongConstant")
private void notifyPlayerUpdatedNotLocked(SessionPlayer oldPlayer) {
// Tells the playlist change first, to current item can change be notified with an item
// within the playlist.
List<MediaItem> oldPlaylist = oldPlayer.getPlaylist();
final List<MediaItem> newPlaylist = getPlaylistOrNull();
if (!ObjectsCompat.equals(oldPlaylist, newPlaylist)) {
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaylistChanged(seq,
newPlaylist, getPlaylistMetadata(), getCurrentMediaItemIndex(),
getPreviousMediaItemIndex(), getNextMediaItemIndex());
}
});
} else {
MediaMetadata oldMetadata = oldPlayer.getPlaylistMetadata();
final MediaMetadata newMetadata = getPlaylistMetadata();
if (!ObjectsCompat.equals(oldMetadata, newMetadata)) {
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaylistMetadataChanged(seq, newMetadata);
}
});
}
}
MediaItem oldCurrentItem = oldPlayer.getCurrentMediaItem();
final MediaItem newCurrentItem = getCurrentMediaItemOrNull();
if (!ObjectsCompat.equals(oldCurrentItem, newCurrentItem)) {
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onCurrentMediaItemChanged(seq, newCurrentItem,
getCurrentMediaItemIndex(), getPreviousMediaItemIndex(),
getNextMediaItemIndex());
}
});
}
final @SessionPlayer.RepeatMode int repeatMode = getRepeatMode();
if (oldPlayer.getRepeatMode() != repeatMode) {
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onRepeatModeChanged(seq, repeatMode, getCurrentMediaItemIndex(),
getPreviousMediaItemIndex(), getNextMediaItemIndex());
}
});
}
final @SessionPlayer.ShuffleMode int shuffleMode = getShuffleMode();
if (oldPlayer.getShuffleMode() != shuffleMode) {
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onShuffleModeChanged(seq, shuffleMode, getCurrentMediaItemIndex(),
getPreviousMediaItemIndex(), getNextMediaItemIndex());
}
});
}
// Always forcefully send the player state and buffered state to send the current position
// and buffered position.
final long currentTimeMs = SystemClock.elapsedRealtime();
final long positionMs = getCurrentPosition();
final int playerState = getPlayerState();
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlayerStateChanged(seq, currentTimeMs, positionMs, playerState);
}
});
final MediaItem item = getCurrentMediaItemOrNull();
if (item != null) {
final int bufferingState = getBufferingState();
final long bufferedPositionMs = getBufferedPosition();
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onBufferingStateChanged(seq, item, bufferingState, bufferedPositionMs,
SystemClock.elapsedRealtime(), getCurrentPosition());
}
});
}
final float speed = getPlaybackSpeed();
if (speed != oldPlayer.getPlaybackSpeed()) {
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaybackSpeedChanged(seq, currentTimeMs, positionMs, speed);
}
});
}
// Note: AudioInfo is updated outside of this API.
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void notifyPlaybackInfoChangedNotLocked(final MediaController.PlaybackInfo info) {
dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaybackInfoChanged(seq, info);
}
});
}
void dispatchRemoteControllerTaskWithoutReturn(@NonNull RemoteControllerTask task) {
List<ControllerInfo> controllers =
mSessionStub.getConnectedControllersManager().getConnectedControllers();
controllers.add(mSessionLegacyStub.getControllersForAll());
for (int i = 0; i < controllers.size(); i++) {
ControllerInfo controller = controllers.get(i);
dispatchRemoteControllerTaskWithoutReturn(controller, task);
}
}
void dispatchRemoteControllerTaskWithoutReturn(@NonNull ControllerInfo controller,
@NonNull RemoteControllerTask task) {
if (!isConnected(controller)) {
// Do not send command to an unconnected controller.
return;
}
try {
final int seq;
final SequencedFutureManager manager =
mSessionStub.getConnectedControllersManager()
.getSequencedFutureManager(controller);
if (manager != null) {
seq = manager.obtainNextSequenceNumber();
} else {
// Can be null in two cases. Use the 0 as sequence number in both cases because
// Case 1) Controller is from the legacy stub
// -> Sequence number isn't needed, so 0 is OK
// Case 2) Controller is removed after the connection check above
// -> Call will fail below or ignored by the controller, so 0 is OK.
seq = 0;
}
task.run(controller.getControllerCb(), seq);
} catch (DeadObjectException e) {
onDeadObjectException(controller, e);
} catch (RemoteException e) {
// Currently it's TransactionTooLargeException or DeadSystemException.
// We'd better to leave log for those cases because
// - TransactionTooLargeException means that we may need to fix our code.
// (e.g. add pagination or special way to deliver Bitmap)
// - DeadSystemException means that errors around it can be ignored.
Log.w(TAG, "Exception in " + controller.toString(), e);
}
}
private ListenableFuture<SessionResult> dispatchRemoteControllerTask(
@NonNull ControllerInfo controller, @NonNull RemoteControllerTask task) {
if (!isConnected(controller)) {
return SessionResult.createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
}
try {
final ListenableFuture<SessionResult> future;
final int seq;
final SequencedFutureManager manager =
mSessionStub.getConnectedControllersManager()
.getSequencedFutureManager(controller);
if (manager != null) {
future = manager.createSequencedFuture(RESULT_WHEN_CLOSED);
seq = ((SequencedFuture<SessionResult>) future).getSequenceNumber();
} else {
// Can be null in two cases. Use the 0 as sequence number in both cases because
// Case 1) Controller is from the legacy stub
// -> Sequence number isn't needed, so 0 is OK
// Case 2) Controller is removed after the connection check above
// -> Call will fail below or ignored by the controller, so 0 is OK.
seq = 0;
future = SessionResult.createFutureWithResult(SessionResult.RESULT_SUCCESS);
}
task.run(controller.getControllerCb(), seq);
return future;
} catch (DeadObjectException e) {
onDeadObjectException(controller, e);
return SessionResult.createFutureWithResult(RESULT_ERROR_SESSION_DISCONNECTED);
} catch (RemoteException e) {
// Currently it's TransactionTooLargeException or DeadSystemException.
// We'd better to leave log for those cases because
// - TransactionTooLargeException means that we may need to fix our code.
// (e.g. add pagination or special way to deliver Bitmap)
// - DeadSystemException means that errors around it can be ignored.
Log.w(TAG, "Exception in " + controller.toString(), e);
}
return SessionResult.createFutureWithResult(RESULT_ERROR_UNKNOWN);
}
/**
* Removes controller. Call this when DeadObjectException is happened with binder call.
*/
private void onDeadObjectException(ControllerInfo controller, DeadObjectException e) {
if (DEBUG) {
Log.d(TAG, controller.toString() + " is gone", e);
}
// Note: Only removing from MediaSessionStub and ignoring (legacy) stubs would be fine for
// now. Because calls to the legacy stubs doesn't throw DeadObjectException.
mSessionStub.getConnectedControllersManager().removeController(controller);
}
@Nullable
private ComponentName getServiceComponentByAction(@NonNull String action) {
PackageManager pm = mContext.getPackageManager();
Intent queryIntent = new Intent(action);
queryIntent.setPackage(mContext.getPackageName());
List<ResolveInfo> resolveInfos = pm.queryIntentServices(queryIntent, 0 /* flags */);
if (resolveInfos == null || resolveInfos.isEmpty()) {
return null;
}
ResolveInfo resolveInfo = resolveInfos.get(0);
return new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
}
///////////////////////////////////////////////////
// Inner classes
///////////////////////////////////////////////////
@FunctionalInterface
interface PlayerTask<T> {
T run(@NonNull SessionPlayer player) throws Exception;
}
@FunctionalInterface
interface RemoteControllerTask {
void run(ControllerCb controller, int seq) throws RemoteException;
}
private static class SessionPlayerCallback extends SessionPlayer.PlayerCallback {
private final WeakReference<MediaSessionImplBase> mSession;
private MediaItem mMediaItem;
private List<MediaItem> mList;
private final CurrentMediaItemListener mCurrentItemChangedListener;
private final PlaylistItemListener mPlaylistItemChangedListener;
SessionPlayerCallback(MediaSessionImplBase session) {
mSession = new WeakReference<>(session);
mCurrentItemChangedListener = new CurrentMediaItemListener(session);
mPlaylistItemChangedListener = new PlaylistItemListener(session);
}
@Override
public void onCurrentMediaItemChanged(final SessionPlayer player, final MediaItem item) {
final MediaSessionImplBase session = getSession();
if (session == null || player == null || session.getPlayer() != player) {
return;
}
synchronized (session.mLock) {
if (mMediaItem != null) {
mMediaItem.removeOnMetadataChangedListener(mCurrentItemChangedListener);
}
if (item != null) {
item.addOnMetadataChangedListener(session.mCallbackExecutor,
mCurrentItemChangedListener);
}
mMediaItem = item;
}
// Note: No sanity check whether the item is in the playlist.
updateDurationIfNeeded(player, item);
session.dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onCurrentMediaItemChanged(seq, item,
session.getCurrentMediaItemIndex(), session.getPreviousMediaItemIndex(),
session.getNextMediaItemIndex());
}
});
}
@Override
public void onPlayerStateChanged(final SessionPlayer player, final int state) {
final MediaSessionImplBase session = getSession();
if (session == null || player == null || session.getPlayer() != player) {
return;
}
session.getCallback().onPlayerStateChanged(session.getInstance(), state);
updateDurationIfNeeded(player, player.getCurrentMediaItem());
session.dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlayerStateChanged(seq, SystemClock.elapsedRealtime(),
player.getCurrentPosition(), state);
}
});
}
@Override
public void onBufferingStateChanged(final SessionPlayer player,
final MediaItem item, final int state) {
updateDurationIfNeeded(player, item);
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onBufferingStateChanged(seq, item, state, player.getBufferedPosition(),
SystemClock.elapsedRealtime(), player.getCurrentPosition());
}
});
}
@Override
public void onPlaybackSpeedChanged(final SessionPlayer player, final float speed) {
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaybackSpeedChanged(seq, SystemClock.elapsedRealtime(),
player.getCurrentPosition(), speed);
}
});
}
@Override
public void onSeekCompleted(final SessionPlayer player, final long position) {
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onSeekCompleted(seq, SystemClock.elapsedRealtime(),
player.getCurrentPosition(), position);
}
});
}
@Override
public void onPlaylistChanged(final SessionPlayer player, final List<MediaItem> list,
final MediaMetadata metadata) {
final MediaSessionImplBase session = getSession();
if (session == null || player == null || session.getPlayer() != player) {
return;
}
synchronized (session.mLock) {
if (mList != null) {
for (int i = 0; i < mList.size(); i++) {
mList.get(i).removeOnMetadataChangedListener(mPlaylistItemChangedListener);
}
}
if (list != null) {
for (int i = 0; i < list.size(); i++) {
list.get(i).addOnMetadataChangedListener(session.mCallbackExecutor,
mPlaylistItemChangedListener);
}
}
mList = list;
}
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaylistChanged(seq, list, metadata,
session.getCurrentMediaItemIndex(), session.getPreviousMediaItemIndex(),
session.getNextMediaItemIndex());
}
});
}
@Override
public void onPlaylistMetadataChanged(final SessionPlayer player,
final MediaMetadata metadata) {
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaylistMetadataChanged(seq, metadata);
}
});
}
@Override
public void onRepeatModeChanged(final SessionPlayer player, final int repeatMode) {
final MediaSessionImplBase session = getSession();
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onRepeatModeChanged(seq, repeatMode,
session.getCurrentMediaItemIndex(),
session.getPreviousMediaItemIndex(),
session.getNextMediaItemIndex());
}
});
}
@Override
public void onShuffleModeChanged(final SessionPlayer player, final int shuffleMode) {
final MediaSessionImplBase session = getSession();
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onShuffleModeChanged(seq, shuffleMode,
session.getCurrentMediaItemIndex(),
session.getPreviousMediaItemIndex(),
session.getNextMediaItemIndex());
}
});
}
@Override
public void onPlaybackCompleted(SessionPlayer player) {
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaybackCompleted(seq);
}
});
}
@Override
public void onAudioAttributesChanged(final SessionPlayer player,
final AudioAttributesCompat attributes) {
final MediaSessionImplBase session = getSession();
if (session == null || player == null || session.getPlayer() != player) {
return;
}
MediaController.PlaybackInfo newInfo = session.createPlaybackInfo(player, attributes);
MediaController.PlaybackInfo oldInfo;
synchronized (session.mLock) {
oldInfo = session.mPlaybackInfo;
session.mPlaybackInfo = newInfo;
}
if (!ObjectsCompat.equals(newInfo, oldInfo)) {
session.notifyPlaybackInfoChangedNotLocked(newInfo);
}
}
@Override
public void onVideoSizeChangedInternal(final @NonNull SessionPlayer player,
final @NonNull MediaItem item, final @NonNull VideoSize videoSize) {
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onVideoSizeChanged(seq, item, videoSize);
}
});
}
@Override
public void onTrackInfoChanged(SessionPlayer player, final List<TrackInfo> trackInfos) {
final MediaSessionImplBase session = getSession();
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onTrackInfoChanged(seq, trackInfos,
session.getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_VIDEO),
session.getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_AUDIO),
session.getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE),
session.getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_METADATA));
}
});
}
@Override
public void onTrackSelected(SessionPlayer player, final TrackInfo trackInfo) {
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onTrackSelected(seq, trackInfo);
}
});
}
@Override
public void onTrackDeselected(SessionPlayer player, final TrackInfo trackInfo) {
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onTrackDeselected(seq, trackInfo);
}
});
}
@Override
public void onSubtitleData(final @NonNull SessionPlayer player,
final @NonNull MediaItem item, final @NonNull TrackInfo track,
final @NonNull SubtitleData data) {
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onSubtitleData(seq, item, track, data);
}
});
}
private MediaSessionImplBase getSession() {
final MediaSessionImplBase session = mSession.get();
if (session == null && DEBUG) {
Log.d(TAG, "Session is closed", new IllegalStateException());
}
return session;
}
private void dispatchRemoteControllerTask(@NonNull SessionPlayer player,
@NonNull RemoteControllerTask task) {
final MediaSessionImplBase session = getSession();
if (session == null || player == null || session.getPlayer() != player) {
return;
}
session.dispatchRemoteControllerTaskWithoutReturn(task);
}
private void updateDurationIfNeeded(@NonNull final SessionPlayer player,
@Nullable final MediaItem item) {
if (item == null) {
return;
}
if (!item.equals(player.getCurrentMediaItem())) {
return;
}
final long duration = player.getDuration();
if (duration <= 0 || duration == UNKNOWN_TIME) {
return;
}
MediaMetadata metadata = item.getMetadata();
if (metadata != null) {
if (!metadata.containsKey(METADATA_KEY_DURATION)) {
metadata = new MediaMetadata.Builder(metadata).putLong(
METADATA_KEY_DURATION, duration).build();
} else {
long durationFromMetadata =
metadata.getLong(METADATA_KEY_DURATION);
if (duration == durationFromMetadata) {
return;
}
// Warns developers about the mismatch. Don't log media item here to keep
// metadata secure.
Log.w(TAG, "duration mismatch for an item."
+ " duration from player=" + duration
+ " duration from metadata=" + durationFromMetadata
+ ". May be a timing issue?");
// Trust duration in the metadata set by developer.
// In theory, duration may differ if the current item has been
// changed before the getDuration(). So it's better not touch
// duration set by developer.
}
} else {
metadata = new MediaMetadata.Builder()
.putLong(METADATA_KEY_DURATION, duration)
.putString(METADATA_KEY_MEDIA_ID, item.getMediaId())
.putLong(METADATA_KEY_PLAYABLE, 1)
.build();
}
if (metadata != null) {
final MediaSessionImplBase session = getSession();
item.setMetadata(metadata);
dispatchRemoteControllerTask(player, new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaylistChanged(seq,
player.getPlaylist(), player.getPlaylistMetadata(),
session.getCurrentMediaItemIndex(),
session.getPreviousMediaItemIndex(),
session.getNextMediaItemIndex());
}
});
}
}
}
static class CurrentMediaItemListener implements MediaItem.OnMetadataChangedListener {
private final WeakReference<MediaSessionImplBase> mSession;
CurrentMediaItemListener(MediaSessionImplBase session) {
mSession = new WeakReference<>(session);
}
@Override
public void onMetadataChanged(final MediaItem item) {
final MediaSessionImplBase session = mSession.get();
if (session == null || item == null) {
return;
}
final MediaItem currentItem = session.getCurrentMediaItem();
if (currentItem != null && item.equals(currentItem)) {
session.dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onCurrentMediaItemChanged(seq, item,
session.getCurrentMediaItemIndex(),
session.getPreviousMediaItemIndex(),
session.getNextMediaItemIndex());
}
});
}
}
}
static class PlaylistItemListener implements MediaItem.OnMetadataChangedListener {
private final WeakReference<MediaSessionImplBase> mSession;
PlaylistItemListener(MediaSessionImplBase session) {
mSession = new WeakReference<>(session);
}
@Override
public void onMetadataChanged(final MediaItem item) {
final MediaSessionImplBase session = mSession.get();
if (session == null || item == null) {
return;
}
final List<MediaItem> list = session.getPlaylist();
if (list == null) {
return;
}
for (int i = 0; i < list.size(); i++) {
if (item.equals(list.get(i))) {
session.dispatchRemoteControllerTaskWithoutReturn(new RemoteControllerTask() {
@Override
public void run(ControllerCb callback, int seq) throws RemoteException {
callback.onPlaylistChanged(seq, list,
session.getPlaylistMetadata(),
session.getCurrentMediaItemIndex(),
session.getPreviousMediaItemIndex(),
session.getNextMediaItemIndex());
}
});
return;
}
}
}
}
static final class CombinedCommandResultFuture<T extends BaseResult>
extends AbstractResolvableFuture<T> {
final ListenableFuture<T>[] mFutures;
AtomicInteger mSuccessCount = new AtomicInteger(0);
@SuppressWarnings("unchecked")
public static <U extends BaseResult> CombinedCommandResultFuture create(
Executor executor, ListenableFuture<U>... futures) {
return new CombinedCommandResultFuture<U>(executor, futures);
}
private CombinedCommandResultFuture(Executor executor,
ListenableFuture<T>[] futures) {
mFutures = futures;
for (int i = 0; i < mFutures.length; ++i) {
final int cur = i;
mFutures[i].addListener(new Runnable() {
@Override
public void run() {
try {
T result = mFutures[cur].get();
int resultCode = result.getResultCode();
if (resultCode != SessionResult.RESULT_SUCCESS
&& resultCode != RESULT_INFO_SKIPPED) {
for (int j = 0; j < mFutures.length; ++j) {
if (!mFutures[j].isCancelled() && !mFutures[j].isDone()
&& cur != j) {
mFutures[j].cancel(true);
}
}
set(result);
} else {
int cnt = mSuccessCount.incrementAndGet();
if (cnt == mFutures.length) {
set(result);
}
}
} catch (Exception e) {
for (int j = 0; j < mFutures.length; ++j) {
if (!mFutures[j].isCancelled() && !mFutures[j].isDone()
&& cur != j) {
mFutures[j].cancel(true);
}
}
setException(e);
}
}
}, executor);
}
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
final class MediaButtonReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
return;
}
Uri sessionUri = intent.getData();
if (!ObjectsCompat.equals(sessionUri, mSessionUri)) {
return;
}
KeyEvent keyEvent = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (keyEvent == null) {
return;
}
getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
}
};
}