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