Java tutorial
/* * 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; } }