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 17package androidx.media; 18 19import static androidx.media.MediaPlayerInterface.BUFFERING_STATE_UNKNOWN; 20import static androidx.media.MediaSession2.ControllerCb; 21import static androidx.media.MediaSession2.ControllerInfo; 22import static androidx.media.MediaSession2.ErrorCode; 23import static androidx.media.MediaSession2.OnDataSourceMissingHelper; 24import static androidx.media.MediaSession2.SessionCallback; 25import static androidx.media.SessionToken2.TYPE_LIBRARY_SERVICE; 26import static androidx.media.SessionToken2.TYPE_SESSION; 27import static androidx.media.SessionToken2.TYPE_SESSION_SERVICE; 28 29import android.annotation.TargetApi; 30import android.app.PendingIntent; 31import android.content.Context; 32import android.content.Intent; 33import android.content.pm.PackageManager; 34import android.content.pm.ResolveInfo; 35import android.media.AudioFocusRequest; 36import android.media.AudioManager; 37import android.os.Build; 38import android.os.Bundle; 39import android.os.DeadObjectException; 40import android.os.Handler; 41import android.os.HandlerThread; 42import android.os.Process; 43import android.os.RemoteException; 44import android.os.ResultReceiver; 45import android.support.v4.media.session.MediaSessionCompat; 46import android.support.v4.media.session.PlaybackStateCompat; 47import android.text.TextUtils; 48import android.util.Log; 49 50import androidx.annotation.GuardedBy; 51import androidx.annotation.NonNull; 52import androidx.annotation.Nullable; 53import androidx.core.util.ObjectsCompat; 54import androidx.media.MediaController2.PlaybackInfo; 55import androidx.media.MediaPlayerInterface.PlayerEventCallback; 56import androidx.media.MediaPlaylistAgent.PlaylistEventCallback; 57 58import java.lang.ref.WeakReference; 59import java.util.List; 60import java.util.NoSuchElementException; 61import java.util.concurrent.Executor; 62import java.util.concurrent.RejectedExecutionException; 63 64@TargetApi(Build.VERSION_CODES.KITKAT) 65class MediaSession2ImplBase extends MediaSession2.SupportLibraryImpl { 66 static final String TAG = "MS2ImplBase"; 67 static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 68 69 private final Object mLock = new Object(); 70 71 private final Context mContext; 72 private final HandlerThread mHandlerThread; 73 private final Handler mHandler; 74 private final MediaSessionCompat mSessionCompat; 75 private final MediaSession2Stub mSession2Stub; 76 private final MediaSessionLegacyStub mSessionLegacyStub; 77 private final String mId; 78 private final Executor mCallbackExecutor; 79 private final SessionCallback mCallback; 80 private final SessionToken2 mSessionToken; 81 private final AudioManager mAudioManager; 82 private final MediaPlayerInterface.PlayerEventCallback mPlayerEventCallback; 83 private final MediaPlaylistAgent.PlaylistEventCallback mPlaylistEventCallback; 84 private final MediaSession2 mInstance; 85 86 @GuardedBy("mLock") 87 private MediaPlayerInterface mPlayer; 88 @GuardedBy("mLock") 89 private MediaPlaylistAgent mPlaylistAgent; 90 @GuardedBy("mLock") 91 private SessionPlaylistAgentImplBase mSessionPlaylistAgent; 92 @GuardedBy("mLock") 93 private VolumeProviderCompat mVolumeProvider; 94 @GuardedBy("mLock") 95 private OnDataSourceMissingHelper mDsmHelper; 96 @GuardedBy("mLock") 97 private PlaybackInfo mPlaybackInfo; 98 99 MediaSession2ImplBase(Context context, MediaSessionCompat sessionCompat, String id, 100 MediaPlayerInterface player, MediaPlaylistAgent playlistAgent, 101 VolumeProviderCompat volumeProvider, PendingIntent sessionActivity, 102 Executor callbackExecutor, SessionCallback callback) { 103 mContext = context; 104 mInstance = createInstance(); 105 mHandlerThread = new HandlerThread("MediaController2_Thread"); 106 mHandlerThread.start(); 107 mHandler = new Handler(mHandlerThread.getLooper()); 108 109 mSessionCompat = sessionCompat; 110 mSession2Stub = new MediaSession2Stub(this); 111 mSessionLegacyStub = new MediaSessionLegacyStub(this); 112 mSessionCompat.setCallback(mSession2Stub, mHandler); 113 mSessionCompat.setSessionActivity(sessionActivity); 114 115 mId = id; 116 mCallback = callback; 117 mCallbackExecutor = callbackExecutor; 118 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 119 120 mPlayerEventCallback = new MyPlayerEventCallback(this); 121 mPlaylistEventCallback = new MyPlaylistEventCallback(this); 122 123 // Infer type from the id and package name. 124 String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id); 125 String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id); 126 if (sessionService != null && libraryService != null) { 127 throw new IllegalArgumentException("Ambiguous session type. Multiple" 128 + " session services define the same id=" + id); 129 } else if (libraryService != null) { 130 mSessionToken = new SessionToken2(Process.myUid(), TYPE_LIBRARY_SERVICE, 131 context.getPackageName(), libraryService, id, mSessionCompat.getSessionToken()); 132 } else if (sessionService != null) { 133 mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION_SERVICE, 134 context.getPackageName(), sessionService, id, mSessionCompat.getSessionToken()); 135 } else { 136 mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION, 137 context.getPackageName(), null, id, mSessionCompat.getSessionToken()); 138 } 139 updatePlayer(player, playlistAgent, volumeProvider); 140 } 141 142 @Override 143 public void updatePlayer(@NonNull MediaPlayerInterface player, 144 @Nullable MediaPlaylistAgent playlistAgent, 145 @Nullable VolumeProviderCompat volumeProvider) { 146 if (player == null) { 147 throw new IllegalArgumentException("player shouldn't be null"); 148 } 149 final boolean hasPlayerChanged; 150 final boolean hasAgentChanged; 151 final boolean hasPlaybackInfoChanged; 152 final MediaPlayerInterface oldPlayer; 153 final MediaPlaylistAgent oldAgent; 154 final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes()); 155 synchronized (mLock) { 156 hasPlayerChanged = (mPlayer != player); 157 hasAgentChanged = (mPlaylistAgent != playlistAgent); 158 hasPlaybackInfoChanged = (mPlaybackInfo != info); 159 oldPlayer = mPlayer; 160 oldAgent = mPlaylistAgent; 161 mPlayer = player; 162 if (playlistAgent == null) { 163 mSessionPlaylistAgent = new SessionPlaylistAgentImplBase(this, mPlayer); 164 if (mDsmHelper != null) { 165 mSessionPlaylistAgent.setOnDataSourceMissingHelper(mDsmHelper); 166 } 167 playlistAgent = mSessionPlaylistAgent; 168 } 169 mPlaylistAgent = playlistAgent; 170 mVolumeProvider = volumeProvider; 171 mPlaybackInfo = info; 172 } 173 if (volumeProvider == null) { 174 int stream = getLegacyStreamType(player.getAudioAttributes()); 175 mSessionCompat.setPlaybackToLocal(stream); 176 } 177 if (player != oldPlayer) { 178 player.registerPlayerEventCallback(mCallbackExecutor, mPlayerEventCallback); 179 if (oldPlayer != null) { 180 // Warning: Poorly implement player may ignore this 181 oldPlayer.unregisterPlayerEventCallback(mPlayerEventCallback); 182 } 183 } 184 if (playlistAgent != oldAgent) { 185 playlistAgent.registerPlaylistEventCallback(mCallbackExecutor, mPlaylistEventCallback); 186 if (oldAgent != null) { 187 // Warning: Poorly implement agent may ignore this 188 oldAgent.unregisterPlaylistEventCallback(mPlaylistEventCallback); 189 } 190 } 191 192 if (oldPlayer != null) { 193 // If it's not the first updatePlayer(), tell changes in the player, agent, and playback 194 // info. 195 if (hasAgentChanged) { 196 // Update agent first. Otherwise current position may be changed off the current 197 // media item's duration, and controller may consider it as a bug. 198 notifyAgentUpdatedNotLocked(oldAgent); 199 } 200 if (hasPlayerChanged) { 201 notifyPlayerUpdatedNotLocked(oldPlayer); 202 } 203 if (hasPlaybackInfoChanged) { 204 // Currently hasPlaybackInfo is always true, but check this in case that we're 205 // adding PlaybackInfo#equals(). 206 notifyToAllControllers(new NotifyRunnable() { 207 @Override 208 public void run(ControllerCb callback) throws RemoteException { 209 callback.onPlaybackInfoChanged(info); 210 } 211 }); 212 } 213 } 214 } 215 216 private PlaybackInfo createPlaybackInfo(VolumeProviderCompat volumeProvider, 217 AudioAttributesCompat attrs) { 218 PlaybackInfo info; 219 if (volumeProvider == null) { 220 int stream = getLegacyStreamType(attrs); 221 int controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; 222 if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) { 223 controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED; 224 } 225 info = PlaybackInfo.createPlaybackInfo( 226 PlaybackInfo.PLAYBACK_TYPE_LOCAL, 227 attrs, 228 controlType, 229 mAudioManager.getStreamMaxVolume(stream), 230 mAudioManager.getStreamVolume(stream)); 231 } else { 232 info = PlaybackInfo.createPlaybackInfo( 233 PlaybackInfo.PLAYBACK_TYPE_REMOTE, 234 attrs, 235 volumeProvider.getVolumeControl(), 236 volumeProvider.getMaxVolume(), 237 volumeProvider.getCurrentVolume()); 238 } 239 return info; 240 } 241 242 private int getLegacyStreamType(@Nullable AudioAttributesCompat attrs) { 243 int stream; 244 if (attrs == null) { 245 stream = AudioManager.STREAM_MUSIC; 246 } else { 247 stream = attrs.getLegacyStreamType(); 248 if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) { 249 // Usually, AudioAttributesCompat#getLegacyStreamType() does not return 250 // USE_DEFAULT_STREAM_TYPE unless the developer sets it with 251 // AudioAttributesCompat.Builder#setLegacyStreamType(). 252 // But for safety, let's convert USE_DEFAULT_STREAM_TYPE to STREAM_MUSIC here. 253 stream = AudioManager.STREAM_MUSIC; 254 } 255 } 256 return stream; 257 } 258 259 @Override 260 public void close() { 261 synchronized (mLock) { 262 if (mPlayer == null) { 263 return; 264 } 265 mPlayer.unregisterPlayerEventCallback(mPlayerEventCallback); 266 mPlayer = null; 267 mSessionCompat.release(); 268 mHandler.removeCallbacksAndMessages(null); 269 if (mHandlerThread.isAlive()) { 270 if (Build.VERSION.SDK_INT >= 18) { 271 mHandlerThread.quitSafely(); 272 } else { 273 mHandlerThread.quit(); 274 } 275 } 276 } 277 } 278 279 @Override 280 public @NonNull MediaPlayerInterface getPlayer() { 281 synchronized (mLock) { 282 return mPlayer; 283 } 284 } 285 286 @Override 287 public @NonNull MediaPlaylistAgent getPlaylistAgent() { 288 synchronized (mLock) { 289 return mPlaylistAgent; 290 } 291 } 292 293 @Override 294 public @Nullable VolumeProviderCompat getVolumeProvider() { 295 synchronized (mLock) { 296 return mVolumeProvider; 297 } 298 } 299 300 @Override 301 public @NonNull SessionToken2 getToken() { 302 return mSessionToken; 303 } 304 305 @Override 306 public @NonNull List<ControllerInfo> getConnectedControllers() { 307 return mSession2Stub.getConnectedControllers(); 308 } 309 310 @Override 311 public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) { 312 } 313 314 @Override 315 public void setCustomLayout(@NonNull ControllerInfo controller, 316 @NonNull final List<MediaSession2.CommandButton> layout) { 317 if (controller == null) { 318 throw new IllegalArgumentException("controller shouldn't be null"); 319 } 320 if (layout == null) { 321 throw new IllegalArgumentException("layout shouldn't be null"); 322 } 323 notifyToController(controller, new NotifyRunnable() { 324 @Override 325 public void run(ControllerCb callback) throws RemoteException { 326 callback.onCustomLayoutChanged(layout); 327 } 328 }); 329 } 330 331 @Override 332 public void setAllowedCommands(@NonNull ControllerInfo controller, 333 @NonNull final SessionCommandGroup2 commands) { 334 if (controller == null) { 335 throw new IllegalArgumentException("controller shouldn't be null"); 336 } 337 if (commands == null) { 338 throw new IllegalArgumentException("commands shouldn't be null"); 339 } 340 mSession2Stub.setAllowedCommands(controller, commands); 341 notifyToController(controller, new NotifyRunnable() { 342 @Override 343 public void run(ControllerCb callback) throws RemoteException { 344 callback.onAllowedCommandsChanged(commands); 345 } 346 }); 347 } 348 349 @Override 350 public void sendCustomCommand(@NonNull final SessionCommand2 command, 351 @Nullable final Bundle args) { 352 if (command == null) { 353 throw new IllegalArgumentException("command shouldn't be null"); 354 } 355 notifyToAllControllers(new NotifyRunnable() { 356 @Override 357 public void run(ControllerCb callback) throws RemoteException { 358 callback.onCustomCommand(command, args, null); 359 } 360 }); 361 } 362 363 @Override 364 public void sendCustomCommand(@NonNull ControllerInfo controller, 365 @NonNull final SessionCommand2 command, @Nullable final Bundle args, 366 @Nullable final ResultReceiver receiver) { 367 if (controller == null) { 368 throw new IllegalArgumentException("controller shouldn't be null"); 369 } 370 if (command == null) { 371 throw new IllegalArgumentException("command shouldn't be null"); 372 } 373 notifyToController(controller, new NotifyRunnable() { 374 @Override 375 public void run(ControllerCb callback) throws RemoteException { 376 callback.onCustomCommand(command, args, receiver); 377 } 378 }); 379 } 380 381 @Override 382 public void play() { 383 MediaPlayerInterface player; 384 synchronized (mLock) { 385 player = mPlayer; 386 } 387 if (player != null) { 388 player.play(); 389 } else if (DEBUG) { 390 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 391 } 392 } 393 394 @Override 395 public void pause() { 396 MediaPlayerInterface player; 397 synchronized (mLock) { 398 player = mPlayer; 399 } 400 if (player != null) { 401 player.pause(); 402 } else if (DEBUG) { 403 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 404 } 405 } 406 407 @Override 408 public void reset() { 409 MediaPlayerInterface player; 410 synchronized (mLock) { 411 player = mPlayer; 412 } 413 if (player != null) { 414 player.reset(); 415 } else if (DEBUG) { 416 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 417 } 418 } 419 420 @Override 421 public void prepare() { 422 MediaPlayerInterface player; 423 synchronized (mLock) { 424 player = mPlayer; 425 } 426 if (player != null) { 427 player.prepare(); 428 } else if (DEBUG) { 429 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 430 } 431 } 432 433 @Override 434 public void seekTo(long pos) { 435 MediaPlayerInterface player; 436 synchronized (mLock) { 437 player = mPlayer; 438 } 439 if (player != null) { 440 player.seekTo(pos); 441 } else if (DEBUG) { 442 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 443 } 444 } 445 446 @Override 447 public void skipForward() { 448 // To match with KEYCODE_MEDIA_SKIP_FORWARD 449 } 450 451 @Override 452 public void skipBackward() { 453 // To match with KEYCODE_MEDIA_SKIP_BACKWARD 454 } 455 456 @Override 457 public void notifyError(@ErrorCode final int errorCode, @Nullable final Bundle extras) { 458 notifyToAllControllers(new NotifyRunnable() { 459 @Override 460 public void run(ControllerCb callback) throws RemoteException { 461 callback.onError(errorCode, extras); 462 } 463 }); 464 } 465 466 @Override 467 public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller, 468 @Nullable final List<Bundle> routes) { 469 notifyToController(controller, new NotifyRunnable() { 470 @Override 471 public void run(ControllerCb callback) throws RemoteException { 472 callback.onRoutesInfoChanged(routes); 473 } 474 }); 475 } 476 477 @Override 478 public @MediaPlayerInterface.PlayerState int getPlayerState() { 479 MediaPlayerInterface player; 480 synchronized (mLock) { 481 player = mPlayer; 482 } 483 if (player != null) { 484 return player.getPlayerState(); 485 } else if (DEBUG) { 486 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 487 } 488 return MediaPlayerInterface.PLAYER_STATE_ERROR; 489 } 490 491 @Override 492 public long getCurrentPosition() { 493 MediaPlayerInterface player; 494 synchronized (mLock) { 495 player = mPlayer; 496 } 497 if (player != null) { 498 return player.getCurrentPosition(); 499 } else if (DEBUG) { 500 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 501 } 502 return MediaPlayerInterface.UNKNOWN_TIME; 503 } 504 505 @Override 506 public long getDuration() { 507 MediaPlayerInterface player; 508 synchronized (mLock) { 509 player = mPlayer; 510 } 511 if (player != null) { 512 // Note: This should be the same as 513 // getCurrentMediaItem().getMetadata().getLong(METADATA_KEY_DURATION) 514 return player.getDuration(); 515 } else if (DEBUG) { 516 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 517 } 518 return MediaPlayerInterface.UNKNOWN_TIME; 519 } 520 521 @Override 522 public long getBufferedPosition() { 523 MediaPlayerInterface player; 524 synchronized (mLock) { 525 player = mPlayer; 526 } 527 if (player != null) { 528 return player.getBufferedPosition(); 529 } else if (DEBUG) { 530 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 531 } 532 return MediaPlayerInterface.UNKNOWN_TIME; 533 } 534 535 @Override 536 public @MediaPlayerInterface.BuffState int getBufferingState() { 537 MediaPlayerInterface player; 538 synchronized (mLock) { 539 player = mPlayer; 540 } 541 if (player != null) { 542 return player.getBufferingState(); 543 } else if (DEBUG) { 544 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 545 } 546 return BUFFERING_STATE_UNKNOWN; 547 } 548 549 @Override 550 public float getPlaybackSpeed() { 551 MediaPlayerInterface player; 552 synchronized (mLock) { 553 player = mPlayer; 554 } 555 if (player != null) { 556 return player.getPlaybackSpeed(); 557 } else if (DEBUG) { 558 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 559 } 560 return 1.0f; 561 } 562 563 @Override 564 public void setPlaybackSpeed(float speed) { 565 MediaPlayerInterface player; 566 synchronized (mLock) { 567 player = mPlayer; 568 } 569 if (player != null) { 570 player.setPlaybackSpeed(speed); 571 } else if (DEBUG) { 572 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 573 } 574 } 575 576 @Override 577 public void setOnDataSourceMissingHelper( 578 @NonNull OnDataSourceMissingHelper helper) { 579 if (helper == null) { 580 throw new IllegalArgumentException("helper shouldn't be null"); 581 } 582 synchronized (mLock) { 583 mDsmHelper = helper; 584 if (mSessionPlaylistAgent != null) { 585 mSessionPlaylistAgent.setOnDataSourceMissingHelper(helper); 586 } 587 } 588 } 589 590 @Override 591 public void clearOnDataSourceMissingHelper() { 592 synchronized (mLock) { 593 mDsmHelper = null; 594 if (mSessionPlaylistAgent != null) { 595 mSessionPlaylistAgent.clearOnDataSourceMissingHelper(); 596 } 597 } 598 } 599 600 @Override 601 public List<MediaItem2> getPlaylist() { 602 MediaPlaylistAgent agent; 603 synchronized (mLock) { 604 agent = mPlaylistAgent; 605 } 606 if (agent != null) { 607 return agent.getPlaylist(); 608 } else if (DEBUG) { 609 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 610 } 611 return null; 612 } 613 614 @Override 615 public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { 616 if (list == null) { 617 throw new IllegalArgumentException("list shouldn't be null"); 618 } 619 MediaPlaylistAgent agent; 620 synchronized (mLock) { 621 agent = mPlaylistAgent; 622 } 623 if (agent != null) { 624 agent.setPlaylist(list, metadata); 625 } else if (DEBUG) { 626 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 627 } 628 } 629 630 @Override 631 public void skipToPlaylistItem(@NonNull MediaItem2 item) { 632 if (item == null) { 633 throw new IllegalArgumentException("item shouldn't be null"); 634 } 635 MediaPlaylistAgent agent; 636 synchronized (mLock) { 637 agent = mPlaylistAgent; 638 } 639 if (agent != null) { 640 agent.skipToPlaylistItem(item); 641 } else if (DEBUG) { 642 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 643 } 644 } 645 646 @Override 647 public void skipToPreviousItem() { 648 MediaPlaylistAgent agent; 649 synchronized (mLock) { 650 agent = mPlaylistAgent; 651 } 652 if (agent != null) { 653 agent.skipToPreviousItem(); 654 } else if (DEBUG) { 655 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 656 } 657 } 658 659 @Override 660 public void skipToNextItem() { 661 MediaPlaylistAgent agent; 662 synchronized (mLock) { 663 agent = mPlaylistAgent; 664 } 665 if (agent != null) { 666 agent.skipToNextItem(); 667 } else if (DEBUG) { 668 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 669 } 670 } 671 672 @Override 673 public MediaMetadata2 getPlaylistMetadata() { 674 MediaPlaylistAgent agent; 675 synchronized (mLock) { 676 agent = mPlaylistAgent; 677 } 678 if (agent != null) { 679 return agent.getPlaylistMetadata(); 680 } else if (DEBUG) { 681 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 682 } 683 return null; 684 } 685 686 @Override 687 public void addPlaylistItem(int index, @NonNull MediaItem2 item) { 688 if (index < 0) { 689 throw new IllegalArgumentException("index shouldn't be negative"); 690 } 691 if (item == null) { 692 throw new IllegalArgumentException("item shouldn't be null"); 693 } 694 MediaPlaylistAgent agent; 695 synchronized (mLock) { 696 agent = mPlaylistAgent; 697 } 698 if (agent != null) { 699 agent.addPlaylistItem(index, item); 700 } else if (DEBUG) { 701 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 702 } 703 } 704 705 @Override 706 public void removePlaylistItem(@NonNull MediaItem2 item) { 707 if (item == null) { 708 throw new IllegalArgumentException("item shouldn't be null"); 709 } 710 MediaPlaylistAgent agent; 711 synchronized (mLock) { 712 agent = mPlaylistAgent; 713 } 714 if (agent != null) { 715 agent.removePlaylistItem(item); 716 } else if (DEBUG) { 717 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 718 } 719 } 720 721 @Override 722 public void replacePlaylistItem(int index, @NonNull MediaItem2 item) { 723 if (index < 0) { 724 throw new IllegalArgumentException("index shouldn't be negative"); 725 } 726 if (item == null) { 727 throw new IllegalArgumentException("item shouldn't be null"); 728 } 729 MediaPlaylistAgent agent; 730 synchronized (mLock) { 731 agent = mPlaylistAgent; 732 } 733 if (agent != null) { 734 agent.replacePlaylistItem(index, item); 735 } else if (DEBUG) { 736 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 737 } 738 } 739 740 @Override 741 public MediaItem2 getCurrentMediaItem() { 742 MediaPlaylistAgent agent; 743 synchronized (mLock) { 744 agent = mPlaylistAgent; 745 } 746 if (agent != null) { 747 return agent.getCurrentMediaItem(); 748 } else if (DEBUG) { 749 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 750 } 751 return null; 752 } 753 754 @Override 755 public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) { 756 MediaPlaylistAgent agent; 757 synchronized (mLock) { 758 agent = mPlaylistAgent; 759 } 760 if (agent != null) { 761 agent.updatePlaylistMetadata(metadata); 762 } else if (DEBUG) { 763 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 764 } 765 } 766 767 @Override 768 public @MediaPlaylistAgent.RepeatMode int getRepeatMode() { 769 MediaPlaylistAgent agent; 770 synchronized (mLock) { 771 agent = mPlaylistAgent; 772 } 773 if (agent != null) { 774 return agent.getRepeatMode(); 775 } else if (DEBUG) { 776 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 777 } 778 return MediaPlaylistAgent.REPEAT_MODE_NONE; 779 } 780 781 @Override 782 public void setRepeatMode(@MediaPlaylistAgent.RepeatMode int repeatMode) { 783 MediaPlaylistAgent agent; 784 synchronized (mLock) { 785 agent = mPlaylistAgent; 786 } 787 if (agent != null) { 788 agent.setRepeatMode(repeatMode); 789 } else if (DEBUG) { 790 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 791 } 792 } 793 794 @Override 795 public @MediaPlaylistAgent.ShuffleMode int getShuffleMode() { 796 MediaPlaylistAgent agent; 797 synchronized (mLock) { 798 agent = mPlaylistAgent; 799 } 800 if (agent != null) { 801 return agent.getShuffleMode(); 802 } else if (DEBUG) { 803 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 804 } 805 return MediaPlaylistAgent.SHUFFLE_MODE_NONE; 806 } 807 808 @Override 809 public void setShuffleMode(int shuffleMode) { 810 MediaPlaylistAgent agent; 811 synchronized (mLock) { 812 agent = mPlaylistAgent; 813 } 814 if (agent != null) { 815 agent.setShuffleMode(shuffleMode); 816 } else if (DEBUG) { 817 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 818 } 819 } 820 821 /////////////////////////////////////////////////// 822 // LibrarySession Methods 823 /////////////////////////////////////////////////// 824 825 @Override 826 void notifyChildrenChanged(ControllerInfo controller, final String parentId, 827 final int itemCount, final Bundle extras, 828 List<MediaSessionManager.RemoteUserInfo> subscribingBrowsers) { 829 if (controller == null) { 830 throw new IllegalArgumentException("controller shouldn't be null"); 831 } 832 if (TextUtils.isEmpty(parentId)) { 833 throw new IllegalArgumentException("query shouldn't be empty"); 834 } 835 836 // Notify controller only if it has subscribed the parentId. 837 for (MediaSessionManager.RemoteUserInfo info : subscribingBrowsers) { 838 if (info.getPackageName().equals(controller.getPackageName()) 839 && info.getUid() == controller.getUid()) { 840 notifyToController(controller, new NotifyRunnable() { 841 @Override 842 public void run(ControllerCb callback) throws RemoteException { 843 callback.onChildrenChanged(parentId, itemCount, extras); 844 } 845 }); 846 return; 847 } 848 } 849 } 850 851 @Override 852 void notifySearchResultChanged(ControllerInfo controller, final String query, 853 final int itemCount, final Bundle extras) { 854 if (controller == null) { 855 throw new IllegalArgumentException("controller shouldn't be null"); 856 } 857 if (TextUtils.isEmpty(query)) { 858 throw new IllegalArgumentException("query shouldn't be empty"); 859 } 860 notifyToController(controller, new NotifyRunnable() { 861 @Override 862 public void run(ControllerCb callback) throws RemoteException { 863 callback.onSearchResultChanged(query, itemCount, extras); 864 } 865 }); 866 } 867 868 /////////////////////////////////////////////////// 869 // package private and private methods 870 /////////////////////////////////////////////////// 871 @Override 872 MediaSession2 createInstance() { 873 return new MediaSession2(this); 874 } 875 876 @Override 877 @NonNull MediaSession2 getInstance() { 878 return mInstance; 879 } 880 881 @Override 882 Context getContext() { 883 return mContext; 884 } 885 886 @Override 887 Executor getCallbackExecutor() { 888 return mCallbackExecutor; 889 } 890 891 @Override 892 SessionCallback getCallback() { 893 return mCallback; 894 } 895 896 @Override 897 MediaSessionCompat getSessionCompat() { 898 return mSessionCompat; 899 } 900 901 @Override 902 boolean isClosed() { 903 return !mHandlerThread.isAlive(); 904 } 905 906 @Override 907 PlaybackStateCompat getPlaybackStateCompat() { 908 synchronized (mLock) { 909 int state = MediaUtils2.createPlaybackStateCompatState(getPlayerState(), 910 getBufferingState()); 911 long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE 912 | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND 913 | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS 914 | PlaybackStateCompat.ACTION_SKIP_TO_NEXT 915 | PlaybackStateCompat.ACTION_FAST_FORWARD 916 | PlaybackStateCompat.ACTION_SET_RATING 917 | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_PLAY_PAUSE 918 | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID 919 | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH 920 | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM 921 | PlaybackStateCompat.ACTION_PLAY_FROM_URI | PlaybackStateCompat.ACTION_PREPARE 922 | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID 923 | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH 924 | PlaybackStateCompat.ACTION_PREPARE_FROM_URI 925 | PlaybackStateCompat.ACTION_SET_REPEAT_MODE 926 | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE 927 | PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED; 928 return new PlaybackStateCompat.Builder() 929 .setState(state, getCurrentPosition(), getPlaybackSpeed()) 930 .setActions(allActions) 931 .setBufferedPosition(getBufferedPosition()) 932 .build(); 933 } 934 } 935 936 @Override 937 PlaybackInfo getPlaybackInfo() { 938 synchronized (mLock) { 939 return mPlaybackInfo; 940 } 941 } 942 private static String getServiceName(Context context, String serviceAction, String id) { 943 PackageManager manager = context.getPackageManager(); 944 Intent serviceIntent = new Intent(serviceAction); 945 serviceIntent.setPackage(context.getPackageName()); 946 List<ResolveInfo> services = manager.queryIntentServices(serviceIntent, 947 PackageManager.GET_META_DATA); 948 String serviceName = null; 949 if (services != null) { 950 for (int i = 0; i < services.size(); i++) { 951 String serviceId = SessionToken2.getSessionId(services.get(i)); 952 if (serviceId != null && TextUtils.equals(id, serviceId)) { 953 if (services.get(i).serviceInfo == null) { 954 continue; 955 } 956 if (serviceName != null) { 957 throw new IllegalArgumentException("Ambiguous session type. Multiple" 958 + " session services define the same id=" + id); 959 } 960 serviceName = services.get(i).serviceInfo.name; 961 } 962 } 963 } 964 return serviceName; 965 } 966 967 private void notifyAgentUpdatedNotLocked(MediaPlaylistAgent oldAgent) { 968 // Tells the playlist change first, to current item can change be notified with an item 969 // within the playlist. 970 List<MediaItem2> oldPlaylist = oldAgent.getPlaylist(); 971 final List<MediaItem2> newPlaylist = getPlaylist(); 972 if (!ObjectsCompat.equals(oldPlaylist, newPlaylist)) { 973 notifyToAllControllers(new NotifyRunnable() { 974 @Override 975 public void run(ControllerCb callback) throws RemoteException { 976 callback.onPlaylistChanged( 977 newPlaylist, getPlaylistMetadata()); 978 } 979 }); 980 } else { 981 MediaMetadata2 oldMetadata = oldAgent.getPlaylistMetadata(); 982 final MediaMetadata2 newMetadata = getPlaylistMetadata(); 983 if (!ObjectsCompat.equals(oldMetadata, newMetadata)) { 984 notifyToAllControllers(new NotifyRunnable() { 985 @Override 986 public void run(ControllerCb callback) throws RemoteException { 987 callback.onPlaylistMetadataChanged(newMetadata); 988 } 989 }); 990 } 991 } 992 MediaItem2 oldCurrentItem = oldAgent.getCurrentMediaItem(); 993 final MediaItem2 newCurrentItem = getCurrentMediaItem(); 994 if (!ObjectsCompat.equals(oldCurrentItem, newCurrentItem)) { 995 notifyToAllControllers(new NotifyRunnable() { 996 @Override 997 public void run(ControllerCb callback) throws RemoteException { 998 callback.onCurrentMediaItemChanged(newCurrentItem); 999 } 1000 }); 1001 } 1002 final int repeatMode = getRepeatMode(); 1003 if (oldAgent.getRepeatMode() != repeatMode) { 1004 notifyToAllControllers(new NotifyRunnable() { 1005 @Override 1006 public void run(ControllerCb callback) throws RemoteException { 1007 callback.onRepeatModeChanged(repeatMode); 1008 } 1009 }); 1010 } 1011 final int shuffleMode = getShuffleMode(); 1012 if (oldAgent.getShuffleMode() != shuffleMode) { 1013 notifyToAllControllers(new NotifyRunnable() { 1014 @Override 1015 public void run(ControllerCb callback) throws RemoteException { 1016 callback.onShuffleModeChanged(shuffleMode); 1017 } 1018 }); 1019 } 1020 } 1021 1022 private void notifyPlayerUpdatedNotLocked(MediaPlayerInterface oldPlayer) { 1023 // Always forcefully send the player state and buffered state to send the current position 1024 // and buffered position. 1025 final int playerState = getPlayerState(); 1026 notifyToAllControllers(new NotifyRunnable() { 1027 @Override 1028 public void run(ControllerCb callback) throws RemoteException { 1029 callback.onPlayerStateChanged(playerState); 1030 } 1031 }); 1032 final MediaItem2 item = getCurrentMediaItem(); 1033 if (item != null) { 1034 final int bufferingState = getBufferingState(); 1035 notifyToAllControllers(new NotifyRunnable() { 1036 @Override 1037 public void run(ControllerCb callback) throws RemoteException { 1038 callback.onBufferingStateChanged(item, bufferingState); 1039 } 1040 }); 1041 } 1042 final float speed = getPlaybackSpeed(); 1043 if (speed != oldPlayer.getPlaybackSpeed()) { 1044 notifyToAllControllers(new NotifyRunnable() { 1045 @Override 1046 public void run(ControllerCb callback) throws RemoteException { 1047 callback.onPlaybackSpeedChanged(speed); 1048 } 1049 }); 1050 } 1051 // Note: AudioInfo is updated outside of this API. 1052 } 1053 1054 private void notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent, 1055 final List<MediaItem2> list, final MediaMetadata2 metadata) { 1056 synchronized (mLock) { 1057 if (playlistAgent != mPlaylistAgent) { 1058 // Ignore calls from the old agent. 1059 return; 1060 } 1061 } 1062 mCallback.onPlaylistChanged(mInstance, playlistAgent, list, metadata); 1063 notifyToAllControllers(new NotifyRunnable() { 1064 @Override 1065 public void run(ControllerCb callback) throws RemoteException { 1066 callback.onPlaylistChanged(list, metadata); 1067 } 1068 }); 1069 } 1070 1071 private void notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent, 1072 final MediaMetadata2 metadata) { 1073 synchronized (mLock) { 1074 if (playlistAgent != mPlaylistAgent) { 1075 // Ignore calls from the old agent. 1076 return; 1077 } 1078 } 1079 mCallback.onPlaylistMetadataChanged(mInstance, playlistAgent, metadata); 1080 notifyToAllControllers(new NotifyRunnable() { 1081 @Override 1082 public void run(ControllerCb callback) throws RemoteException { 1083 callback.onPlaylistMetadataChanged(metadata); 1084 } 1085 }); 1086 } 1087 1088 private void notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, 1089 final int repeatMode) { 1090 synchronized (mLock) { 1091 if (playlistAgent != mPlaylistAgent) { 1092 // Ignore calls from the old agent. 1093 return; 1094 } 1095 } 1096 mCallback.onRepeatModeChanged(mInstance, playlistAgent, repeatMode); 1097 notifyToAllControllers(new NotifyRunnable() { 1098 @Override 1099 public void run(ControllerCb callback) throws RemoteException { 1100 callback.onRepeatModeChanged(repeatMode); 1101 } 1102 }); 1103 } 1104 1105 private void notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, 1106 final int shuffleMode) { 1107 synchronized (mLock) { 1108 if (playlistAgent != mPlaylistAgent) { 1109 // Ignore calls from the old agent. 1110 return; 1111 } 1112 } 1113 mCallback.onShuffleModeChanged(mInstance, playlistAgent, shuffleMode); 1114 notifyToAllControllers(new NotifyRunnable() { 1115 @Override 1116 public void run(ControllerCb callback) throws RemoteException { 1117 callback.onShuffleModeChanged(shuffleMode); 1118 } 1119 }); 1120 } 1121 1122 private void notifyToController(@NonNull final ControllerInfo controller, 1123 @NonNull NotifyRunnable runnable) { 1124 if (controller == null) { 1125 return; 1126 } 1127 try { 1128 runnable.run(controller.getControllerCb()); 1129 } catch (DeadObjectException e) { 1130 if (DEBUG) { 1131 Log.d(TAG, controller.toString() + " is gone", e); 1132 } 1133 mSession2Stub.removeControllerInfo(controller); 1134 mCallbackExecutor.execute(new Runnable() { 1135 @Override 1136 public void run() { 1137 mCallback.onDisconnected(MediaSession2ImplBase.this.getInstance(), controller); 1138 } 1139 }); 1140 } catch (RemoteException e) { 1141 // Currently it's TransactionTooLargeException or DeadSystemException. 1142 // We'd better to leave log for those cases because 1143 // - TransactionTooLargeException means that we may need to fix our code. 1144 // (e.g. add pagination or special way to deliver Bitmap) 1145 // - DeadSystemException means that errors around it can be ignored. 1146 Log.w(TAG, "Exception in " + controller.toString(), e); 1147 } 1148 } 1149 1150 private void notifyToAllControllers(@NonNull NotifyRunnable runnable) { 1151 List<ControllerInfo> controllers = getConnectedControllers(); 1152 for (int i = 0; i < controllers.size(); i++) { 1153 notifyToController(controllers.get(i), runnable); 1154 } 1155 } 1156 1157 /////////////////////////////////////////////////// 1158 // Inner classes 1159 /////////////////////////////////////////////////// 1160 @FunctionalInterface 1161 private interface NotifyRunnable { 1162 void run(ControllerCb callback) throws RemoteException; 1163 } 1164 1165 private static class MyPlayerEventCallback extends PlayerEventCallback { 1166 private final WeakReference<MediaSession2ImplBase> mSession; 1167 1168 private MyPlayerEventCallback(MediaSession2ImplBase session) { 1169 mSession = new WeakReference<>(session); 1170 } 1171 1172 @Override 1173 public void onCurrentDataSourceChanged(final MediaPlayerInterface player, 1174 final DataSourceDesc dsd) { 1175 final MediaSession2ImplBase session = getSession(); 1176 if (session == null) { 1177 return; 1178 } 1179 session.getCallbackExecutor().execute(new Runnable() { 1180 @Override 1181 public void run() { 1182 final MediaItem2 item; 1183 if (dsd == null) { 1184 // This is OK because onCurrentDataSourceChanged() can be called with the 1185 // null dsd, so onCurrentMediaItemChanged() can be as well. 1186 item = null; 1187 } else { 1188 item = MyPlayerEventCallback.this.getMediaItem(session, dsd); 1189 if (item == null) { 1190 Log.w(TAG, "Cannot obtain media item from the dsd=" + dsd); 1191 return; 1192 } 1193 } 1194 session.getCallback().onCurrentMediaItemChanged(session.getInstance(), player, 1195 item); 1196 session.notifyToAllControllers(new NotifyRunnable() { 1197 @Override 1198 public void run(ControllerCb callback) throws RemoteException { 1199 callback.onCurrentMediaItemChanged(item); 1200 } 1201 }); 1202 } 1203 }); 1204 } 1205 1206 @Override 1207 public void onMediaPrepared(final MediaPlayerInterface mpb, final DataSourceDesc dsd) { 1208 final MediaSession2ImplBase session = getSession(); 1209 if (session == null || dsd == null) { 1210 return; 1211 } 1212 session.getCallbackExecutor().execute(new Runnable() { 1213 @Override 1214 public void run() { 1215 MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd); 1216 if (item == null) { 1217 return; 1218 } 1219 if (item.equals(session.getCurrentMediaItem())) { 1220 long duration = session.getDuration(); 1221 if (duration < 0) { 1222 return; 1223 } 1224 MediaMetadata2 metadata = item.getMetadata(); 1225 if (metadata != null) { 1226 if (!metadata.containsKey(MediaMetadata2.METADATA_KEY_DURATION)) { 1227 metadata = new MediaMetadata2.Builder(metadata).putLong( 1228 MediaMetadata2.METADATA_KEY_DURATION, duration).build(); 1229 } else { 1230 long durationFromMetadata = 1231 metadata.getLong(MediaMetadata2.METADATA_KEY_DURATION); 1232 if (duration != durationFromMetadata) { 1233 // Warns developers about the mismatch. Don't log media item 1234 // here to keep metadata secure. 1235 Log.w(TAG, "duration mismatch for an item." 1236 + " duration from player=" + duration 1237 + " duration from metadata=" + durationFromMetadata 1238 + ". May be a timing issue?"); 1239 } 1240 // Trust duration in the metadata set by developer. 1241 // In theory, duration may differ if the current item has been 1242 // changed before the getDuration(). So it's better not touch 1243 // duration set by developer. 1244 metadata = null; 1245 } 1246 } else { 1247 metadata = new MediaMetadata2.Builder() 1248 .putLong(MediaMetadata2.METADATA_KEY_DURATION, duration) 1249 .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, 1250 item.getMediaId()) 1251 .build(); 1252 } 1253 if (metadata != null) { 1254 item.setMetadata(metadata); 1255 session.notifyToAllControllers(new NotifyRunnable() { 1256 @Override 1257 public void run(ControllerCb callback) throws RemoteException { 1258 callback.onPlaylistChanged( 1259 session.getPlaylist(), session.getPlaylistMetadata()); 1260 } 1261 }); 1262 } 1263 } 1264 session.getCallback().onMediaPrepared(session.getInstance(), mpb, item); 1265 } 1266 }); 1267 } 1268 1269 @Override 1270 public void onPlayerStateChanged(final MediaPlayerInterface player, final int state) { 1271 final MediaSession2ImplBase session = getSession(); 1272 if (session == null) { 1273 return; 1274 } 1275 session.getCallbackExecutor().execute(new Runnable() { 1276 @Override 1277 public void run() { 1278 session.getCallback().onPlayerStateChanged( 1279 session.getInstance(), player, state); 1280 session.notifyToAllControllers(new NotifyRunnable() { 1281 @Override 1282 public void run(ControllerCb callback) throws RemoteException { 1283 callback.onPlayerStateChanged(state); 1284 } 1285 }); 1286 } 1287 }); 1288 } 1289 1290 @Override 1291 public void onBufferingStateChanged(final MediaPlayerInterface mpb, 1292 final DataSourceDesc dsd, final int state) { 1293 final MediaSession2ImplBase session = getSession(); 1294 if (session == null || dsd == null) { 1295 return; 1296 } 1297 session.getCallbackExecutor().execute(new Runnable() { 1298 @Override 1299 public void run() { 1300 final MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd); 1301 if (item == null) { 1302 return; 1303 } 1304 session.getCallback().onBufferingStateChanged( 1305 session.getInstance(), mpb, item, state); 1306 session.notifyToAllControllers(new NotifyRunnable() { 1307 @Override 1308 public void run(ControllerCb callback) throws RemoteException { 1309 callback.onBufferingStateChanged(item, state); 1310 } 1311 }); 1312 } 1313 }); 1314 } 1315 1316 @Override 1317 public void onPlaybackSpeedChanged(final MediaPlayerInterface mpb, final float speed) { 1318 final MediaSession2ImplBase session = getSession(); 1319 if (session == null) { 1320 return; 1321 } 1322 session.getCallbackExecutor().execute(new Runnable() { 1323 @Override 1324 public void run() { 1325 session.getCallback().onPlaybackSpeedChanged(session.getInstance(), mpb, speed); 1326 session.notifyToAllControllers(new NotifyRunnable() { 1327 @Override 1328 public void run(ControllerCb callback) throws RemoteException { 1329 callback.onPlaybackSpeedChanged(speed); 1330 } 1331 }); 1332 } 1333 }); 1334 } 1335 1336 @Override 1337 public void onSeekCompleted(final MediaPlayerInterface mpb, final long position) { 1338 final MediaSession2ImplBase session = getSession(); 1339 if (session == null) { 1340 return; 1341 } 1342 session.getCallbackExecutor().execute(new Runnable() { 1343 @Override 1344 public void run() { 1345 session.getCallback().onSeekCompleted(session.getInstance(), mpb, position); 1346 session.notifyToAllControllers(new NotifyRunnable() { 1347 @Override 1348 public void run(ControllerCb callback) throws RemoteException { 1349 callback.onSeekCompleted(position); 1350 } 1351 }); 1352 } 1353 }); 1354 } 1355 1356 private MediaSession2ImplBase getSession() { 1357 final MediaSession2ImplBase session = mSession.get(); 1358 if (session == null && DEBUG) { 1359 Log.d(TAG, "Session is closed", new IllegalStateException()); 1360 } 1361 return session; 1362 } 1363 1364 private MediaItem2 getMediaItem(MediaSession2ImplBase session, DataSourceDesc dsd) { 1365 MediaPlaylistAgent agent = session.getPlaylistAgent(); 1366 if (agent == null) { 1367 if (DEBUG) { 1368 Log.d(TAG, "Session is closed", new IllegalStateException()); 1369 } 1370 return null; 1371 } 1372 MediaItem2 item = agent.getMediaItem(dsd); 1373 if (item == null) { 1374 if (DEBUG) { 1375 Log.d(TAG, "Could not find matching item for dsd=" + dsd, 1376 new NoSuchElementException()); 1377 } 1378 } 1379 return item; 1380 } 1381 } 1382 1383 private static class MyPlaylistEventCallback extends PlaylistEventCallback { 1384 private final WeakReference<MediaSession2ImplBase> mSession; 1385 1386 private MyPlaylistEventCallback(MediaSession2ImplBase session) { 1387 mSession = new WeakReference<>(session); 1388 } 1389 1390 @Override 1391 public void onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list, 1392 MediaMetadata2 metadata) { 1393 final MediaSession2ImplBase session = mSession.get(); 1394 if (session == null) { 1395 return; 1396 } 1397 session.notifyPlaylistChangedOnExecutor(playlistAgent, list, metadata); 1398 } 1399 1400 @Override 1401 public void onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent, 1402 MediaMetadata2 metadata) { 1403 final MediaSession2ImplBase session = mSession.get(); 1404 if (session == null) { 1405 return; 1406 } 1407 session.notifyPlaylistMetadataChangedOnExecutor(playlistAgent, metadata); 1408 } 1409 1410 @Override 1411 public void onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode) { 1412 final MediaSession2ImplBase session = mSession.get(); 1413 if (session == null) { 1414 return; 1415 } 1416 session.notifyRepeatModeChangedOnExecutor(playlistAgent, repeatMode); 1417 } 1418 1419 @Override 1420 public void onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode) { 1421 final MediaSession2ImplBase session = mSession.get(); 1422 if (session == null) { 1423 return; 1424 } 1425 session.notifyShuffleModeChangedOnExecutor(playlistAgent, shuffleMode); 1426 } 1427 } 1428 1429 abstract static class BuilderBase 1430 <T extends MediaSession2, C extends SessionCallback> { 1431 final Context mContext; 1432 MediaPlayerInterface mPlayer; 1433 String mId; 1434 Executor mCallbackExecutor; 1435 C mCallback; 1436 MediaPlaylistAgent mPlaylistAgent; 1437 VolumeProviderCompat mVolumeProvider; 1438 PendingIntent mSessionActivity; 1439 1440 BuilderBase(Context context) { 1441 if (context == null) { 1442 throw new IllegalArgumentException("context shouldn't be null"); 1443 } 1444 mContext = context; 1445 // Ensure MediaSessionCompat non-null or empty 1446 mId = TAG; 1447 } 1448 1449 void setPlayer(@NonNull MediaPlayerInterface player) { 1450 if (player == null) { 1451 throw new IllegalArgumentException("player shouldn't be null"); 1452 } 1453 mPlayer = player; 1454 } 1455 1456 void setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) { 1457 if (playlistAgent == null) { 1458 throw new IllegalArgumentException("playlistAgent shouldn't be null"); 1459 } 1460 mPlaylistAgent = playlistAgent; 1461 } 1462 1463 void setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) { 1464 mVolumeProvider = volumeProvider; 1465 } 1466 1467 void setSessionActivity(@Nullable PendingIntent pi) { 1468 mSessionActivity = pi; 1469 } 1470 1471 void setId(@NonNull String id) { 1472 if (id == null) { 1473 throw new IllegalArgumentException("id shouldn't be null"); 1474 } 1475 mId = id; 1476 } 1477 1478 void setSessionCallback(@NonNull Executor executor, @NonNull C callback) { 1479 if (executor == null) { 1480 throw new IllegalArgumentException("executor shouldn't be null"); 1481 } 1482 if (callback == null) { 1483 throw new IllegalArgumentException("callback shouldn't be null"); 1484 } 1485 mCallbackExecutor = executor; 1486 mCallback = callback; 1487 } 1488 1489 abstract @NonNull T build(); 1490 } 1491 1492 static final class Builder extends 1493 BuilderBase<MediaSession2, MediaSession2.SessionCallback> { 1494 Builder(Context context) { 1495 super(context); 1496 } 1497 1498 @Override 1499 public @NonNull MediaSession2 build() { 1500 if (mCallbackExecutor == null) { 1501 mCallbackExecutor = new MainHandlerExecutor(mContext); 1502 } 1503 if (mCallback == null) { 1504 mCallback = new SessionCallback() {}; 1505 } 1506 return new MediaSession2(new MediaSession2ImplBase(mContext, 1507 new MediaSessionCompat(mContext, mId), mId, mPlayer, mPlaylistAgent, 1508 mVolumeProvider, mSessionActivity, mCallbackExecutor, mCallback)); 1509 } 1510 } 1511 1512 static class MainHandlerExecutor implements Executor { 1513 private final Handler mHandler; 1514 1515 MainHandlerExecutor(Context context) { 1516 mHandler = new Handler(context.getMainLooper()); 1517 } 1518 1519 @Override 1520 public void execute(Runnable command) { 1521 if (!mHandler.post(command)) { 1522 throw new RejectedExecutionException(mHandler + " is shutting down"); 1523 } 1524 } 1525 } 1526} 1527