| /* |
| * 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 androidx.media; |
| |
| import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS; |
| import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS; |
| import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE; |
| import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS; |
| import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE; |
| import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND; |
| import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE; |
| import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS; |
| import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK; |
| import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID; |
| import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM; |
| import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME; |
| import static androidx.media.MediaConstants2.ARGUMENT_PID; |
| import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO; |
| import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED; |
| import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT; |
| import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE; |
| import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST; |
| import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX; |
| import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA; |
| import static androidx.media.MediaConstants2.ARGUMENT_QUERY; |
| import static androidx.media.MediaConstants2.ARGUMENT_RATING; |
| import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE; |
| import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER; |
| import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE; |
| import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION; |
| import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE; |
| import static androidx.media.MediaConstants2.ARGUMENT_UID; |
| import static androidx.media.MediaConstants2.ARGUMENT_URI; |
| import static androidx.media.MediaConstants2.ARGUMENT_VOLUME; |
| import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION; |
| import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS; |
| import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED; |
| import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED; |
| import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE; |
| import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND; |
| import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT; |
| import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND; |
| import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME; |
| import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.DeadObjectException; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| import android.support.v4.media.session.IMediaControllerCallback; |
| import android.support.v4.media.session.MediaSessionCompat; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.media.MediaController2.PlaybackInfo; |
| import androidx.media.MediaSession2.CommandButton; |
| import androidx.media.MediaSession2.ControllerInfo; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| @TargetApi(Build.VERSION_CODES.KITKAT) |
| class MediaSession2StubImplBase extends MediaSessionCompat.Callback { |
| |
| private static final String TAG = "MS2StubImplBase"; |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest = |
| new SparseArray<>(); |
| |
| static { |
| SessionCommandGroup2 group = new SessionCommandGroup2(); |
| group.addAllPlaybackCommands(); |
| group.addAllPlaylistCommands(); |
| group.addAllVolumeCommands(); |
| Set<SessionCommand2> commands = group.getCommands(); |
| for (SessionCommand2 command : commands) { |
| sCommandsForOnCommandRequest.append(command.getCommandCode(), command); |
| } |
| } |
| |
| private final Object mLock = new Object(); |
| |
| final MediaSession2.SupportLibraryImpl mSession; |
| final Context mContext; |
| |
| @GuardedBy("mLock") |
| private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>(); |
| @GuardedBy("mLock") |
| private final Set<IBinder> mConnectingControllers = new HashSet<>(); |
| @GuardedBy("mLock") |
| private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap = |
| new ArrayMap<>(); |
| |
| MediaSession2StubImplBase(MediaSession2.SupportLibraryImpl session) { |
| mSession = session; |
| mContext = mSession.getContext(); |
| } |
| |
| @Override |
| public void onPrepare() { |
| mSession.getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mSession.isClosed()) { |
| return; |
| } |
| mSession.prepare(); |
| } |
| }); |
| } |
| |
| @Override |
| public void onPlay() { |
| mSession.getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mSession.isClosed()) { |
| return; |
| } |
| mSession.play(); |
| } |
| }); |
| } |
| |
| @Override |
| public void onPause() { |
| mSession.getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mSession.isClosed()) { |
| return; |
| } |
| mSession.pause(); |
| } |
| }); |
| } |
| |
| @Override |
| public void onStop() { |
| mSession.getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mSession.isClosed()) { |
| return; |
| } |
| mSession.reset(); |
| } |
| }); |
| } |
| |
| @Override |
| public void onSeekTo(final long pos) { |
| mSession.getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mSession.isClosed()) { |
| return; |
| } |
| mSession.seekTo(pos); |
| } |
| }); |
| } |
| |
| @Override |
| public void onCommand(String command, final Bundle extras, final ResultReceiver cb) { |
| switch (command) { |
| case CONTROLLER_COMMAND_CONNECT: |
| connect(extras, cb); |
| break; |
| case CONTROLLER_COMMAND_DISCONNECT: |
| disconnect(extras); |
| break; |
| case CONTROLLER_COMMAND_BY_COMMAND_CODE: { |
| final int commandCode = extras.getInt(ARGUMENT_COMMAND_CODE); |
| IMediaControllerCallback caller = |
| (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK); |
| if (caller == null) { |
| return; |
| } |
| |
| onCommand2(caller.asBinder(), commandCode, new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) { |
| switch (commandCode) { |
| case COMMAND_CODE_PLAYBACK_PLAY: |
| mSession.play(); |
| break; |
| case COMMAND_CODE_PLAYBACK_PAUSE: |
| mSession.pause(); |
| break; |
| case COMMAND_CODE_PLAYBACK_RESET: |
| mSession.reset(); |
| break; |
| case COMMAND_CODE_PLAYBACK_PREPARE: |
| mSession.prepare(); |
| break; |
| case COMMAND_CODE_PLAYBACK_SEEK_TO: { |
| long seekPos = extras.getLong(ARGUMENT_SEEK_POSITION); |
| mSession.seekTo(seekPos); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE: { |
| int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE); |
| mSession.setRepeatMode(repeatMode); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE: { |
| int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE); |
| mSession.setShuffleMode(shuffleMode); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_SET_LIST: { |
| List<MediaItem2> list = MediaUtils2.fromMediaItem2ParcelableArray( |
| extras.getParcelableArray(ARGUMENT_PLAYLIST)); |
| MediaMetadata2 metadata = MediaMetadata2.fromBundle( |
| extras.getBundle(ARGUMENT_PLAYLIST_METADATA)); |
| mSession.setPlaylist(list, metadata); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_SET_LIST_METADATA: { |
| MediaMetadata2 metadata = MediaMetadata2.fromBundle( |
| extras.getBundle(ARGUMENT_PLAYLIST_METADATA)); |
| mSession.updatePlaylistMetadata(metadata); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_ADD_ITEM: { |
| int index = extras.getInt(ARGUMENT_PLAYLIST_INDEX); |
| MediaItem2 item = MediaItem2.fromBundle( |
| extras.getBundle(ARGUMENT_MEDIA_ITEM)); |
| mSession.addPlaylistItem(index, item); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_REMOVE_ITEM: { |
| MediaItem2 item = MediaItem2.fromBundle( |
| extras.getBundle(ARGUMENT_MEDIA_ITEM)); |
| mSession.removePlaylistItem(item); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_REPLACE_ITEM: { |
| int index = extras.getInt(ARGUMENT_PLAYLIST_INDEX); |
| MediaItem2 item = MediaItem2.fromBundle( |
| extras.getBundle(ARGUMENT_MEDIA_ITEM)); |
| mSession.replacePlaylistItem(index, item); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM: { |
| mSession.skipToNextItem(); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM: { |
| mSession.skipToPreviousItem(); |
| break; |
| } |
| case COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM: { |
| MediaItem2 item = MediaItem2.fromBundle( |
| extras.getBundle(ARGUMENT_MEDIA_ITEM)); |
| mSession.skipToPlaylistItem(item); |
| break; |
| } |
| case COMMAND_CODE_VOLUME_SET_VOLUME: { |
| int value = extras.getInt(ARGUMENT_VOLUME); |
| int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS); |
| VolumeProviderCompat vp = mSession.getVolumeProvider(); |
| if (vp == null) { |
| // TODO: Revisit |
| } else { |
| vp.onSetVolumeTo(value); |
| } |
| break; |
| } |
| case COMMAND_CODE_VOLUME_ADJUST_VOLUME: { |
| int direction = extras.getInt(ARGUMENT_VOLUME_DIRECTION); |
| int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS); |
| VolumeProviderCompat vp = mSession.getVolumeProvider(); |
| if (vp == null) { |
| // TODO: Revisit |
| } else { |
| vp.onAdjustVolume(direction); |
| } |
| break; |
| } |
| case COMMAND_CODE_SESSION_REWIND: { |
| mSession.getCallback().onRewind( |
| mSession.getInstance(), controller); |
| break; |
| } |
| case COMMAND_CODE_SESSION_FAST_FORWARD: { |
| mSession.getCallback().onFastForward( |
| mSession.getInstance(), controller); |
| break; |
| } |
| case COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID: { |
| String mediaId = extras.getString(ARGUMENT_MEDIA_ID); |
| Bundle extra = extras.getBundle(ARGUMENT_EXTRAS); |
| mSession.getCallback().onPlayFromMediaId( |
| mSession.getInstance(), controller, mediaId, extra); |
| break; |
| } |
| case COMMAND_CODE_SESSION_PLAY_FROM_SEARCH: { |
| String query = extras.getString(ARGUMENT_QUERY); |
| Bundle extra = extras.getBundle(ARGUMENT_EXTRAS); |
| mSession.getCallback().onPlayFromSearch( |
| mSession.getInstance(), controller, query, extra); |
| break; |
| } |
| case COMMAND_CODE_SESSION_PLAY_FROM_URI: { |
| Uri uri = extras.getParcelable(ARGUMENT_URI); |
| Bundle extra = extras.getBundle(ARGUMENT_EXTRAS); |
| mSession.getCallback().onPlayFromUri( |
| mSession.getInstance(), controller, uri, extra); |
| break; |
| } |
| case COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID: { |
| String mediaId = extras.getString(ARGUMENT_MEDIA_ID); |
| Bundle extra = extras.getBundle(ARGUMENT_EXTRAS); |
| mSession.getCallback().onPrepareFromMediaId( |
| mSession.getInstance(), controller, mediaId, extra); |
| break; |
| } |
| case COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH: { |
| String query = extras.getString(ARGUMENT_QUERY); |
| Bundle extra = extras.getBundle(ARGUMENT_EXTRAS); |
| mSession.getCallback().onPrepareFromSearch( |
| mSession.getInstance(), controller, query, extra); |
| break; |
| } |
| case COMMAND_CODE_SESSION_PREPARE_FROM_URI: { |
| Uri uri = extras.getParcelable(ARGUMENT_URI); |
| Bundle extra = extras.getBundle(ARGUMENT_EXTRAS); |
| mSession.getCallback().onPrepareFromUri( |
| mSession.getInstance(), controller, uri, extra); |
| break; |
| } |
| case COMMAND_CODE_SESSION_SET_RATING: { |
| String mediaId = extras.getString(ARGUMENT_MEDIA_ID); |
| Rating2 rating = Rating2.fromBundle( |
| extras.getBundle(ARGUMENT_RATING)); |
| mSession.getCallback().onSetRating( |
| mSession.getInstance(), controller, mediaId, rating); |
| break; |
| } |
| case COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO: { |
| mSession.getCallback().onSubscribeRoutesInfo( |
| mSession.getInstance(), controller); |
| break; |
| } |
| case COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO: { |
| mSession.getCallback().onUnsubscribeRoutesInfo( |
| mSession.getInstance(), controller); |
| break; |
| } |
| case COMMAND_CODE_SESSION_SELECT_ROUTE: { |
| Bundle route = extras.getBundle(ARGUMENT_ROUTE_BUNDLE); |
| mSession.getCallback().onSelectRoute( |
| mSession.getInstance(), controller, route); |
| break; |
| } |
| case COMMAND_CODE_PLAYBACK_SET_SPEED: { |
| float speed = extras.getFloat(ARGUMENT_PLAYBACK_SPEED); |
| mSession.setPlaybackSpeed(speed); |
| break; |
| } |
| } |
| } |
| }); |
| break; |
| } |
| case CONTROLLER_COMMAND_BY_CUSTOM_COMMAND: { |
| final SessionCommand2 customCommand = |
| SessionCommand2.fromBundle(extras.getBundle(ARGUMENT_CUSTOM_COMMAND)); |
| IMediaControllerCallback caller = |
| (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK); |
| if (caller == null || customCommand == null) { |
| return; |
| } |
| |
| final Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS); |
| onCommand2(caller.asBinder(), customCommand, new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| mSession.getCallback().onCustomCommand( |
| mSession.getInstance(), controller, customCommand, args, cb); |
| } |
| }); |
| break; |
| } |
| } |
| } |
| |
| List<ControllerInfo> getConnectedControllers() { |
| ArrayList<ControllerInfo> controllers = new ArrayList<>(); |
| synchronized (mLock) { |
| for (int i = 0; i < mControllers.size(); i++) { |
| controllers.add(mControllers.valueAt(i)); |
| } |
| } |
| return controllers; |
| } |
| |
| void notifyCustomLayout(ControllerInfo controller, final List<CommandButton> layout) { |
| notifyInternal(controller, new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS, |
| MediaUtils2.toCommandButtonParcelableArray(layout)); |
| controller.getControllerBinder().onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle); |
| } |
| }); |
| } |
| |
| void setAllowedCommands(ControllerInfo controller, final SessionCommandGroup2 commands) { |
| synchronized (mLock) { |
| mAllowedCommandGroupMap.put(controller, commands); |
| } |
| notifyInternal(controller, new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle()); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| public void sendCustomCommand(ControllerInfo controller, final SessionCommand2 command, |
| final Bundle args, final ResultReceiver receiver) { |
| if (receiver != null && controller == null) { |
| throw new IllegalArgumentException("Controller shouldn't be null if result receiver is" |
| + " specified"); |
| } |
| if (command == null) { |
| throw new IllegalArgumentException("command shouldn't be null"); |
| } |
| notifyInternal(controller, new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| // TODO: Send this event through MediaSessionCompat.XXX() |
| Bundle bundle = new Bundle(); |
| bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle()); |
| bundle.putBundle(ARGUMENT_ARGUMENTS, args); |
| bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver); |
| controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle); |
| } |
| }); |
| } |
| |
| public void sendCustomCommand(final SessionCommand2 command, final Bundle args) { |
| if (command == null) { |
| throw new IllegalArgumentException("command shouldn't be null"); |
| } |
| final Bundle bundle = new Bundle(); |
| bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle()); |
| bundle.putBundle(ARGUMENT_ARGUMENTS, args); |
| notifyAll(new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle); |
| } |
| }); |
| } |
| |
| void notifyCurrentMediaItemChanged(final MediaItem2 item) { |
| notifyAll(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM, new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle()); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| void notifyPlaybackInfoChanged(final PlaybackInfo info) { |
| notifyAll(new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle()); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| void notifyPlayerStateChanged(final int state) { |
| notifyAll(new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putInt(ARGUMENT_PLAYER_STATE, state); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| void notifyPlaybackSpeedChanged(final float speed) { |
| notifyAll(new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putParcelable( |
| ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat()); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| void notifyBufferingStateChanged(final MediaItem2 item, final int bufferingState) { |
| notifyAll(new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle()); |
| bundle.putInt(ARGUMENT_BUFFERING_STATE, bufferingState); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED, bundle); |
| } |
| }); |
| } |
| |
| void notifyError(final int errorCode, final Bundle extras) { |
| notifyAll(new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putInt(ARGUMENT_ERROR_CODE, errorCode); |
| bundle.putBundle(ARGUMENT_EXTRAS, extras); |
| controller.getControllerBinder().onEvent(SESSION_EVENT_ON_ERROR, bundle); |
| } |
| }); |
| } |
| |
| void notifyRoutesInfoChanged(@NonNull final ControllerInfo controller, |
| @Nullable final List<Bundle> routes) { |
| notifyInternal(controller, new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = null; |
| if (routes != null) { |
| bundle = new Bundle(); |
| bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0])); |
| } |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| void notifyPlaylistChanged(final List<MediaItem2> playlist, |
| final MediaMetadata2 metadata) { |
| notifyAll(COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putParcelableArray(ARGUMENT_PLAYLIST, |
| MediaUtils2.toMediaItem2ParcelableArray(playlist)); |
| bundle.putBundle(ARGUMENT_PLAYLIST_METADATA, |
| metadata == null ? null : metadata.toBundle()); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| void notifyPlaylistMetadataChanged(final MediaMetadata2 metadata) { |
| notifyAll(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA, new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putBundle(ARGUMENT_PLAYLIST_METADATA, |
| metadata == null ? null : metadata.toBundle()); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| void notifyRepeatModeChanged(final int repeatMode) { |
| notifyAll(new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| void notifyShuffleModeChanged(final int shuffleMode) { |
| notifyAll(new Session2Runnable() { |
| @Override |
| public void run(ControllerInfo controller) throws RemoteException { |
| Bundle bundle = new Bundle(); |
| bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode); |
| controller.getControllerBinder().onEvent( |
| SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle); |
| } |
| }); |
| } |
| |
| private List<ControllerInfo> getControllers() { |
| ArrayList<ControllerInfo> controllers = new ArrayList<>(); |
| synchronized (mLock) { |
| for (int i = 0; i < mControllers.size(); i++) { |
| controllers.add(mControllers.valueAt(i)); |
| } |
| } |
| return controllers; |
| } |
| |
| private void notifyAll(@NonNull Session2Runnable runnable) { |
| List<ControllerInfo> controllers = getControllers(); |
| for (int i = 0; i < controllers.size(); i++) { |
| notifyInternal(controllers.get(i), runnable); |
| } |
| } |
| |
| private void notifyAll(int commandCode, @NonNull Session2Runnable runnable) { |
| List<ControllerInfo> controllers = getControllers(); |
| for (int i = 0; i < controllers.size(); i++) { |
| ControllerInfo controller = controllers.get(i); |
| if (isAllowedCommand(controller, commandCode)) { |
| notifyInternal(controller, runnable); |
| } |
| } |
| } |
| |
| // TODO: Add a way to check permission from here. |
| private void notifyInternal(@NonNull ControllerInfo controller, |
| @NonNull Session2Runnable runnable) { |
| if (controller == null || controller.getControllerBinder() == null) { |
| return; |
| } |
| try { |
| runnable.run(controller); |
| } catch (DeadObjectException e) { |
| if (DEBUG) { |
| Log.d(TAG, controller.toString() + " is gone", e); |
| } |
| onControllerClosed(controller.getControllerBinder()); |
| } 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 boolean isAllowedCommand(ControllerInfo controller, SessionCommand2 command) { |
| SessionCommandGroup2 allowedCommands; |
| synchronized (mLock) { |
| allowedCommands = mAllowedCommandGroupMap.get(controller); |
| } |
| return allowedCommands != null && allowedCommands.hasCommand(command); |
| } |
| |
| private boolean isAllowedCommand(ControllerInfo controller, int commandCode) { |
| SessionCommandGroup2 allowedCommands; |
| synchronized (mLock) { |
| allowedCommands = mAllowedCommandGroupMap.get(controller); |
| } |
| return allowedCommands != null && allowedCommands.hasCommand(commandCode); |
| } |
| |
| private void onCommand2(@NonNull IBinder caller, final int commandCode, |
| @NonNull final Session2Runnable runnable) { |
| // TODO: Prevent instantiation of SessionCommand2 |
| onCommand2(caller, new SessionCommand2(commandCode), runnable); |
| } |
| |
| private void onCommand2(@NonNull IBinder caller, @NonNull final SessionCommand2 sessionCommand, |
| @NonNull final Session2Runnable runnable) { |
| final ControllerInfo controller; |
| synchronized (mLock) { |
| controller = mControllers.get(caller); |
| } |
| if (mSession == null || controller == null) { |
| return; |
| } |
| mSession.getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| if (!isAllowedCommand(controller, sessionCommand)) { |
| return; |
| } |
| int commandCode = sessionCommand.getCommandCode(); |
| SessionCommand2 command = sCommandsForOnCommandRequest.get(commandCode); |
| if (command != null) { |
| boolean accepted = mSession.getCallback().onCommandRequest( |
| mSession.getInstance(), controller, command); |
| if (!accepted) { |
| // Don't run rejected command. |
| if (DEBUG) { |
| Log.d(TAG, "Command (code=" + commandCode + ") from " |
| + controller + " was rejected by " + mSession); |
| } |
| return; |
| } |
| } |
| try { |
| runnable.run(controller); |
| } 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 void onControllerClosed(IMediaControllerCallback iController) { |
| ControllerInfo controller; |
| synchronized (mLock) { |
| controller = mControllers.remove(iController.asBinder()); |
| if (DEBUG) { |
| Log.d(TAG, "releasing " + controller); |
| } |
| } |
| if (controller == null) { |
| return; |
| } |
| final ControllerInfo removedController = controller; |
| mSession.getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| mSession.getCallback().onDisconnected(mSession.getInstance(), removedController); |
| } |
| }); |
| } |
| |
| private ControllerInfo createControllerInfo(Bundle extras) { |
| IMediaControllerCallback callback = IMediaControllerCallback.Stub.asInterface( |
| extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK)); |
| String packageName = extras.getString(ARGUMENT_PACKAGE_NAME); |
| int uid = extras.getInt(ARGUMENT_UID); |
| int pid = extras.getInt(ARGUMENT_PID); |
| // TODO: sanity check for packageName, uid, and pid. |
| |
| return new ControllerInfo(mContext, uid, pid, packageName, callback); |
| } |
| |
| private void connect(Bundle extras, final ResultReceiver cb) { |
| final ControllerInfo controllerInfo = createControllerInfo(extras); |
| mSession.getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mSession.isClosed()) { |
| return; |
| } |
| synchronized (mLock) { |
| // Keep connecting controllers. |
| // This helps sessions to call APIs in the onConnect() |
| // (e.g. setCustomLayout()) instead of pending them. |
| mConnectingControllers.add(controllerInfo.getId()); |
| } |
| SessionCommandGroup2 allowedCommands = mSession.getCallback().onConnect( |
| mSession.getInstance(), controllerInfo); |
| // Don't reject connection for the request from trusted app. |
| // Otherwise server will fail to retrieve session's information to dispatch |
| // media keys to. |
| boolean accept = allowedCommands != null || controllerInfo.isTrusted(); |
| if (accept) { |
| if (DEBUG) { |
| Log.d(TAG, "Accepting connection, controllerInfo=" + controllerInfo |
| + " allowedCommands=" + allowedCommands); |
| } |
| if (allowedCommands == null) { |
| // For trusted apps, send non-null allowed commands to keep |
| // connection. |
| allowedCommands = new SessionCommandGroup2(); |
| } |
| synchronized (mLock) { |
| mConnectingControllers.remove(controllerInfo.getId()); |
| mControllers.put(controllerInfo.getId(), controllerInfo); |
| mAllowedCommandGroupMap.put(controllerInfo, allowedCommands); |
| } |
| // If connection is accepted, notify the current state to the |
| // controller. It's needed because we cannot call synchronous calls |
| // between session/controller. |
| // Note: We're doing this after the onConnectionChanged(), but there's |
| // no guarantee that events here are notified after the |
| // onConnected() because IMediaController2 is oneway (i.e. async |
| // call) and Stub will use thread poll for incoming calls. |
| final Bundle resultData = new Bundle(); |
| resultData.putBundle(ARGUMENT_ALLOWED_COMMANDS, |
| allowedCommands.toBundle()); |
| resultData.putInt(ARGUMENT_PLAYER_STATE, mSession.getPlayerState()); |
| resultData.putInt(ARGUMENT_BUFFERING_STATE, mSession.getBufferingState()); |
| resultData.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT, |
| mSession.getPlaybackStateCompat()); |
| resultData.putInt(ARGUMENT_REPEAT_MODE, mSession.getRepeatMode()); |
| resultData.putInt(ARGUMENT_SHUFFLE_MODE, mSession.getShuffleMode()); |
| final List<MediaItem2> playlist = allowedCommands.hasCommand( |
| COMMAND_CODE_PLAYLIST_GET_LIST) ? mSession.getPlaylist() : null; |
| if (playlist != null) { |
| resultData.putParcelableArray(ARGUMENT_PLAYLIST, |
| MediaUtils2.toMediaItem2ParcelableArray(playlist)); |
| } |
| final MediaItem2 currentMediaItem = |
| allowedCommands.hasCommand(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM) |
| ? mSession.getCurrentMediaItem() : null; |
| if (currentMediaItem != null) { |
| resultData.putBundle(ARGUMENT_MEDIA_ITEM, currentMediaItem.toBundle()); |
| } |
| resultData.putBundle(ARGUMENT_PLAYBACK_INFO, |
| mSession.getPlaybackInfo().toBundle()); |
| final MediaMetadata2 playlistMetadata = mSession.getPlaylistMetadata(); |
| if (playlistMetadata != null) { |
| resultData.putBundle(ARGUMENT_PLAYLIST_METADATA, |
| playlistMetadata.toBundle()); |
| } |
| // Double check if session is still there, because close() can be |
| // called in another thread. |
| if (mSession.isClosed()) { |
| return; |
| } |
| cb.send(CONNECT_RESULT_CONNECTED, resultData); |
| } else { |
| synchronized (mLock) { |
| mConnectingControllers.remove(controllerInfo.getId()); |
| } |
| if (DEBUG) { |
| Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo); |
| } |
| cb.send(CONNECT_RESULT_DISCONNECTED, null); |
| } |
| } |
| }); |
| } |
| |
| private void disconnect(Bundle extras) { |
| final ControllerInfo controllerInfo = createControllerInfo(extras); |
| mSession.getCallbackExecutor().execute(new Runnable() { |
| @Override |
| public void run() { |
| if (mSession.isClosed()) { |
| return; |
| } |
| mSession.getCallback().onDisconnected(mSession.getInstance(), controllerInfo); |
| } |
| }); |
| } |
| |
| @FunctionalInterface |
| private interface Session2Runnable { |
| void run(ControllerInfo controller) throws RemoteException; |
| } |
| } |