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 MediaSession2Stub(MediaSession2Impl session)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 destroyNotLocked()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 getSession()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 getLibrarySession()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. getControllerIfAble(IMediaController2 caller)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. getControllerIfAble(IMediaController2 caller, int commandCode)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. getControllerIfAble(IMediaController2 caller, SessionCommand2 command)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. getControllerBinderIfAble(ControllerInfo 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. getControllerBinderIfAble(ControllerInfo controller, int commandCode)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 onCommand(@onNull IMediaController2 caller, int commandCode, @NonNull SessionRunnable runnable)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 onBrowserCommand(@onNull IMediaController2 caller, @NonNull LibrarySessionRunnable runnable)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 notifyAll(int commandCode, @NonNull NotifyRunnable runnable)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 notifyAll(@onNull NotifyRunnable runnable)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 notify(@onNull ControllerInfo controller, @NonNull NotifyRunnable runnable)291 private void notify(@NonNull ControllerInfo controller, @NonNull NotifyRunnable runnable) { 292 notifyInternal(controller, getControllerBinderIfAble(controller), runnable); 293 } 294 notify(@onNull ControllerInfo controller, int commandCode, @NonNull NotifyRunnable runnable)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. notifyInternal(@onNull ControllerInfo controller, @NonNull IMediaController2 iController, @NonNull NotifyRunnable runnable)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 onControllerClosed(IMediaController2 iController)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 connect(final IMediaController2 caller, final String callingPackage)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 release(final IMediaController2 caller)453 public void release(final IMediaController2 caller) throws RemoteException { 454 onControllerClosed(caller); 455 } 456 457 @Override setVolumeTo(final IMediaController2 caller, final int value, final int flags)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 adjustVolume(IMediaController2 caller, int direction, int flags)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 sendTransportControlCommand(IMediaController2 caller, int commandCode, Bundle args)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 sendCustomCommand(final IMediaController2 caller, final Bundle commandBundle, final Bundle args, final ResultReceiver receiver)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 prepareFromUri(final IMediaController2 caller, final Uri uri, final Bundle extras)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 prepareFromSearch(final IMediaController2 caller, final String query, final Bundle extras)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 prepareFromMediaId(final IMediaController2 caller, final String mediaId, final Bundle extras)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 playFromUri(final IMediaController2 caller, final Uri uri, final Bundle extras)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 playFromSearch(final IMediaController2 caller, final String query, final Bundle extras)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 playFromMediaId(final IMediaController2 caller, final String mediaId, final Bundle extras)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 setRating(final IMediaController2 caller, final String mediaId, final Bundle ratingBundle)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 setPlaylist(final IMediaController2 caller, final List<Bundle> playlist, final Bundle metadata)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 updatePlaylistMetadata(final IMediaController2 caller, final Bundle metadata)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 addPlaylistItem(IMediaController2 caller, int index, Bundle mediaItem)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 removePlaylistItem(IMediaController2 caller, Bundle mediaItem)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 replacePlaylistItem(IMediaController2 caller, int index, Bundle mediaItem)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 skipToPlaylistItem(IMediaController2 caller, Bundle mediaItem)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 skipToPreviousItem(IMediaController2 caller)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 skipToNextItem(IMediaController2 caller)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 setRepeatMode(IMediaController2 caller, int repeatMode)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 setShuffleMode(IMediaController2 caller, int shuffleMode)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 getLibraryRoot(final IMediaController2 caller, final Bundle rootHints)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 getItem(final IMediaController2 caller, final String mediaId)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 getChildren(final IMediaController2 caller, final String parentId, final int page, final int pageSize, final Bundle extras)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 search(IMediaController2 caller, String query, Bundle extras)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 getSearchResult(final IMediaController2 caller, final String query, final int page, final int pageSize, final Bundle extras)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 subscribe(final IMediaController2 caller, final String parentId, final Bundle option)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 unsubscribe(final IMediaController2 caller, final String parentId)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 getControllers()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. notifyPlayerStateChangedNotLocked(int state)923 public void notifyPlayerStateChangedNotLocked(int state) { 924 notifyAll((controller, iController) -> { 925 iController.onPlayerStateChanged(state); 926 }); 927 } 928 929 // TODO(jaewan): Rename notifyPositionChangedNotLocked(long eventTimeMs, long positionMs)930 public void notifyPositionChangedNotLocked(long eventTimeMs, long positionMs) { 931 notifyAll((controller, iController) -> { 932 iController.onPositionChanged(eventTimeMs, positionMs); 933 }); 934 } 935 notifyPlaybackSpeedChangedNotLocked(float speed)936 public void notifyPlaybackSpeedChangedNotLocked(float speed) { 937 notifyAll((controller, iController) -> { 938 iController.onPlaybackSpeedChanged(speed); 939 }); 940 } 941 notifyBufferedPositionChangedNotLocked(long bufferedPositionMs)942 public void notifyBufferedPositionChangedNotLocked(long bufferedPositionMs) { 943 notifyAll((controller, iController) -> { 944 iController.onBufferedPositionChanged(bufferedPositionMs); 945 }); 946 } 947 notifyCustomLayoutNotLocked(ControllerInfo controller, List<CommandButton> layout)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 notifyPlaylistChangedNotLocked(List<MediaItem2> playlist, MediaMetadata2 metadata)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 notifyPlaylistMetadataChangedNotLocked(MediaMetadata2 metadata)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 notifyRepeatModeChangedNotLocked(int repeatMode)996 public void notifyRepeatModeChangedNotLocked(int repeatMode) { 997 notifyAll((unused, iController) -> { 998 iController.onRepeatModeChanged(repeatMode); 999 }); 1000 } 1001 notifyShuffleModeChangedNotLocked(int shuffleMode)1002 public void notifyShuffleModeChangedNotLocked(int shuffleMode) { 1003 notifyAll((unused, iController) -> { 1004 iController.onShuffleModeChanged(shuffleMode); 1005 }); 1006 } 1007 notifyPlaybackInfoChanged(MediaController2.PlaybackInfo playbackInfo)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 setAllowedCommands(ControllerInfo controller, SessionCommandGroup2 commands)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 sendCustomCommand(ControllerInfo controller, SessionCommand2 command, Bundle args, ResultReceiver receiver)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 sendCustomCommand(SessionCommand2 command, Bundle args)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 notifyError(int errorCode, Bundle extras)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 notifySearchResultChanged(ControllerInfo controller, String query, int itemCount, Bundle extras)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 notifyChildrenChangedNotLocked(ControllerInfo controller, String parentId, int itemCount, Bundle extras)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 notifyChildrenChangedNotLocked(String parentId, int itemCount, Bundle extras)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 isSubscribed(ControllerInfo controller, String parentId)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 { run(final MediaSession2Impl session, final ControllerInfo controller)1100 void run(final MediaSession2Impl session, final ControllerInfo controller); 1101 } 1102 1103 @FunctionalInterface 1104 private interface LibrarySessionRunnable { run(final MediaLibrarySessionImpl session, final ControllerInfo controller)1105 void run(final MediaLibrarySessionImpl session, final ControllerInfo controller); 1106 } 1107 1108 @FunctionalInterface 1109 private interface NotifyRunnable { run(final ControllerInfo controller, final IMediaController2 iController)1110 void run(final ControllerInfo controller, 1111 final IMediaController2 iController) throws RemoteException; 1112 } 1113 } 1114