/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.media; import static android.media.SessionCommand2.COMMAND_CODE_SET_VOLUME; import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM; import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM; import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM; import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST; import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA; import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE; import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE; import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID; import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH; import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI; import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID; import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH; import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.media.AudioAttributes; import android.media.MediaController2; import android.media.MediaController2.ControllerCallback; import android.media.MediaController2.PlaybackInfo; import android.media.MediaItem2; import android.media.MediaMetadata2; import android.media.MediaPlaylistAgent.RepeatMode; import android.media.MediaPlaylistAgent.ShuffleMode; import android.media.SessionCommand2; import android.media.MediaSession2.CommandButton; import android.media.SessionCommandGroup2; import android.media.MediaSessionService2; import android.media.Rating2; import android.media.SessionToken2; import android.media.update.MediaController2Provider; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; import android.support.annotation.GuardedBy; import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; public class MediaController2Impl implements MediaController2Provider { private static final String TAG = "MediaController2"; private static final boolean DEBUG = true; // TODO(jaewan): Change private final MediaController2 mInstance; private final Context mContext; private final Object mLock = new Object(); private final MediaController2Stub mControllerStub; private final SessionToken2 mToken; private final ControllerCallback mCallback; private final Executor mCallbackExecutor; private final IBinder.DeathRecipient mDeathRecipient; @GuardedBy("mLock") private SessionServiceConnection mServiceConnection; @GuardedBy("mLock") private boolean mIsReleased; @GuardedBy("mLock") private List mPlaylist; @GuardedBy("mLock") private MediaMetadata2 mPlaylistMetadata; @GuardedBy("mLock") private @RepeatMode int mRepeatMode; @GuardedBy("mLock") private @ShuffleMode int mShuffleMode; @GuardedBy("mLock") private int mPlayerState; @GuardedBy("mLock") private long mPositionEventTimeMs; @GuardedBy("mLock") private long mPositionMs; @GuardedBy("mLock") private float mPlaybackSpeed; @GuardedBy("mLock") private long mBufferedPositionMs; @GuardedBy("mLock") private PlaybackInfo mPlaybackInfo; @GuardedBy("mLock") private PendingIntent mSessionActivity; @GuardedBy("mLock") private SessionCommandGroup2 mAllowedCommands; // Assignment should be used with the lock hold, but should be used without a lock to prevent // potential deadlock. // Postfix -Binder is added to explicitly show that it's potentially remote process call. // Technically -Interface is more correct, but it may misread that it's interface (vs class) // so let's keep this postfix until we find better postfix. @GuardedBy("mLock") private volatile IMediaSession2 mSessionBinder; // TODO(jaewan): Require session activeness changed listener, because controller can be // available when the session's player is null. public MediaController2Impl(Context context, MediaController2 instance, SessionToken2 token, Executor executor, ControllerCallback callback) { mInstance = instance; if (context == null) { throw new IllegalArgumentException("context shouldn't be null"); } if (token == null) { throw new IllegalArgumentException("token shouldn't be null"); } if (callback == null) { throw new IllegalArgumentException("callback shouldn't be null"); } if (executor == null) { throw new IllegalArgumentException("executor shouldn't be null"); } mContext = context; mControllerStub = new MediaController2Stub(this); mToken = token; mCallback = callback; mCallbackExecutor = executor; mDeathRecipient = () -> { mInstance.close(); }; mSessionBinder = null; } @Override public void initialize() { // TODO(jaewan): More sanity checks. if (mToken.getType() == SessionToken2.TYPE_SESSION) { // Session mServiceConnection = null; connectToSession(SessionToken2Impl.from(mToken).getSessionBinder()); } else { // Session service if (Process.myUid() == Process.SYSTEM_UID) { // It's system server (MediaSessionService) that wants to monitor session. // Don't bind if able.. IMediaSession2 binder = SessionToken2Impl.from(mToken).getSessionBinder(); if (binder != null) { // Use binder in the session token instead of bind by its own. // Otherwise server will holds the binding to the service *forever* and service // will never stop. mServiceConnection = null; connectToSession(SessionToken2Impl.from(mToken).getSessionBinder()); return; } else if (DEBUG) { // Should happen only when system server wants to dispatch media key events to // a dead service. Log.d(TAG, "System server binds to a session service. Should unbind" + " immediately after the use."); } } mServiceConnection = new SessionServiceConnection(); connectToService(); } } private void connectToService() { // Service. Needs to get fresh binder whenever connection is needed. SessionToken2Impl impl = SessionToken2Impl.from(mToken); final Intent intent = new Intent(MediaSessionService2.SERVICE_INTERFACE); intent.setClassName(mToken.getPackageName(), impl.getServiceName()); // Use bindService() instead of startForegroundService() to start session service for three // reasons. // 1. Prevent session service owner's stopSelf() from destroying service. // With the startForegroundService(), service's call of stopSelf() will trigger immediate // onDestroy() calls on the main thread even when onConnect() is running in another // thread. // 2. Minimize APIs for developers to take care about. // With bindService(), developers only need to take care about Service.onBind() // but Service.onStartCommand() should be also taken care about with the // startForegroundService(). // 3. Future support for UI-less playback // If a service wants to keep running, it should be either foreground service or // bounded service. But there had been request for the feature for system apps // and using bindService() will be better fit with it. boolean result; if (Process.myUid() == Process.SYSTEM_UID) { // Use bindServiceAsUser() for binding from system service to avoid following warning. // ContextImpl: Calling a method in the system process without a qualified user result = mContext.bindServiceAsUser(intent, mServiceConnection, Context.BIND_AUTO_CREATE, UserHandle.getUserHandleForUid(mToken.getUid())); } else { result = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); } if (!result) { Log.w(TAG, "bind to " + mToken + " failed"); } else if (DEBUG) { Log.d(TAG, "bind to " + mToken + " success"); } } private void connectToSession(IMediaSession2 sessionBinder) { try { sessionBinder.connect(mControllerStub, mContext.getPackageName()); } catch (RemoteException e) { Log.w(TAG, "Failed to call connection request. Framework will retry" + " automatically"); } } @Override public void close_impl() { if (DEBUG) { Log.d(TAG, "release from " + mToken); } final IMediaSession2 binder; synchronized (mLock) { if (mIsReleased) { // Prevent re-enterance from the ControllerCallback.onDisconnected() return; } mIsReleased = true; if (mServiceConnection != null) { mContext.unbindService(mServiceConnection); mServiceConnection = null; } binder = mSessionBinder; mSessionBinder = null; mControllerStub.destroy(); } if (binder != null) { try { binder.asBinder().unlinkToDeath(mDeathRecipient, 0); binder.release(mControllerStub); } catch (RemoteException e) { // No-op. } } mCallbackExecutor.execute(() -> { mCallback.onDisconnected(mInstance); }); } IMediaSession2 getSessionBinder() { return mSessionBinder; } MediaController2Stub getControllerStub() { return mControllerStub; } Executor getCallbackExecutor() { return mCallbackExecutor; } Context getContext() { return mContext; } MediaController2 getInstance() { return mInstance; } // Returns session binder if the controller can send the command. IMediaSession2 getSessionBinderIfAble(int commandCode) { synchronized (mLock) { if (!mAllowedCommands.hasCommand(commandCode)) { // Cannot send because isn't allowed to. Log.w(TAG, "Controller isn't allowed to call command, commandCode=" + commandCode); return null; } } // TODO(jaewan): Should we do this with the lock hold? final IMediaSession2 binder = mSessionBinder; if (binder == null) { // Cannot send because disconnected. Log.w(TAG, "Session is disconnected"); } return binder; } // Returns session binder if the controller can send the command. IMediaSession2 getSessionBinderIfAble(SessionCommand2 command) { synchronized (mLock) { if (!mAllowedCommands.hasCommand(command)) { Log.w(TAG, "Controller isn't allowed to call command, command=" + command); return null; } } // TODO(jaewan): Should we do this with the lock hold? final IMediaSession2 binder = mSessionBinder; if (binder == null) { // Cannot send because disconnected. Log.w(TAG, "Session is disconnected"); } return binder; } @Override public SessionToken2 getSessionToken_impl() { return mToken; } @Override public boolean isConnected_impl() { final IMediaSession2 binder = mSessionBinder; return binder != null; } @Override public void play_impl() { sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY); } @Override public void pause_impl() { sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE); } @Override public void stop_impl() { sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_STOP); } @Override public void skipToPlaylistItem_impl(MediaItem2 item) { if (item == null) { throw new IllegalArgumentException("item shouldn't be null"); } final IMediaSession2 binder = mSessionBinder; if (binder != null) { try { binder.skipToPlaylistItem(mControllerStub, item.toBundle()); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public void skipToPreviousItem_impl() { final IMediaSession2 binder = mSessionBinder; if (binder != null) { try { binder.skipToPreviousItem(mControllerStub); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public void skipToNextItem_impl() { final IMediaSession2 binder = mSessionBinder; if (binder != null) { try { binder.skipToNextItem(mControllerStub); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } private void sendTransportControlCommand(int commandCode) { sendTransportControlCommand(commandCode, null); } private void sendTransportControlCommand(int commandCode, Bundle args) { final IMediaSession2 binder = mSessionBinder; if (binder != null) { try { binder.sendTransportControlCommand(mControllerStub, commandCode, args); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public PendingIntent getSessionActivity_impl() { return mSessionActivity; } @Override public void setVolumeTo_impl(int value, int flags) { // TODO(hdmoon): sanity check final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SET_VOLUME); if (binder != null) { try { binder.setVolumeTo(mControllerStub, value, flags); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public void adjustVolume_impl(int direction, int flags) { // TODO(hdmoon): sanity check final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SET_VOLUME); if (binder != null) { try { binder.adjustVolume(mControllerStub, direction, flags); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public void prepareFromUri_impl(Uri uri, Bundle extras) { final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SESSION_PREPARE_FROM_URI); if (uri == null) { throw new IllegalArgumentException("uri shouldn't be null"); } if (binder != null) { try { binder.prepareFromUri(mControllerStub, uri, extras); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { // TODO(jaewan): Handle. } } @Override public void prepareFromSearch_impl(String query, Bundle extras) { final IMediaSession2 binder = getSessionBinderIfAble( COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH); if (TextUtils.isEmpty(query)) { throw new IllegalArgumentException("query shouldn't be empty"); } if (binder != null) { try { binder.prepareFromSearch(mControllerStub, query, extras); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { // TODO(jaewan): Handle. } } @Override public void prepareFromMediaId_impl(String mediaId, Bundle extras) { final IMediaSession2 binder = getSessionBinderIfAble( COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID); if (mediaId == null) { throw new IllegalArgumentException("mediaId shouldn't be null"); } if (binder != null) { try { binder.prepareFromMediaId(mControllerStub, mediaId, extras); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { // TODO(jaewan): Handle. } } @Override public void playFromUri_impl(Uri uri, Bundle extras) { final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SESSION_PLAY_FROM_URI); if (uri == null) { throw new IllegalArgumentException("uri shouldn't be null"); } if (binder != null) { try { binder.playFromUri(mControllerStub, uri, extras); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { // TODO(jaewan): Handle. } } @Override public void playFromSearch_impl(String query, Bundle extras) { final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH); if (TextUtils.isEmpty(query)) { throw new IllegalArgumentException("query shouldn't be empty"); } if (binder != null) { try { binder.playFromSearch(mControllerStub, query, extras); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { // TODO(jaewan): Handle. } } @Override public void playFromMediaId_impl(String mediaId, Bundle extras) { final IMediaSession2 binder = getSessionBinderIfAble( COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID); if (mediaId == null) { throw new IllegalArgumentException("mediaId shouldn't be null"); } if (binder != null) { try { binder.playFromMediaId(mControllerStub, mediaId, extras); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { // TODO(jaewan): Handle. } } @Override public void setRating_impl(String mediaId, Rating2 rating) { if (mediaId == null) { throw new IllegalArgumentException("mediaId shouldn't be null"); } if (rating == null) { throw new IllegalArgumentException("rating shouldn't be null"); } final IMediaSession2 binder = mSessionBinder; if (binder != null) { try { binder.setRating(mControllerStub, mediaId, rating.toBundle()); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { // TODO(jaewan): Handle. } } @Override public void sendCustomCommand_impl(SessionCommand2 command, Bundle args, ResultReceiver cb) { if (command == null) { throw new IllegalArgumentException("command shouldn't be null"); } final IMediaSession2 binder = getSessionBinderIfAble(command); if (binder != null) { try { binder.sendCustomCommand(mControllerStub, command.toBundle(), args, cb); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public List getPlaylist_impl() { synchronized (mLock) { return mPlaylist; } } @Override public void setPlaylist_impl(List list, MediaMetadata2 metadata) { if (list == null) { throw new IllegalArgumentException("list shouldn't be null"); } final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_SET_LIST); if (binder != null) { List bundleList = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { bundleList.add(list.get(i).toBundle()); } Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle(); try { binder.setPlaylist(mControllerStub, bundleList, metadataBundle); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public MediaMetadata2 getPlaylistMetadata_impl() { synchronized (mLock) { return mPlaylistMetadata; } } @Override public void updatePlaylistMetadata_impl(MediaMetadata2 metadata) { final IMediaSession2 binder = getSessionBinderIfAble( COMMAND_CODE_PLAYLIST_SET_LIST_METADATA); if (binder != null) { Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle(); try { binder.updatePlaylistMetadata(mControllerStub, metadataBundle); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public void prepare_impl() { sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE); } @Override public void fastForward_impl() { // TODO(jaewan): Implement this. Note that fast forward isn't a transport command anymore //sendTransportControlCommand(MediaSession2.COMMAND_CODE_SESSION_FAST_FORWARD); } @Override public void rewind_impl() { // TODO(jaewan): Implement this. Note that rewind isn't a transport command anymore //sendTransportControlCommand(MediaSession2.COMMAND_CODE_SESSION_REWIND); } @Override public void seekTo_impl(long pos) { if (pos < 0) { throw new IllegalArgumentException("position shouldn't be negative"); } Bundle args = new Bundle(); args.putLong(MediaSession2Stub.ARGUMENT_KEY_POSITION, pos); sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO, args); } @Override public void addPlaylistItem_impl(int index, MediaItem2 item) { if (index < 0) { throw new IllegalArgumentException("index shouldn't be negative"); } if (item == null) { throw new IllegalArgumentException("item shouldn't be null"); } final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_ADD_ITEM); if (binder != null) { try { binder.addPlaylistItem(mControllerStub, index, item.toBundle()); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public void removePlaylistItem_impl(MediaItem2 item) { if (item == null) { throw new IllegalArgumentException("item shouldn't be null"); } final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_REMOVE_ITEM); if (binder != null) { try { binder.removePlaylistItem(mControllerStub, item.toBundle()); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public void replacePlaylistItem_impl(int index, MediaItem2 item) { if (index < 0) { throw new IllegalArgumentException("index shouldn't be negative"); } if (item == null) { throw new IllegalArgumentException("item shouldn't be null"); } final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_REPLACE_ITEM); if (binder != null) { try { binder.replacePlaylistItem(mControllerStub, index, item.toBundle()); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public int getShuffleMode_impl() { return mShuffleMode; } @Override public void setShuffleMode_impl(int shuffleMode) { final IMediaSession2 binder = getSessionBinderIfAble( COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE); if (binder != null) { try { binder.setShuffleMode(mControllerStub, shuffleMode); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public int getRepeatMode_impl() { return mRepeatMode; } @Override public void setRepeatMode_impl(int repeatMode) { final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE); if (binder != null) { try { binder.setRepeatMode(mControllerStub, repeatMode); } catch (RemoteException e) { Log.w(TAG, "Cannot connect to the service or the session is gone", e); } } else { Log.w(TAG, "Session isn't active", new IllegalStateException()); } } @Override public PlaybackInfo getPlaybackInfo_impl() { synchronized (mLock) { return mPlaybackInfo; } } @Override public int getPlayerState_impl() { synchronized (mLock) { return mPlayerState; } } @Override public long getCurrentPosition_impl() { synchronized (mLock) { long timeDiff = System.currentTimeMillis() - mPositionEventTimeMs; long expectedPosition = mPositionMs + (long) (mPlaybackSpeed * timeDiff); return Math.max(0, expectedPosition); } } @Override public float getPlaybackSpeed_impl() { synchronized (mLock) { return mPlaybackSpeed; } } @Override public long getBufferedPosition_impl() { synchronized (mLock) { return mBufferedPositionMs; } } @Override public MediaItem2 getCurrentMediaItem_impl() { // TODO(jaewan): Implement return null; } void pushPlayerStateChanges(final int state) { synchronized (mLock) { mPlayerState = state; } mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } mCallback.onPlayerStateChanged(mInstance, state); }); } // TODO(jaewan): Rename to seek completed void pushPositionChanges(final long eventTimeMs, final long positionMs) { synchronized (mLock) { mPositionEventTimeMs = eventTimeMs; mPositionMs = positionMs; } mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } mCallback.onSeekCompleted(mInstance, positionMs); }); } void pushPlaybackSpeedChanges(final float speed) { synchronized (mLock) { mPlaybackSpeed = speed; } mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } mCallback.onPlaybackSpeedChanged(mInstance, speed); }); } void pushBufferedPositionChanges(final long bufferedPositionMs) { synchronized (mLock) { mBufferedPositionMs = bufferedPositionMs; } mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } // TODO(jaewan): Fix this -- it's now buffered state //mCallback.onBufferedPositionChanged(mInstance, bufferedPositionMs); }); } void pushPlaybackInfoChanges(final PlaybackInfo info) { synchronized (mLock) { mPlaybackInfo = info; } mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } mCallback.onPlaybackInfoChanged(mInstance, info); }); } void pushPlaylistChanges(final List playlist, final MediaMetadata2 metadata) { synchronized (mLock) { mPlaylist = playlist; mPlaylistMetadata = metadata; } mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } mCallback.onPlaylistChanged(mInstance, playlist, metadata); }); } void pushPlaylistMetadataChanges(MediaMetadata2 metadata) { synchronized (mLock) { mPlaylistMetadata = metadata; } mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } mCallback.onPlaylistMetadataChanged(mInstance, metadata); }); } void pushShuffleModeChanges(int shuffleMode) { synchronized (mLock) { mShuffleMode = shuffleMode; } mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } mCallback.onShuffleModeChanged(mInstance, shuffleMode); }); } void pushRepeatModeChanges(int repeatMode) { synchronized (mLock) { mRepeatMode = repeatMode; } mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } mCallback.onRepeatModeChanged(mInstance, repeatMode); }); } void pushError(int errorCode, Bundle extras) { mCallbackExecutor.execute(() -> { if (!mInstance.isConnected()) { return; } mCallback.onError(mInstance, errorCode, extras); }); } // Should be used without a lock to prevent potential deadlock. void onConnectedNotLocked(IMediaSession2 sessionBinder, final SessionCommandGroup2 allowedCommands, final int playerState, final long positionEventTimeMs, final long positionMs, final float playbackSpeed, final long bufferedPositionMs, final PlaybackInfo info, final int repeatMode, final int shuffleMode, final List playlist, final PendingIntent sessionActivity) { if (DEBUG) { Log.d(TAG, "onConnectedNotLocked sessionBinder=" + sessionBinder + ", allowedCommands=" + allowedCommands); } boolean close = false; try { if (sessionBinder == null || allowedCommands == null) { // Connection rejected. close = true; return; } synchronized (mLock) { if (mIsReleased) { return; } if (mSessionBinder != null) { Log.e(TAG, "Cannot be notified about the connection result many times." + " Probably a bug or malicious app."); close = true; return; } mAllowedCommands = allowedCommands; mPlayerState = playerState; mPositionEventTimeMs = positionEventTimeMs; mPositionMs = positionMs; mPlaybackSpeed = playbackSpeed; mBufferedPositionMs = bufferedPositionMs; mPlaybackInfo = info; mRepeatMode = repeatMode; mShuffleMode = shuffleMode; mPlaylist = playlist; mSessionActivity = sessionActivity; mSessionBinder = sessionBinder; try { // Implementation for the local binder is no-op, // so can be used without worrying about deadlock. mSessionBinder.asBinder().linkToDeath(mDeathRecipient, 0); } catch (RemoteException e) { if (DEBUG) { Log.d(TAG, "Session died too early.", e); } close = true; return; } } // TODO(jaewan): Keep commands to prevents illegal API calls. mCallbackExecutor.execute(() -> { // Note: We may trigger ControllerCallbacks with the initial values // But it's hard to define the order of the controller callbacks // Only notify about the mCallback.onConnected(mInstance, allowedCommands); }); } finally { if (close) { // Trick to call release() without holding the lock, to prevent potential deadlock // with the developer's custom lock within the ControllerCallback.onDisconnected(). mInstance.close(); } } } void onCustomCommand(final SessionCommand2 command, final Bundle args, final ResultReceiver receiver) { if (DEBUG) { Log.d(TAG, "onCustomCommand cmd=" + command); } mCallbackExecutor.execute(() -> { // TODO(jaewan): Double check if the controller exists. mCallback.onCustomCommand(mInstance, command, args, receiver); }); } void onAllowedCommandsChanged(final SessionCommandGroup2 commands) { mCallbackExecutor.execute(() -> { mCallback.onAllowedCommandsChanged(mInstance, commands); }); } void onCustomLayoutChanged(final List layout) { mCallbackExecutor.execute(() -> { mCallback.onCustomLayoutChanged(mInstance, layout); }); } // This will be called on the main thread. private class SessionServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { // Note that it's always main-thread. if (DEBUG) { Log.d(TAG, "onServiceConnected " + name + " " + this); } // Sanity check if (!mToken.getPackageName().equals(name.getPackageName())) { Log.wtf(TAG, name + " was connected, but expected pkg=" + mToken.getPackageName() + " with id=" + mToken.getId()); return; } final IMediaSession2 sessionBinder = IMediaSession2.Stub.asInterface(service); connectToSession(sessionBinder); } @Override public void onServiceDisconnected(ComponentName name) { // Temporal lose of the binding because of the service crash. System will automatically // rebind, so just no-op. // TODO(jaewan): Really? Either disconnect cleanly or if (DEBUG) { Log.w(TAG, "Session service " + name + " is disconnected."); } } @Override public void onBindingDied(ComponentName name) { // Permanent lose of the binding because of the service package update or removed. // This SessionServiceRecord will be removed accordingly, but forget session binder here // for sure. mInstance.close(); } } public static final class PlaybackInfoImpl implements PlaybackInfoProvider { private static final String KEY_PLAYBACK_TYPE = "android.media.playbackinfo_impl.playback_type"; private static final String KEY_CONTROL_TYPE = "android.media.playbackinfo_impl.control_type"; private static final String KEY_MAX_VOLUME = "android.media.playbackinfo_impl.max_volume"; private static final String KEY_CURRENT_VOLUME = "android.media.playbackinfo_impl.current_volume"; private static final String KEY_AUDIO_ATTRIBUTES = "android.media.playbackinfo_impl.audio_attrs"; private final PlaybackInfo mInstance; private final int mPlaybackType; private final int mControlType; private final int mMaxVolume; private final int mCurrentVolume; private final AudioAttributes mAudioAttrs; private PlaybackInfoImpl(int playbackType, AudioAttributes attrs, int controlType, int max, int current) { mPlaybackType = playbackType; mAudioAttrs = attrs; mControlType = controlType; mMaxVolume = max; mCurrentVolume = current; mInstance = new PlaybackInfo(this); } @Override public int getPlaybackType_impl() { return mPlaybackType; } @Override public AudioAttributes getAudioAttributes_impl() { return mAudioAttrs; } @Override public int getControlType_impl() { return mControlType; } @Override public int getMaxVolume_impl() { return mMaxVolume; } @Override public int getCurrentVolume_impl() { return mCurrentVolume; } PlaybackInfo getInstance() { return mInstance; } Bundle toBundle() { Bundle bundle = new Bundle(); bundle.putInt(KEY_PLAYBACK_TYPE, mPlaybackType); bundle.putInt(KEY_CONTROL_TYPE, mControlType); bundle.putInt(KEY_MAX_VOLUME, mMaxVolume); bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume); bundle.putParcelable(KEY_AUDIO_ATTRIBUTES, mAudioAttrs); return bundle; } static PlaybackInfo createPlaybackInfo(int playbackType, AudioAttributes attrs, int controlType, int max, int current) { return new PlaybackInfoImpl(playbackType, attrs, controlType, max, current) .getInstance(); } static PlaybackInfo fromBundle(Bundle bundle) { if (bundle == null) { return null; } final int volumeType = bundle.getInt(KEY_PLAYBACK_TYPE); final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE); final int maxVolume = bundle.getInt(KEY_MAX_VOLUME); final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME); final AudioAttributes attrs = bundle.getParcelable(KEY_AUDIO_ATTRIBUTES); return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume, currentVolume); } } }