Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.media;
     18 
     19 import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
     20 import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
     21 import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
     22 import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
     23 import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
     24 import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
     25 import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
     26 import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
     27 import static androidx.media.MediaConstants2.ARGUMENT_ITEM_COUNT;
     28 import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
     29 import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
     30 import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
     31 import static androidx.media.MediaConstants2.ARGUMENT_PID;
     32 import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
     33 import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
     34 import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
     35 import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
     36 import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
     37 import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
     38 import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
     39 import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
     40 import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
     41 import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
     42 import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
     43 import static androidx.media.MediaConstants2.ARGUMENT_UID;
     44 import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
     45 import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
     46 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
     47 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHANGED;
     48 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CHILDREN_CHANGED;
     49 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
     50 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
     51 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
     52 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
     53 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
     54 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
     55 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
     56 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
     57 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
     58 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEARCH_RESULT_CHANGED;
     59 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SEEK_COMPLETED;
     60 import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
     61 import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
     62 import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
     63 import static androidx.media.SessionCommand2.COMMAND_CODE_CUSTOM;
     64 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM;
     65 import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST;
     66 
     67 import android.annotation.TargetApi;
     68 import android.content.Context;
     69 import android.os.Build;
     70 import android.os.Bundle;
     71 import android.os.IBinder;
     72 import android.os.RemoteException;
     73 import android.os.ResultReceiver;
     74 import android.support.v4.media.session.IMediaControllerCallback;
     75 import android.support.v4.media.session.MediaSessionCompat;
     76 import android.util.Log;
     77 import android.util.SparseArray;
     78 
     79 import androidx.annotation.GuardedBy;
     80 import androidx.annotation.NonNull;
     81 import androidx.annotation.Nullable;
     82 import androidx.collection.ArrayMap;
     83 import androidx.core.app.BundleCompat;
     84 import androidx.media.MediaController2.PlaybackInfo;
     85 import androidx.media.MediaSession2.CommandButton;
     86 import androidx.media.MediaSession2.ControllerCb;
     87 import androidx.media.MediaSession2.ControllerInfo;
     88 
     89 import java.util.ArrayList;
     90 import java.util.HashSet;
     91 import java.util.List;
     92 import java.util.Set;
     93 
     94 @TargetApi(Build.VERSION_CODES.KITKAT)
     95 class MediaSessionLegacyStub extends MediaSessionCompat.Callback {
     96 
     97     private static final String TAG = "MS2StubImplBase";
     98     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     99 
    100     private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest =
    101             new SparseArray<>();
    102 
    103     static {
    104         SessionCommandGroup2 group = new SessionCommandGroup2();
    105         group.addAllPlaybackCommands();
    106         group.addAllPlaylistCommands();
    107         group.addAllVolumeCommands();
    108         Set<SessionCommand2> commands = group.getCommands();
    109         for (SessionCommand2 command : commands) {
    110             sCommandsForOnCommandRequest.append(command.getCommandCode(), command);
    111         }
    112     }
    113 
    114     private final Object mLock = new Object();
    115 
    116     final MediaSession2.SupportLibraryImpl mSession;
    117     final Context mContext;
    118 
    119     @GuardedBy("mLock")
    120     private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
    121     @GuardedBy("mLock")
    122     private final Set<IBinder> mConnectingControllers = new HashSet<>();
    123     @GuardedBy("mLock")
    124     private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap =
    125             new ArrayMap<>();
    126 
    127     MediaSessionLegacyStub(MediaSession2.SupportLibraryImpl session) {
    128         mSession = session;
    129         mContext = mSession.getContext();
    130     }
    131 
    132     @Override
    133     public void onPrepare() {
    134         mSession.getCallbackExecutor().execute(new Runnable() {
    135             @Override
    136             public void run() {
    137                 if (mSession.isClosed()) {
    138                     return;
    139                 }
    140                 mSession.prepare();
    141             }
    142         });
    143     }
    144 
    145     @Override
    146     public void onPlay() {
    147         mSession.getCallbackExecutor().execute(new Runnable() {
    148             @Override
    149             public void run() {
    150                 if (mSession.isClosed()) {
    151                     return;
    152                 }
    153                 mSession.play();
    154             }
    155         });
    156     }
    157 
    158     @Override
    159     public void onPause() {
    160         mSession.getCallbackExecutor().execute(new Runnable() {
    161             @Override
    162             public void run() {
    163                 if (mSession.isClosed()) {
    164                     return;
    165                 }
    166                 mSession.pause();
    167             }
    168         });
    169     }
    170 
    171     @Override
    172     public void onStop() {
    173         mSession.getCallbackExecutor().execute(new Runnable() {
    174             @Override
    175             public void run() {
    176                 if (mSession.isClosed()) {
    177                     return;
    178                 }
    179                 mSession.reset();
    180             }
    181         });
    182     }
    183 
    184     @Override
    185     public void onSeekTo(final long pos) {
    186         mSession.getCallbackExecutor().execute(new Runnable() {
    187             @Override
    188             public void run() {
    189                 if (mSession.isClosed()) {
    190                     return;
    191                 }
    192                 mSession.seekTo(pos);
    193             }
    194         });
    195     }
    196 
    197     List<ControllerInfo> getConnectedControllers() {
    198         ArrayList<ControllerInfo> controllers = new ArrayList<>();
    199         synchronized (mLock) {
    200             for (int i = 0; i < mControllers.size(); i++) {
    201                 controllers.add(mControllers.valueAt(i));
    202             }
    203         }
    204         return controllers;
    205     }
    206 
    207     void setAllowedCommands(ControllerInfo controller, final SessionCommandGroup2 commands) {
    208         synchronized (mLock) {
    209             mAllowedCommandGroupMap.put(controller, commands);
    210         }
    211     }
    212 
    213     private boolean isAllowedCommand(ControllerInfo controller, SessionCommand2 command) {
    214         SessionCommandGroup2 allowedCommands;
    215         synchronized (mLock) {
    216             allowedCommands = mAllowedCommandGroupMap.get(controller);
    217         }
    218         return allowedCommands != null && allowedCommands.hasCommand(command);
    219     }
    220 
    221     private boolean isAllowedCommand(ControllerInfo controller, int commandCode) {
    222         SessionCommandGroup2 allowedCommands;
    223         synchronized (mLock) {
    224             allowedCommands = mAllowedCommandGroupMap.get(controller);
    225         }
    226         return allowedCommands != null && allowedCommands.hasCommand(commandCode);
    227     }
    228 
    229     private void onCommand2(@NonNull IBinder caller, final int commandCode,
    230             @NonNull final Session2Runnable runnable) {
    231         onCommand2Internal(caller, null, commandCode, runnable);
    232     }
    233 
    234     private void onCommand2(@NonNull IBinder caller, @NonNull final SessionCommand2 sessionCommand,
    235             @NonNull final Session2Runnable runnable) {
    236         onCommand2Internal(caller, sessionCommand, COMMAND_CODE_CUSTOM, runnable);
    237     }
    238 
    239     private void onCommand2Internal(@NonNull IBinder caller,
    240             @Nullable final SessionCommand2 sessionCommand, final int commandCode,
    241             @NonNull final Session2Runnable runnable) {
    242         final ControllerInfo controller;
    243         synchronized (mLock) {
    244             controller = mControllers.get(caller);
    245         }
    246         if (mSession == null || controller == null) {
    247             return;
    248         }
    249         mSession.getCallbackExecutor().execute(new Runnable() {
    250             @Override
    251             public void run() {
    252                 SessionCommand2 command;
    253                 if (sessionCommand != null) {
    254                     if (!isAllowedCommand(controller, sessionCommand)) {
    255                         return;
    256                     }
    257                     command = sCommandsForOnCommandRequest.get(sessionCommand.getCommandCode());
    258                 } else {
    259                     if (!isAllowedCommand(controller, commandCode)) {
    260                         return;
    261                     }
    262                     command = sCommandsForOnCommandRequest.get(commandCode);
    263                 }
    264                 if (command != null) {
    265                     boolean accepted = mSession.getCallback().onCommandRequest(
    266                             mSession.getInstance(), controller, command);
    267                     if (!accepted) {
    268                         // Don't run rejected command.
    269                         if (DEBUG) {
    270                             Log.d(TAG, "Command (" + command + ") from "
    271                                     + controller + " was rejected by " + mSession);
    272                         }
    273                         return;
    274                     }
    275                 }
    276                 try {
    277                     runnable.run(controller);
    278                 } catch (RemoteException e) {
    279                     // Currently it's TransactionTooLargeException or DeadSystemException.
    280                     // We'd better to leave log for those cases because
    281                     //   - TransactionTooLargeException means that we may need to fix our code.
    282                     //     (e.g. add pagination or special way to deliver Bitmap)
    283                     //   - DeadSystemException means that errors around it can be ignored.
    284                     Log.w(TAG, "Exception in " + controller.toString(), e);
    285                 }
    286             }
    287         });
    288     }
    289 
    290     void removeControllerInfo(ControllerInfo controller) {
    291         synchronized (mLock) {
    292             controller = mControllers.remove(controller.getId());
    293             if (DEBUG) {
    294                 Log.d(TAG, "releasing " + controller);
    295             }
    296         }
    297     }
    298 
    299     private ControllerInfo createControllerInfo(Bundle extras) {
    300         IMediaControllerCallback callback = IMediaControllerCallback.Stub.asInterface(
    301                 BundleCompat.getBinder(extras, ARGUMENT_ICONTROLLER_CALLBACK));
    302         String packageName = extras.getString(ARGUMENT_PACKAGE_NAME);
    303         int uid = extras.getInt(ARGUMENT_UID);
    304         int pid = extras.getInt(ARGUMENT_PID);
    305         return new ControllerInfo(packageName, pid, uid, new ControllerLegacyCb(callback));
    306     }
    307 
    308     private void connect(Bundle extras, final ResultReceiver cb) {
    309         final ControllerInfo controllerInfo = createControllerInfo(extras);
    310         mSession.getCallbackExecutor().execute(new Runnable() {
    311             @Override
    312             public void run() {
    313                 if (mSession.isClosed()) {
    314                     return;
    315                 }
    316                 synchronized (mLock) {
    317                     // Keep connecting controllers.
    318                     // This helps sessions to call APIs in the onConnect()
    319                     // (e.g. setCustomLayout()) instead of pending them.
    320                     mConnectingControllers.add(controllerInfo.getId());
    321                 }
    322                 SessionCommandGroup2 allowedCommands = mSession.getCallback().onConnect(
    323                         mSession.getInstance(), controllerInfo);
    324                 // Don't reject connection for the request from trusted app.
    325                 // Otherwise server will fail to retrieve session's information to dispatch
    326                 // media keys to.
    327                 boolean accept = allowedCommands != null || controllerInfo.isTrusted();
    328                 if (accept) {
    329                     if (DEBUG) {
    330                         Log.d(TAG, "Accepting connection, controllerInfo=" + controllerInfo
    331                                 + " allowedCommands=" + allowedCommands);
    332                     }
    333                     if (allowedCommands == null) {
    334                         // For trusted apps, send non-null allowed commands to keep
    335                         // connection.
    336                         allowedCommands = new SessionCommandGroup2();
    337                     }
    338                     synchronized (mLock) {
    339                         mConnectingControllers.remove(controllerInfo.getId());
    340                         mControllers.put(controllerInfo.getId(), controllerInfo);
    341                         mAllowedCommandGroupMap.put(controllerInfo, allowedCommands);
    342                     }
    343                     // If connection is accepted, notify the current state to the
    344                     // controller. It's needed because we cannot call synchronous calls
    345                     // between session/controller.
    346                     // Note: We're doing this after the onConnectionChanged(), but there's
    347                     //       no guarantee that events here are notified after the
    348                     //       onConnected() because IMediaController2 is oneway (i.e. async
    349                     //       call) and Stub will use thread poll for incoming calls.
    350                     final Bundle resultData = new Bundle();
    351                     resultData.putBundle(ARGUMENT_ALLOWED_COMMANDS,
    352                             allowedCommands.toBundle());
    353                     resultData.putInt(ARGUMENT_PLAYER_STATE, mSession.getPlayerState());
    354                     resultData.putInt(ARGUMENT_BUFFERING_STATE, mSession.getBufferingState());
    355                     resultData.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
    356                             mSession.getPlaybackStateCompat());
    357                     resultData.putInt(ARGUMENT_REPEAT_MODE, mSession.getRepeatMode());
    358                     resultData.putInt(ARGUMENT_SHUFFLE_MODE, mSession.getShuffleMode());
    359                     final List<MediaItem2> playlist = allowedCommands.hasCommand(
    360                             COMMAND_CODE_PLAYLIST_GET_LIST) ? mSession.getPlaylist() : null;
    361                     if (playlist != null) {
    362                         resultData.putParcelableArray(ARGUMENT_PLAYLIST,
    363                                 MediaUtils2.toMediaItem2ParcelableArray(playlist));
    364                     }
    365                     final MediaItem2 currentMediaItem =
    366                             allowedCommands.hasCommand(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM)
    367                                     ? mSession.getCurrentMediaItem() : null;
    368                     if (currentMediaItem != null) {
    369                         resultData.putBundle(ARGUMENT_MEDIA_ITEM, currentMediaItem.toBundle());
    370                     }
    371                     resultData.putBundle(ARGUMENT_PLAYBACK_INFO,
    372                             mSession.getPlaybackInfo().toBundle());
    373                     final MediaMetadata2 playlistMetadata = mSession.getPlaylistMetadata();
    374                     if (playlistMetadata != null) {
    375                         resultData.putBundle(ARGUMENT_PLAYLIST_METADATA,
    376                                 playlistMetadata.toBundle());
    377                     }
    378                     // Double check if session is still there, because close() can be
    379                     // called in another thread.
    380                     if (mSession.isClosed()) {
    381                         return;
    382                     }
    383                     cb.send(CONNECT_RESULT_CONNECTED, resultData);
    384                 } else {
    385                     synchronized (mLock) {
    386                         mConnectingControllers.remove(controllerInfo.getId());
    387                     }
    388                     if (DEBUG) {
    389                         Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo);
    390                     }
    391                     cb.send(CONNECT_RESULT_DISCONNECTED, null);
    392                 }
    393             }
    394         });
    395     }
    396 
    397     private void disconnect(Bundle extras) {
    398         final ControllerInfo controllerInfo = createControllerInfo(extras);
    399         mSession.getCallbackExecutor().execute(new Runnable() {
    400             @Override
    401             public void run() {
    402                 if (mSession.isClosed()) {
    403                     return;
    404                 }
    405                 mSession.getCallback().onDisconnected(mSession.getInstance(), controllerInfo);
    406             }
    407         });
    408     }
    409 
    410     @FunctionalInterface
    411     private interface Session2Runnable {
    412         void run(ControllerInfo controller) throws RemoteException;
    413     }
    414 
    415     final class ControllerLegacyCb extends ControllerCb {
    416         private final IMediaControllerCallback mIControllerCallback;
    417 
    418         ControllerLegacyCb(@NonNull IMediaControllerCallback callback) {
    419             mIControllerCallback = callback;
    420         }
    421 
    422         @Override
    423         @NonNull IBinder getId() {
    424             return mIControllerCallback.asBinder();
    425         }
    426 
    427         @Override
    428         void onCustomLayoutChanged(List<CommandButton> layout) throws RemoteException {
    429             Bundle bundle = new Bundle();
    430             bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS,
    431                     MediaUtils2.toCommandButtonParcelableArray(layout));
    432             mIControllerCallback.onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle);
    433         }
    434 
    435         @Override
    436         void onPlaybackInfoChanged(PlaybackInfo info) throws RemoteException {
    437             Bundle bundle = new Bundle();
    438             bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle());
    439             mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle);
    440 
    441         }
    442 
    443         @Override
    444         void onAllowedCommandsChanged(SessionCommandGroup2 commands) throws RemoteException {
    445             Bundle bundle = new Bundle();
    446             bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle());
    447             mIControllerCallback.onEvent(SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle);
    448         }
    449 
    450         @Override
    451         void onCustomCommand(SessionCommand2 command, Bundle args, ResultReceiver receiver)
    452                 throws RemoteException {
    453             Bundle bundle = new Bundle();
    454             bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
    455             bundle.putBundle(ARGUMENT_ARGUMENTS, args);
    456             bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver);
    457             mIControllerCallback.onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
    458         }
    459 
    460         @Override
    461         void onPlayerStateChanged(int playerState)
    462                 throws RemoteException {
    463             // Note: current position should be also sent to the controller here for controller
    464             // to calculate the position more correctly.
    465             Bundle bundle = new Bundle();
    466             bundle.putInt(ARGUMENT_PLAYER_STATE, playerState);
    467             bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
    468             mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle);
    469         }
    470 
    471         @Override
    472         void onPlaybackSpeedChanged(float speed) throws RemoteException {
    473             // Note: current position should be also sent to the controller here for controller
    474             // to calculate the position more correctly.
    475             Bundle bundle = new Bundle();
    476             bundle.putParcelable(
    477                     ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
    478             mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
    479         }
    480 
    481         @Override
    482         void onBufferingStateChanged(MediaItem2 item, int state) throws RemoteException {
    483             // Note: buffered position should be also sent to the controller here. It's to
    484             // follow the behavior of MediaPlayerInterface.PlayerEventCallback.
    485             Bundle bundle = new Bundle();
    486             bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
    487             bundle.putInt(ARGUMENT_BUFFERING_STATE, state);
    488             bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
    489                     mSession.getPlaybackStateCompat());
    490             mIControllerCallback.onEvent(SESSION_EVENT_ON_BUFFERING_STATE_CHANGED, bundle);
    491 
    492         }
    493 
    494         @Override
    495         void onSeekCompleted(long position) throws RemoteException {
    496             // Note: current position should be also sent to the controller here because the
    497             // position here may refer to the parameter of the previous seek() API calls.
    498             Bundle bundle = new Bundle();
    499             bundle.putLong(ARGUMENT_SEEK_POSITION, position);
    500             bundle.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
    501                     mSession.getPlaybackStateCompat());
    502             mIControllerCallback.onEvent(SESSION_EVENT_ON_SEEK_COMPLETED, bundle);
    503         }
    504 
    505         @Override
    506         void onError(int errorCode, Bundle extras) throws RemoteException {
    507             Bundle bundle = new Bundle();
    508             bundle.putInt(ARGUMENT_ERROR_CODE, errorCode);
    509             bundle.putBundle(ARGUMENT_EXTRAS, extras);
    510             mIControllerCallback.onEvent(SESSION_EVENT_ON_ERROR, bundle);
    511         }
    512 
    513         @Override
    514         void onCurrentMediaItemChanged(MediaItem2 item) throws RemoteException {
    515             Bundle bundle = new Bundle();
    516             bundle.putBundle(ARGUMENT_MEDIA_ITEM, (item == null) ? null : item.toBundle());
    517             mIControllerCallback.onEvent(SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle);
    518         }
    519 
    520         @Override
    521         void onPlaylistChanged(List<MediaItem2> playlist, MediaMetadata2 metadata)
    522                 throws RemoteException {
    523             Bundle bundle = new Bundle();
    524             bundle.putParcelableArray(ARGUMENT_PLAYLIST,
    525                     MediaUtils2.toMediaItem2ParcelableArray(playlist));
    526             bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
    527                     metadata == null ? null : metadata.toBundle());
    528             mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle);
    529         }
    530 
    531         @Override
    532         void onPlaylistMetadataChanged(MediaMetadata2 metadata) throws RemoteException {
    533             Bundle bundle = new Bundle();
    534             bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
    535                     metadata == null ? null : metadata.toBundle());
    536             mIControllerCallback.onEvent(SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle);
    537         }
    538 
    539         @Override
    540         void onShuffleModeChanged(int shuffleMode) throws RemoteException {
    541             Bundle bundle = new Bundle();
    542             bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
    543             mIControllerCallback.onEvent(SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle);
    544         }
    545 
    546         @Override
    547         void onRepeatModeChanged(int repeatMode) throws RemoteException {
    548             Bundle bundle = new Bundle();
    549             bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
    550             mIControllerCallback.onEvent(SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle);
    551         }
    552 
    553         @Override
    554         void onRoutesInfoChanged(List<Bundle> routes) throws RemoteException {
    555             Bundle bundle = null;
    556             if (routes != null) {
    557                 bundle = new Bundle();
    558                 bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0]));
    559             }
    560             mIControllerCallback.onEvent(SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle);
    561         }
    562 
    563         @Override
    564         void onChildrenChanged(String parentId, int itemCount, Bundle extras)
    565                 throws RemoteException {
    566             Bundle bundle = new Bundle();
    567             bundle.putString(ARGUMENT_MEDIA_ID, parentId);
    568             bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
    569             bundle.putBundle(ARGUMENT_EXTRAS, extras);
    570             mIControllerCallback.onEvent(SESSION_EVENT_ON_CHILDREN_CHANGED, bundle);
    571         }
    572 
    573         @Override
    574         void onSearchResultChanged(String query, int itemCount, Bundle extras)
    575                 throws RemoteException {
    576             Bundle bundle = new Bundle();
    577             bundle.putString(ARGUMENT_QUERY, query);
    578             bundle.putInt(ARGUMENT_ITEM_COUNT, itemCount);
    579             bundle.putBundle(ARGUMENT_EXTRAS, extras);
    580             mIControllerCallback.onEvent(SESSION_EVENT_ON_SEARCH_RESULT_CHANGED, bundle);
    581         }
    582     }
    583 }
    584