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 com.android.media; 18 19 import android.app.PendingIntent; 20 import android.content.Context; 21 import android.media.MediaController2; 22 import android.media.MediaItem2; 23 import android.media.MediaLibraryService2.LibraryRoot; 24 import android.media.MediaMetadata2; 25 import android.media.SessionCommand2; 26 import android.media.MediaSession2.CommandButton; 27 import android.media.SessionCommandGroup2; 28 import android.media.MediaSession2.ControllerInfo; 29 import android.media.Rating2; 30 import android.media.VolumeProvider2; 31 import android.net.Uri; 32 import android.os.Binder; 33 import android.os.Bundle; 34 import android.os.DeadObjectException; 35 import android.os.IBinder; 36 import android.os.RemoteException; 37 import android.os.ResultReceiver; 38 import android.support.annotation.GuardedBy; 39 import android.support.annotation.NonNull; 40 import android.text.TextUtils; 41 import android.util.ArrayMap; 42 import android.util.Log; 43 import android.util.SparseArray; 44 45 import com.android.media.MediaLibraryService2Impl.MediaLibrarySessionImpl; 46 import com.android.media.MediaSession2Impl.CommandButtonImpl; 47 import com.android.media.MediaSession2Impl.CommandGroupImpl; 48 import com.android.media.MediaSession2Impl.ControllerInfoImpl; 49 50 import java.lang.ref.WeakReference; 51 import java.util.ArrayList; 52 import java.util.HashSet; 53 import java.util.List; 54 import java.util.Set; 55 56 public class MediaSession2Stub extends IMediaSession2.Stub { 57 58 static final String ARGUMENT_KEY_POSITION = "android.media.media_session2.key_position"; 59 static final String ARGUMENT_KEY_ITEM_INDEX = "android.media.media_session2.key_item_index"; 60 static final String ARGUMENT_KEY_PLAYLIST_PARAMS = 61 "android.media.media_session2.key_playlist_params"; 62 63 private static final String TAG = "MediaSession2Stub"; 64 private static final boolean DEBUG = true; // TODO(jaewan): Rename. 65 66 private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest = 67 new SparseArray<>(); 68 69 private final Object mLock = new Object(); 70 private final WeakReference<MediaSession2Impl> mSession; 71 72 @GuardedBy("mLock") 73 private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>(); 74 @GuardedBy("mLock") 75 private final Set<IBinder> mConnectingControllers = new HashSet<>(); 76 @GuardedBy("mLock") 77 private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap = 78 new ArrayMap<>(); 79 @GuardedBy("mLock") 80 private final ArrayMap<ControllerInfo, Set<String>> mSubscriptions = new ArrayMap<>(); 81 82 public MediaSession2Stub(MediaSession2Impl session) { 83 mSession = new WeakReference<>(session); 84 85 synchronized (sCommandsForOnCommandRequest) { 86 if (sCommandsForOnCommandRequest.size() == 0) { 87 CommandGroupImpl group = new CommandGroupImpl(); 88 group.addAllPlaybackCommands(); 89 group.addAllPlaylistCommands(); 90 Set<SessionCommand2> commands = group.getCommands(); 91 for (SessionCommand2 command : commands) { 92 sCommandsForOnCommandRequest.append(command.getCommandCode(), command); 93 } 94 } 95 } 96 } 97 98 public void destroyNotLocked() { 99 final List<ControllerInfo> list; 100 synchronized (mLock) { 101 mSession.clear(); 102 list = getControllers(); 103 mControllers.clear(); 104 } 105 for (int i = 0; i < list.size(); i++) { 106 IMediaController2 controllerBinder = 107 ((ControllerInfoImpl) list.get(i).getProvider()).getControllerBinder(); 108 try { 109 // Should be used without a lock hold to prevent potential deadlock. 110 controllerBinder.onDisconnected(); 111 } catch (RemoteException e) { 112 // Controller is gone. Should be fine because we're destroying. 113 } 114 } 115 } 116 117 private MediaSession2Impl getSession() { 118 final MediaSession2Impl session = mSession.get(); 119 if (session == null && DEBUG) { 120 Log.d(TAG, "Session is closed", new IllegalStateException()); 121 } 122 return session; 123 } 124 125 private MediaLibrarySessionImpl getLibrarySession() throws IllegalStateException { 126 final MediaSession2Impl session = getSession(); 127 if (!(session instanceof MediaLibrarySessionImpl)) { 128 throw new RuntimeException("Session isn't a library session"); 129 } 130 return (MediaLibrarySessionImpl) session; 131 } 132 133 // Get controller if the command from caller to session is able to be handled. 134 private ControllerInfo getControllerIfAble(IMediaController2 caller) { 135 synchronized (mLock) { 136 final ControllerInfo controllerInfo = mControllers.get(caller.asBinder()); 137 if (controllerInfo == null && DEBUG) { 138 Log.d(TAG, "Controller is disconnected", new IllegalStateException()); 139 } 140 return controllerInfo; 141 } 142 } 143 144 // Get controller if the command from caller to session is able to be handled. 145 private ControllerInfo getControllerIfAble(IMediaController2 caller, int commandCode) { 146 synchronized (mLock) { 147 final ControllerInfo controllerInfo = getControllerIfAble(caller); 148 if (controllerInfo == null) { 149 return null; 150 } 151 SessionCommandGroup2 allowedCommands = mAllowedCommandGroupMap.get(controllerInfo); 152 if (allowedCommands == null) { 153 Log.w(TAG, "Controller with null allowed commands. Ignoring", 154 new IllegalStateException()); 155 return null; 156 } 157 if (!allowedCommands.hasCommand(commandCode)) { 158 if (DEBUG) { 159 Log.d(TAG, "Controller isn't allowed for command " + commandCode); 160 } 161 return null; 162 } 163 return controllerInfo; 164 } 165 } 166 167 // Get controller if the command from caller to session is able to be handled. 168 private ControllerInfo getControllerIfAble(IMediaController2 caller, SessionCommand2 command) { 169 synchronized (mLock) { 170 final ControllerInfo controllerInfo = getControllerIfAble(caller); 171 if (controllerInfo == null) { 172 return null; 173 } 174 SessionCommandGroup2 allowedCommands = mAllowedCommandGroupMap.get(controllerInfo); 175 if (allowedCommands == null) { 176 Log.w(TAG, "Controller with null allowed commands. Ignoring", 177 new IllegalStateException()); 178 return null; 179 } 180 if (!allowedCommands.hasCommand(command)) { 181 if (DEBUG) { 182 Log.d(TAG, "Controller isn't allowed for command " + command); 183 } 184 return null; 185 } 186 return controllerInfo; 187 } 188 } 189 190 // Return binder if the session is able to send a command to the controller. 191 private IMediaController2 getControllerBinderIfAble(ControllerInfo controller) { 192 if (getSession() == null) { 193 // getSession() already logged if session is closed. 194 return null; 195 } 196 final ControllerInfoImpl impl = ControllerInfoImpl.from(controller); 197 synchronized (mLock) { 198 if (mControllers.get(impl.getId()) != null 199 || mConnectingControllers.contains(impl.getId())) { 200 return impl.getControllerBinder(); 201 } 202 if (DEBUG) { 203 Log.d(TAG, controller + " isn't connected nor connecting", 204 new IllegalArgumentException()); 205 } 206 return null; 207 } 208 } 209 210 // Return binder if the session is able to send a command to the controller. 211 private IMediaController2 getControllerBinderIfAble(ControllerInfo controller, 212 int commandCode) { 213 synchronized (mLock) { 214 SessionCommandGroup2 allowedCommands = mAllowedCommandGroupMap.get(controller); 215 if (allowedCommands == null) { 216 Log.w(TAG, "Controller with null allowed commands. Ignoring"); 217 return null; 218 } 219 if (!allowedCommands.hasCommand(commandCode)) { 220 if (DEBUG) { 221 Log.d(TAG, "Controller isn't allowed for command " + commandCode); 222 } 223 return null; 224 } 225 return getControllerBinderIfAble(controller); 226 } 227 } 228 229 private void onCommand(@NonNull IMediaController2 caller, int commandCode, 230 @NonNull SessionRunnable runnable) { 231 final MediaSession2Impl session = getSession(); 232 final ControllerInfo controller = getControllerIfAble(caller, commandCode); 233 if (session == null || controller == null) { 234 return; 235 } 236 session.getCallbackExecutor().execute(() -> { 237 if (getControllerIfAble(caller, commandCode) == null) { 238 return; 239 } 240 SessionCommand2 command = sCommandsForOnCommandRequest.get(commandCode); 241 if (command != null) { 242 boolean accepted = session.getCallback().onCommandRequest(session.getInstance(), 243 controller, command); 244 if (!accepted) { 245 // Don't run rejected command. 246 if (DEBUG) { 247 Log.d(TAG, "Command (code=" + commandCode + ") from " 248 + controller + " was rejected by " + session); 249 } 250 return; 251 } 252 } 253 runnable.run(session, controller); 254 }); 255 } 256 257 private void onBrowserCommand(@NonNull IMediaController2 caller, 258 @NonNull LibrarySessionRunnable runnable) { 259 final MediaLibrarySessionImpl session = getLibrarySession(); 260 // TODO(jaewan): Consider command code 261 final ControllerInfo controller = getControllerIfAble(caller); 262 if (session == null || controller == null) { 263 return; 264 } 265 session.getCallbackExecutor().execute(() -> { 266 // TODO(jaewan): Consider command code 267 if (getControllerIfAble(caller) == null) { 268 return; 269 } 270 runnable.run(session, controller); 271 }); 272 } 273 274 275 private void notifyAll(int commandCode, @NonNull NotifyRunnable runnable) { 276 List<ControllerInfo> controllers = getControllers(); 277 for (int i = 0; i < controllers.size(); i++) { 278 notifyInternal(controllers.get(i), 279 getControllerBinderIfAble(controllers.get(i), commandCode), runnable); 280 } 281 } 282 283 private void notifyAll(@NonNull NotifyRunnable runnable) { 284 List<ControllerInfo> controllers = getControllers(); 285 for (int i = 0; i < controllers.size(); i++) { 286 notifyInternal(controllers.get(i), 287 getControllerBinderIfAble(controllers.get(i)), runnable); 288 } 289 } 290 291 private void notify(@NonNull ControllerInfo controller, @NonNull NotifyRunnable runnable) { 292 notifyInternal(controller, getControllerBinderIfAble(controller), runnable); 293 } 294 295 private void notify(@NonNull ControllerInfo controller, int commandCode, 296 @NonNull NotifyRunnable runnable) { 297 notifyInternal(controller, getControllerBinderIfAble(controller, commandCode), runnable); 298 } 299 300 // Do not call this API directly. Use notify() instead. 301 private void notifyInternal(@NonNull ControllerInfo controller, 302 @NonNull IMediaController2 iController, @NonNull NotifyRunnable runnable) { 303 if (controller == null || iController == null) { 304 return; 305 } 306 try { 307 runnable.run(controller, iController); 308 } catch (DeadObjectException e) { 309 if (DEBUG) { 310 Log.d(TAG, controller.toString() + " is gone", e); 311 } 312 onControllerClosed(iController); 313 } catch (RemoteException e) { 314 // Currently it's TransactionTooLargeException or DeadSystemException. 315 // We'd better to leave log for those cases because 316 // - TransactionTooLargeException means that we may need to fix our code. 317 // (e.g. add pagination or special way to deliver Bitmap) 318 // - DeadSystemException means that errors around it can be ignored. 319 Log.w(TAG, "Exception in " + controller.toString(), e); 320 } 321 } 322 323 private void onControllerClosed(IMediaController2 iController) { 324 ControllerInfo controller; 325 synchronized (mLock) { 326 controller = mControllers.remove(iController.asBinder()); 327 if (DEBUG) { 328 Log.d(TAG, "releasing " + controller); 329 } 330 mSubscriptions.remove(controller); 331 } 332 final MediaSession2Impl session = getSession(); 333 if (session == null || controller == null) { 334 return; 335 } 336 session.getCallbackExecutor().execute(() -> { 337 session.getCallback().onDisconnected(session.getInstance(), controller); 338 }); 339 } 340 341 ////////////////////////////////////////////////////////////////////////////////////////////// 342 // AIDL methods for session overrides 343 ////////////////////////////////////////////////////////////////////////////////////////////// 344 @Override 345 public void connect(final IMediaController2 caller, final String callingPackage) 346 throws RuntimeException { 347 final MediaSession2Impl session = getSession(); 348 if (session == null) { 349 return; 350 } 351 final Context context = session.getContext(); 352 final ControllerInfo controllerInfo = new ControllerInfo(context, 353 Binder.getCallingUid(), Binder.getCallingPid(), callingPackage, caller); 354 session.getCallbackExecutor().execute(() -> { 355 if (getSession() == null) { 356 return; 357 } 358 synchronized (mLock) { 359 // Keep connecting controllers. 360 // This helps sessions to call APIs in the onConnect() (e.g. setCustomLayout()) 361 // instead of pending them. 362 mConnectingControllers.add(ControllerInfoImpl.from(controllerInfo).getId()); 363 } 364 SessionCommandGroup2 allowedCommands = session.getCallback().onConnect( 365 session.getInstance(), controllerInfo); 366 // Don't reject connection for the request from trusted app. 367 // Otherwise server will fail to retrieve session's information to dispatch 368 // media keys to. 369 boolean accept = allowedCommands != null || controllerInfo.isTrusted(); 370 if (accept) { 371 ControllerInfoImpl controllerImpl = ControllerInfoImpl.from(controllerInfo); 372 if (DEBUG) { 373 Log.d(TAG, "Accepting connection, controllerInfo=" + controllerInfo 374 + " allowedCommands=" + allowedCommands); 375 } 376 if (allowedCommands == null) { 377 // For trusted apps, send non-null allowed commands to keep connection. 378 allowedCommands = new SessionCommandGroup2(); 379 } 380 synchronized (mLock) { 381 mConnectingControllers.remove(controllerImpl.getId()); 382 mControllers.put(controllerImpl.getId(), controllerInfo); 383 mAllowedCommandGroupMap.put(controllerInfo, allowedCommands); 384 } 385 // If connection is accepted, notify the current state to the controller. 386 // It's needed because we cannot call synchronous calls between session/controller. 387 // Note: We're doing this after the onConnectionChanged(), but there's no guarantee 388 // that events here are notified after the onConnected() because 389 // IMediaController2 is oneway (i.e. async call) and Stub will 390 // use thread poll for incoming calls. 391 final int playerState = session.getInstance().getPlayerState(); 392 final long positionEventTimeMs = System.currentTimeMillis(); 393 final long positionMs = session.getInstance().getCurrentPosition(); 394 final float playbackSpeed = session.getInstance().getPlaybackSpeed(); 395 final long bufferedPositionMs = session.getInstance().getBufferedPosition(); 396 final Bundle playbackInfoBundle = ((MediaController2Impl.PlaybackInfoImpl) 397 session.getPlaybackInfo().getProvider()).toBundle(); 398 final int repeatMode = session.getInstance().getRepeatMode(); 399 final int shuffleMode = session.getInstance().getShuffleMode(); 400 final PendingIntent sessionActivity = session.getSessionActivity(); 401 final List<MediaItem2> playlist = 402 allowedCommands.hasCommand(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST) 403 ? session.getInstance().getPlaylist() : null; 404 final List<Bundle> playlistBundle; 405 if (playlist != null) { 406 playlistBundle = new ArrayList<>(); 407 // TODO(jaewan): Find a way to avoid concurrent modification exception. 408 for (int i = 0; i < playlist.size(); i++) { 409 final MediaItem2 item = playlist.get(i); 410 if (item != null) { 411 final Bundle itemBundle = item.toBundle(); 412 if (itemBundle != null) { 413 playlistBundle.add(itemBundle); 414 } 415 } 416 } 417 } else { 418 playlistBundle = null; 419 } 420 421 // Double check if session is still there, because close() can be called in another 422 // thread. 423 if (getSession() == null) { 424 return; 425 } 426 try { 427 caller.onConnected(MediaSession2Stub.this, allowedCommands.toBundle(), 428 playerState, positionEventTimeMs, positionMs, playbackSpeed, 429 bufferedPositionMs, playbackInfoBundle, repeatMode, shuffleMode, 430 playlistBundle, sessionActivity); 431 } catch (RemoteException e) { 432 // Controller may be died prematurely. 433 // TODO(jaewan): Handle here. 434 } 435 } else { 436 synchronized (mLock) { 437 mConnectingControllers.remove(ControllerInfoImpl.from(controllerInfo).getId()); 438 } 439 if (DEBUG) { 440 Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo); 441 } 442 try { 443 caller.onDisconnected(); 444 } catch (RemoteException e) { 445 // Controller may be died prematurely. 446 // Not an issue because we'll ignore it anyway. 447 } 448 } 449 }); 450 } 451 452 @Override 453 public void release(final IMediaController2 caller) throws RemoteException { 454 onControllerClosed(caller); 455 } 456 457 @Override 458 public void setVolumeTo(final IMediaController2 caller, final int value, final int flags) 459 throws RuntimeException { 460 onCommand(caller, SessionCommand2.COMMAND_CODE_SET_VOLUME, 461 (session, controller) -> { 462 VolumeProvider2 volumeProvider = session.getVolumeProvider(); 463 if (volumeProvider == null) { 464 // TODO(jaewan): Set local stream volume 465 } else { 466 volumeProvider.onSetVolumeTo(value); 467 } 468 }); 469 } 470 471 @Override 472 public void adjustVolume(IMediaController2 caller, int direction, int flags) 473 throws RuntimeException { 474 onCommand(caller, SessionCommand2.COMMAND_CODE_SET_VOLUME, 475 (session, controller) -> { 476 VolumeProvider2 volumeProvider = session.getVolumeProvider(); 477 if (volumeProvider == null) { 478 // TODO(jaewan): Adjust local stream volume 479 } else { 480 volumeProvider.onAdjustVolume(direction); 481 } 482 }); 483 } 484 485 @Override 486 public void sendTransportControlCommand(IMediaController2 caller, 487 int commandCode, Bundle args) throws RuntimeException { 488 onCommand(caller, commandCode, (session, controller) -> { 489 switch (commandCode) { 490 case SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY: 491 session.getInstance().play(); 492 break; 493 case SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE: 494 session.getInstance().pause(); 495 break; 496 case SessionCommand2.COMMAND_CODE_PLAYBACK_STOP: 497 session.getInstance().stop(); 498 break; 499 case SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE: 500 session.getInstance().prepare(); 501 break; 502 case SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO: 503 session.getInstance().seekTo(args.getLong(ARGUMENT_KEY_POSITION)); 504 break; 505 default: 506 // TODO(jaewan): Resend unknown (new) commands through the custom command. 507 } 508 }); 509 } 510 511 @Override 512 public void sendCustomCommand(final IMediaController2 caller, final Bundle commandBundle, 513 final Bundle args, final ResultReceiver receiver) { 514 final MediaSession2Impl session = getSession(); 515 if (session == null) { 516 return; 517 } 518 final SessionCommand2 command = SessionCommand2.fromBundle(commandBundle); 519 if (command == null) { 520 Log.w(TAG, "sendCustomCommand(): Ignoring null command from " 521 + getControllerIfAble(caller)); 522 return; 523 } 524 final ControllerInfo controller = getControllerIfAble(caller, command); 525 if (controller == null) { 526 return; 527 } 528 session.getCallbackExecutor().execute(() -> { 529 if (getControllerIfAble(caller, command) == null) { 530 return; 531 } 532 session.getCallback().onCustomCommand(session.getInstance(), 533 controller, command, args, receiver); 534 }); 535 } 536 537 @Override 538 public void prepareFromUri(final IMediaController2 caller, final Uri uri, 539 final Bundle extras) { 540 onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI, 541 (session, controller) -> { 542 if (uri == null) { 543 Log.w(TAG, "prepareFromUri(): Ignoring null uri from " + controller); 544 return; 545 } 546 session.getCallback().onPrepareFromUri(session.getInstance(), controller, uri, 547 extras); 548 }); 549 } 550 551 @Override 552 public void prepareFromSearch(final IMediaController2 caller, final String query, 553 final Bundle extras) { 554 onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, 555 (session, controller) -> { 556 if (TextUtils.isEmpty(query)) { 557 Log.w(TAG, "prepareFromSearch(): Ignoring empty query from " + controller); 558 return; 559 } 560 session.getCallback().onPrepareFromSearch(session.getInstance(), 561 controller, query, extras); 562 }); 563 } 564 565 @Override 566 public void prepareFromMediaId(final IMediaController2 caller, final String mediaId, 567 final Bundle extras) { 568 onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, 569 (session, controller) -> { 570 if (mediaId == null) { 571 Log.w(TAG, "prepareFromMediaId(): Ignoring null mediaId from " + controller); 572 return; 573 } 574 session.getCallback().onPrepareFromMediaId(session.getInstance(), 575 controller, mediaId, extras); 576 }); 577 } 578 579 @Override 580 public void playFromUri(final IMediaController2 caller, final Uri uri, 581 final Bundle extras) { 582 onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI, 583 (session, controller) -> { 584 if (uri == null) { 585 Log.w(TAG, "playFromUri(): Ignoring null uri from " + controller); 586 return; 587 } 588 session.getCallback().onPlayFromUri(session.getInstance(), controller, uri, 589 extras); 590 }); 591 } 592 593 @Override 594 public void playFromSearch(final IMediaController2 caller, final String query, 595 final Bundle extras) { 596 onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, 597 (session, controller) -> { 598 if (TextUtils.isEmpty(query)) { 599 Log.w(TAG, "playFromSearch(): Ignoring empty query from " + controller); 600 return; 601 } 602 session.getCallback().onPlayFromSearch(session.getInstance(), 603 controller, query, extras); 604 }); 605 } 606 607 @Override 608 public void playFromMediaId(final IMediaController2 caller, final String mediaId, 609 final Bundle extras) { 610 onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, 611 (session, controller) -> { 612 if (mediaId == null) { 613 Log.w(TAG, "playFromMediaId(): Ignoring null mediaId from " + controller); 614 return; 615 } 616 session.getCallback().onPlayFromMediaId(session.getInstance(), controller, 617 mediaId, extras); 618 }); 619 } 620 621 @Override 622 public void setRating(final IMediaController2 caller, final String mediaId, 623 final Bundle ratingBundle) { 624 onCommand(caller, SessionCommand2.COMMAND_CODE_SESSION_SET_RATING, 625 (session, controller) -> { 626 if (mediaId == null) { 627 Log.w(TAG, "setRating(): Ignoring null mediaId from " + controller); 628 return; 629 } 630 if (ratingBundle == null) { 631 Log.w(TAG, "setRating(): Ignoring null ratingBundle from " + controller); 632 return; 633 } 634 Rating2 rating = Rating2.fromBundle(ratingBundle); 635 if (rating == null) { 636 if (ratingBundle == null) { 637 Log.w(TAG, "setRating(): Ignoring null rating from " + controller); 638 return; 639 } 640 return; 641 } 642 session.getCallback().onSetRating(session.getInstance(), controller, mediaId, 643 rating); 644 }); 645 } 646 647 @Override 648 public void setPlaylist(final IMediaController2 caller, final List<Bundle> playlist, 649 final Bundle metadata) { 650 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST, (session, controller) -> { 651 if (playlist == null) { 652 Log.w(TAG, "setPlaylist(): Ignoring null playlist from " + controller); 653 return; 654 } 655 List<MediaItem2> list = new ArrayList<>(); 656 for (int i = 0; i < playlist.size(); i++) { 657 // Recreates UUID in the playlist 658 MediaItem2 item = MediaItem2Impl.fromBundle(playlist.get(i), null); 659 if (item != null) { 660 list.add(item); 661 } 662 } 663 session.getInstance().setPlaylist(list, MediaMetadata2.fromBundle(metadata)); 664 }); 665 } 666 667 @Override 668 public void updatePlaylistMetadata(final IMediaController2 caller, final Bundle metadata) { 669 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, 670 (session, controller) -> { 671 session.getInstance().updatePlaylistMetadata(MediaMetadata2.fromBundle(metadata)); 672 }); 673 } 674 675 @Override 676 public void addPlaylistItem(IMediaController2 caller, int index, Bundle mediaItem) { 677 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM, 678 (session, controller) -> { 679 // Resets the UUID from the incoming media id, so controller may reuse a media 680 // item multiple times for addPlaylistItem. 681 session.getInstance().addPlaylistItem(index, 682 MediaItem2Impl.fromBundle(mediaItem, null)); 683 }); 684 } 685 686 @Override 687 public void removePlaylistItem(IMediaController2 caller, Bundle mediaItem) { 688 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM, 689 (session, controller) -> { 690 MediaItem2 item = MediaItem2.fromBundle(mediaItem); 691 // Note: MediaItem2 has hidden UUID to identify it across the processes. 692 session.getInstance().removePlaylistItem(item); 693 }); 694 } 695 696 @Override 697 public void replacePlaylistItem(IMediaController2 caller, int index, Bundle mediaItem) { 698 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM, 699 (session, controller) -> { 700 // Resets the UUID from the incoming media id, so controller may reuse a media 701 // item multiple times for replacePlaylistItem. 702 session.getInstance().replacePlaylistItem(index, 703 MediaItem2Impl.fromBundle(mediaItem, null)); 704 }); 705 } 706 707 @Override 708 public void skipToPlaylistItem(IMediaController2 caller, Bundle mediaItem) { 709 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, 710 (session, controller) -> { 711 if (mediaItem == null) { 712 Log.w(TAG, "skipToPlaylistItem(): Ignoring null mediaItem from " 713 + controller); 714 } 715 // Note: MediaItem2 has hidden UUID to identify it across the processes. 716 session.getInstance().skipToPlaylistItem(MediaItem2.fromBundle(mediaItem)); 717 }); 718 } 719 720 @Override 721 public void skipToPreviousItem(IMediaController2 caller) { 722 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_PREV_ITEM, 723 (session, controller) -> { 724 session.getInstance().skipToPreviousItem(); 725 }); 726 } 727 728 @Override 729 public void skipToNextItem(IMediaController2 caller) { 730 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_NEXT_ITEM, 731 (session, controller) -> { 732 session.getInstance().skipToNextItem(); 733 }); 734 } 735 736 @Override 737 public void setRepeatMode(IMediaController2 caller, int repeatMode) { 738 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, 739 (session, controller) -> { 740 session.getInstance().setRepeatMode(repeatMode); 741 }); 742 } 743 744 @Override 745 public void setShuffleMode(IMediaController2 caller, int shuffleMode) { 746 onCommand(caller, SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, 747 (session, controller) -> { 748 session.getInstance().setShuffleMode(shuffleMode); 749 }); 750 } 751 752 ////////////////////////////////////////////////////////////////////////////////////////////// 753 // AIDL methods for LibrarySession overrides 754 ////////////////////////////////////////////////////////////////////////////////////////////// 755 756 @Override 757 public void getLibraryRoot(final IMediaController2 caller, final Bundle rootHints) 758 throws RuntimeException { 759 onBrowserCommand(caller, (session, controller) -> { 760 final LibraryRoot root = session.getCallback().onGetLibraryRoot(session.getInstance(), 761 controller, rootHints); 762 notify(controller, (unused, iController) -> { 763 iController.onGetLibraryRootDone(rootHints, 764 root == null ? null : root.getRootId(), 765 root == null ? null : root.getExtras()); 766 }); 767 }); 768 } 769 770 @Override 771 public void getItem(final IMediaController2 caller, final String mediaId) 772 throws RuntimeException { 773 onBrowserCommand(caller, (session, controller) -> { 774 if (mediaId == null) { 775 if (DEBUG) { 776 Log.d(TAG, "mediaId shouldn't be null"); 777 } 778 return; 779 } 780 final MediaItem2 result = session.getCallback().onGetItem(session.getInstance(), 781 controller, mediaId); 782 notify(controller, (unused, iController) -> { 783 iController.onGetItemDone(mediaId, result == null ? null : result.toBundle()); 784 }); 785 }); 786 } 787 788 @Override 789 public void getChildren(final IMediaController2 caller, final String parentId, 790 final int page, final int pageSize, final Bundle extras) throws RuntimeException { 791 onBrowserCommand(caller, (session, controller) -> { 792 if (parentId == null) { 793 if (DEBUG) { 794 Log.d(TAG, "parentId shouldn't be null"); 795 } 796 return; 797 } 798 if (page < 1 || pageSize < 1) { 799 if (DEBUG) { 800 Log.d(TAG, "Neither page nor pageSize should be less than 1"); 801 } 802 return; 803 } 804 List<MediaItem2> result = session.getCallback().onGetChildren(session.getInstance(), 805 controller, parentId, page, pageSize, extras); 806 if (result != null && result.size() > pageSize) { 807 throw new IllegalArgumentException("onGetChildren() shouldn't return media items " 808 + "more than pageSize. result.size()=" + result.size() + " pageSize=" 809 + pageSize); 810 } 811 final List<Bundle> bundleList; 812 if (result != null) { 813 bundleList = new ArrayList<>(); 814 for (MediaItem2 item : result) { 815 bundleList.add(item == null ? null : item.toBundle()); 816 } 817 } else { 818 bundleList = null; 819 } 820 notify(controller, (unused, iController) -> { 821 iController.onGetChildrenDone(parentId, page, pageSize, bundleList, extras); 822 }); 823 }); 824 } 825 826 @Override 827 public void search(IMediaController2 caller, String query, Bundle extras) { 828 onBrowserCommand(caller, (session, controller) -> { 829 if (TextUtils.isEmpty(query)) { 830 Log.w(TAG, "search(): Ignoring empty query from " + controller); 831 return; 832 } 833 session.getCallback().onSearch(session.getInstance(), controller, query, extras); 834 }); 835 } 836 837 @Override 838 public void getSearchResult(final IMediaController2 caller, final String query, 839 final int page, final int pageSize, final Bundle extras) { 840 onBrowserCommand(caller, (session, controller) -> { 841 if (TextUtils.isEmpty(query)) { 842 Log.w(TAG, "getSearchResult(): Ignoring empty query from " + controller); 843 return; 844 } 845 if (page < 1 || pageSize < 1) { 846 Log.w(TAG, "getSearchResult(): Ignoring negative page / pageSize." 847 + " page=" + page + " pageSize=" + pageSize + " from " + controller); 848 return; 849 } 850 List<MediaItem2> result = session.getCallback().onGetSearchResult(session.getInstance(), 851 controller, query, page, pageSize, extras); 852 if (result != null && result.size() > pageSize) { 853 throw new IllegalArgumentException("onGetSearchResult() shouldn't return media " 854 + "items more than pageSize. result.size()=" + result.size() + " pageSize=" 855 + pageSize); 856 } 857 final List<Bundle> bundleList; 858 if (result != null) { 859 bundleList = new ArrayList<>(); 860 for (MediaItem2 item : result) { 861 bundleList.add(item == null ? null : item.toBundle()); 862 } 863 } else { 864 bundleList = null; 865 } 866 notify(controller, (unused, iController) -> { 867 iController.onGetSearchResultDone(query, page, pageSize, bundleList, extras); 868 }); 869 }); 870 } 871 872 @Override 873 public void subscribe(final IMediaController2 caller, final String parentId, 874 final Bundle option) { 875 onBrowserCommand(caller, (session, controller) -> { 876 if (parentId == null) { 877 Log.w(TAG, "subscribe(): Ignoring null parentId from " + controller); 878 return; 879 } 880 session.getCallback().onSubscribe(session.getInstance(), 881 controller, parentId, option); 882 synchronized (mLock) { 883 Set<String> subscription = mSubscriptions.get(controller); 884 if (subscription == null) { 885 subscription = new HashSet<>(); 886 mSubscriptions.put(controller, subscription); 887 } 888 subscription.add(parentId); 889 } 890 }); 891 } 892 893 @Override 894 public void unsubscribe(final IMediaController2 caller, final String parentId) { 895 onBrowserCommand(caller, (session, controller) -> { 896 if (parentId == null) { 897 Log.w(TAG, "unsubscribe(): Ignoring null parentId from " + controller); 898 return; 899 } 900 session.getCallback().onUnsubscribe(session.getInstance(), controller, parentId); 901 synchronized (mLock) { 902 mSubscriptions.remove(controller); 903 } 904 }); 905 } 906 907 ////////////////////////////////////////////////////////////////////////////////////////////// 908 // APIs for MediaSession2Impl 909 ////////////////////////////////////////////////////////////////////////////////////////////// 910 911 // TODO(jaewan): (Can be Post-P) Need a way to get controller with permissions 912 public List<ControllerInfo> getControllers() { 913 ArrayList<ControllerInfo> controllers = new ArrayList<>(); 914 synchronized (mLock) { 915 for (int i = 0; i < mControllers.size(); i++) { 916 controllers.add(mControllers.valueAt(i)); 917 } 918 } 919 return controllers; 920 } 921 922 // Should be used without a lock to prevent potential deadlock. 923 public void notifyPlayerStateChangedNotLocked(int state) { 924 notifyAll((controller, iController) -> { 925 iController.onPlayerStateChanged(state); 926 }); 927 } 928 929 // TODO(jaewan): Rename 930 public void notifyPositionChangedNotLocked(long eventTimeMs, long positionMs) { 931 notifyAll((controller, iController) -> { 932 iController.onPositionChanged(eventTimeMs, positionMs); 933 }); 934 } 935 936 public void notifyPlaybackSpeedChangedNotLocked(float speed) { 937 notifyAll((controller, iController) -> { 938 iController.onPlaybackSpeedChanged(speed); 939 }); 940 } 941 942 public void notifyBufferedPositionChangedNotLocked(long bufferedPositionMs) { 943 notifyAll((controller, iController) -> { 944 iController.onBufferedPositionChanged(bufferedPositionMs); 945 }); 946 } 947 948 public void notifyCustomLayoutNotLocked(ControllerInfo controller, List<CommandButton> layout) { 949 notify(controller, (unused, iController) -> { 950 List<Bundle> layoutBundles = new ArrayList<>(); 951 for (int i = 0; i < layout.size(); i++) { 952 Bundle bundle = ((CommandButtonImpl) layout.get(i).getProvider()).toBundle(); 953 if (bundle != null) { 954 layoutBundles.add(bundle); 955 } 956 } 957 iController.onCustomLayoutChanged(layoutBundles); 958 }); 959 } 960 961 public void notifyPlaylistChangedNotLocked(List<MediaItem2> playlist, MediaMetadata2 metadata) { 962 final List<Bundle> bundleList; 963 if (playlist != null) { 964 bundleList = new ArrayList<>(); 965 for (int i = 0; i < playlist.size(); i++) { 966 if (playlist.get(i) != null) { 967 Bundle bundle = playlist.get(i).toBundle(); 968 if (bundle != null) { 969 bundleList.add(bundle); 970 } 971 } 972 } 973 } else { 974 bundleList = null; 975 } 976 final Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle(); 977 notifyAll((controller, iController) -> { 978 if (getControllerBinderIfAble(controller, 979 SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST) != null) { 980 iController.onPlaylistChanged(bundleList, metadataBundle); 981 } else if (getControllerBinderIfAble(controller, 982 SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA) != null) { 983 iController.onPlaylistMetadataChanged(metadataBundle); 984 } 985 }); 986 } 987 988 public void notifyPlaylistMetadataChangedNotLocked(MediaMetadata2 metadata) { 989 final Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle(); 990 notifyAll(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA, 991 (unused, iController) -> { 992 iController.onPlaylistMetadataChanged(metadataBundle); 993 }); 994 } 995 996 public void notifyRepeatModeChangedNotLocked(int repeatMode) { 997 notifyAll((unused, iController) -> { 998 iController.onRepeatModeChanged(repeatMode); 999 }); 1000 } 1001 1002 public void notifyShuffleModeChangedNotLocked(int shuffleMode) { 1003 notifyAll((unused, iController) -> { 1004 iController.onShuffleModeChanged(shuffleMode); 1005 }); 1006 } 1007 1008 public void notifyPlaybackInfoChanged(MediaController2.PlaybackInfo playbackInfo) { 1009 final Bundle playbackInfoBundle = 1010 ((MediaController2Impl.PlaybackInfoImpl) playbackInfo.getProvider()).toBundle(); 1011 notifyAll((unused, iController) -> { 1012 iController.onPlaybackInfoChanged(playbackInfoBundle); 1013 }); 1014 } 1015 1016 public void setAllowedCommands(ControllerInfo controller, SessionCommandGroup2 commands) { 1017 synchronized (mLock) { 1018 mAllowedCommandGroupMap.put(controller, commands); 1019 } 1020 notify(controller, (unused, iController) -> { 1021 iController.onAllowedCommandsChanged(commands.toBundle()); 1022 }); 1023 } 1024 1025 public void sendCustomCommand(ControllerInfo controller, SessionCommand2 command, Bundle args, 1026 ResultReceiver receiver) { 1027 if (receiver != null && controller == null) { 1028 throw new IllegalArgumentException("Controller shouldn't be null if result receiver is" 1029 + " specified"); 1030 } 1031 if (command == null) { 1032 throw new IllegalArgumentException("command shouldn't be null"); 1033 } 1034 notify(controller, (unused, iController) -> { 1035 Bundle commandBundle = command.toBundle(); 1036 iController.onCustomCommand(commandBundle, args, null); 1037 }); 1038 } 1039 1040 public void sendCustomCommand(SessionCommand2 command, Bundle args) { 1041 if (command == null) { 1042 throw new IllegalArgumentException("command shouldn't be null"); 1043 } 1044 Bundle commandBundle = command.toBundle(); 1045 notifyAll((unused, iController) -> { 1046 iController.onCustomCommand(commandBundle, args, null); 1047 }); 1048 } 1049 1050 public void notifyError(int errorCode, Bundle extras) { 1051 notifyAll((unused, iController) -> { 1052 iController.onError(errorCode, extras); 1053 }); 1054 } 1055 1056 ////////////////////////////////////////////////////////////////////////////////////////////// 1057 // APIs for MediaLibrarySessionImpl 1058 ////////////////////////////////////////////////////////////////////////////////////////////// 1059 1060 public void notifySearchResultChanged(ControllerInfo controller, String query, int itemCount, 1061 Bundle extras) { 1062 notify(controller, (unused, iController) -> { 1063 iController.onSearchResultChanged(query, itemCount, extras); 1064 }); 1065 } 1066 1067 public void notifyChildrenChangedNotLocked(ControllerInfo controller, String parentId, 1068 int itemCount, Bundle extras) { 1069 notify(controller, (unused, iController) -> { 1070 if (isSubscribed(controller, parentId)) { 1071 iController.onChildrenChanged(parentId, itemCount, extras); 1072 } 1073 }); 1074 } 1075 1076 public void notifyChildrenChangedNotLocked(String parentId, int itemCount, Bundle extras) { 1077 notifyAll((controller, iController) -> { 1078 if (isSubscribed(controller, parentId)) { 1079 iController.onChildrenChanged(parentId, itemCount, extras); 1080 } 1081 }); 1082 } 1083 1084 private boolean isSubscribed(ControllerInfo controller, String parentId) { 1085 synchronized (mLock) { 1086 Set<String> subscriptions = mSubscriptions.get(controller); 1087 if (subscriptions == null || !subscriptions.contains(parentId)) { 1088 return false; 1089 } 1090 } 1091 return true; 1092 } 1093 1094 ////////////////////////////////////////////////////////////////////////////////////////////// 1095 // Misc 1096 ////////////////////////////////////////////////////////////////////////////////////////////// 1097 1098 @FunctionalInterface 1099 private interface SessionRunnable { 1100 void run(final MediaSession2Impl session, final ControllerInfo controller); 1101 } 1102 1103 @FunctionalInterface 1104 private interface LibrarySessionRunnable { 1105 void run(final MediaLibrarySessionImpl session, final ControllerInfo controller); 1106 } 1107 1108 @FunctionalInterface 1109 private interface NotifyRunnable { 1110 void run(final ControllerInfo controller, 1111 final IMediaController2 iController) throws RemoteException; 1112 } 1113 } 1114