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 com.android.media; 18 19import static android.media.SessionCommand2.COMMAND_CODE_CUSTOM; 20import static android.media.SessionToken2.TYPE_LIBRARY_SERVICE; 21import static android.media.SessionToken2.TYPE_SESSION; 22import static android.media.SessionToken2.TYPE_SESSION_SERVICE; 23 24import android.annotation.NonNull; 25import android.annotation.Nullable; 26import android.app.PendingIntent; 27import android.content.Context; 28import android.content.Intent; 29import android.content.pm.PackageManager; 30import android.content.pm.ResolveInfo; 31import android.media.AudioAttributes; 32import android.media.AudioFocusRequest; 33import android.media.AudioManager; 34import android.media.DataSourceDesc; 35import android.media.MediaController2; 36import android.media.MediaController2.PlaybackInfo; 37import android.media.MediaItem2; 38import android.media.MediaLibraryService2; 39import android.media.MediaMetadata2; 40import android.media.MediaPlayerBase; 41import android.media.MediaPlayerBase.PlayerEventCallback; 42import android.media.MediaPlayerBase.PlayerState; 43import android.media.MediaPlaylistAgent; 44import android.media.MediaPlaylistAgent.PlaylistEventCallback; 45import android.media.MediaSession2; 46import android.media.MediaSession2.Builder; 47import android.media.SessionCommand2; 48import android.media.MediaSession2.CommandButton; 49import android.media.SessionCommandGroup2; 50import android.media.MediaSession2.ControllerInfo; 51import android.media.MediaSession2.OnDataSourceMissingHelper; 52import android.media.MediaSession2.SessionCallback; 53import android.media.MediaSessionService2; 54import android.media.SessionToken2; 55import android.media.VolumeProvider2; 56import android.media.session.MediaSessionManager; 57import android.media.update.MediaSession2Provider; 58import android.os.Bundle; 59import android.os.IBinder; 60import android.os.Parcelable; 61import android.os.Process; 62import android.os.ResultReceiver; 63import android.support.annotation.GuardedBy; 64import android.text.TextUtils; 65import android.util.Log; 66 67import java.lang.ref.WeakReference; 68import java.lang.reflect.Field; 69import java.util.ArrayList; 70import java.util.Collections; 71import java.util.HashSet; 72import java.util.List; 73import java.util.NoSuchElementException; 74import java.util.Set; 75import java.util.concurrent.Executor; 76 77public class MediaSession2Impl implements MediaSession2Provider { 78 private static final String TAG = "MediaSession2"; 79 private static final boolean DEBUG = true;//Log.isLoggable(TAG, Log.DEBUG); 80 81 private final Object mLock = new Object(); 82 83 private final MediaSession2 mInstance; 84 private final Context mContext; 85 private final String mId; 86 private final Executor mCallbackExecutor; 87 private final SessionCallback mCallback; 88 private final MediaSession2Stub mSessionStub; 89 private final SessionToken2 mSessionToken; 90 private final AudioManager mAudioManager; 91 private final PendingIntent mSessionActivity; 92 private final PlayerEventCallback mPlayerEventCallback; 93 private final PlaylistEventCallback mPlaylistEventCallback; 94 95 // mPlayer is set to null when the session is closed, and we shouldn't throw an exception 96 // nor leave log always for using mPlayer when it's null. Here's the reason. 97 // When a MediaSession2 is closed, there could be a pended operation in the session callback 98 // executor that may want to access the player. Here's the sample code snippet for that. 99 // 100 // public void onFoo() { 101 // if (mPlayer == null) return; // first check 102 // mSessionCallbackExecutor.executor(() -> { 103 // // Error. Session may be closed and mPlayer can be null here. 104 // mPlayer.foo(); 105 // }); 106 // } 107 // 108 // By adding protective code, we can also protect APIs from being called after the close() 109 // 110 // TODO(jaewan): Should we put volatile here? 111 @GuardedBy("mLock") 112 private MediaPlayerBase mPlayer; 113 @GuardedBy("mLock") 114 private MediaPlaylistAgent mPlaylistAgent; 115 @GuardedBy("mLock") 116 private SessionPlaylistAgent mSessionPlaylistAgent; 117 @GuardedBy("mLock") 118 private VolumeProvider2 mVolumeProvider; 119 @GuardedBy("mLock") 120 private PlaybackInfo mPlaybackInfo; 121 @GuardedBy("mLock") 122 private OnDataSourceMissingHelper mDsmHelper; 123 124 /** 125 * Can be only called by the {@link Builder#build()}. 126 * @param context 127 * @param player 128 * @param id 129 * @param playlistAgent 130 * @param volumeProvider 131 * @param sessionActivity 132 * @param callbackExecutor 133 * @param callback 134 */ 135 public MediaSession2Impl(Context context, MediaPlayerBase player, String id, 136 MediaPlaylistAgent playlistAgent, VolumeProvider2 volumeProvider, 137 PendingIntent sessionActivity, 138 Executor callbackExecutor, SessionCallback callback) { 139 // TODO(jaewan): Keep other params. 140 mInstance = createInstance(); 141 142 // Argument checks are done by builder already. 143 // Initialize finals first. 144 mContext = context; 145 mId = id; 146 mCallback = callback; 147 mCallbackExecutor = callbackExecutor; 148 mSessionActivity = sessionActivity; 149 mSessionStub = new MediaSession2Stub(this); 150 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 151 mPlayerEventCallback = new MyPlayerEventCallback(this); 152 mPlaylistEventCallback = new MyPlaylistEventCallback(this); 153 154 // Infer type from the id and package name. 155 String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id); 156 String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id); 157 if (sessionService != null && libraryService != null) { 158 throw new IllegalArgumentException("Ambiguous session type. Multiple" 159 + " session services define the same id=" + id); 160 } else if (libraryService != null) { 161 mSessionToken = new SessionToken2Impl(Process.myUid(), TYPE_LIBRARY_SERVICE, 162 mContext.getPackageName(), libraryService, id, mSessionStub).getInstance(); 163 } else if (sessionService != null) { 164 mSessionToken = new SessionToken2Impl(Process.myUid(), TYPE_SESSION_SERVICE, 165 mContext.getPackageName(), sessionService, id, mSessionStub).getInstance(); 166 } else { 167 mSessionToken = new SessionToken2Impl(Process.myUid(), TYPE_SESSION, 168 mContext.getPackageName(), null, id, mSessionStub).getInstance(); 169 } 170 171 updatePlayer(player, playlistAgent, volumeProvider); 172 173 // Ask server for the sanity check, and starts 174 // Sanity check for making session ID unique 'per package' cannot be done in here. 175 // Server can only know if the package has another process and has another session with the 176 // same id. Note that 'ID is unique per package' is important for controller to distinguish 177 // a session in another package. 178 MediaSessionManager manager = 179 (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE); 180 if (!manager.createSession2(mSessionToken)) { 181 throw new IllegalStateException("Session with the same id is already used by" 182 + " another process. Use MediaController2 instead."); 183 } 184 } 185 186 MediaSession2 createInstance() { 187 return new MediaSession2(this); 188 } 189 190 private static String getServiceName(Context context, String serviceAction, String id) { 191 PackageManager manager = context.getPackageManager(); 192 Intent serviceIntent = new Intent(serviceAction); 193 serviceIntent.setPackage(context.getPackageName()); 194 List<ResolveInfo> services = manager.queryIntentServices(serviceIntent, 195 PackageManager.GET_META_DATA); 196 String serviceName = null; 197 if (services != null) { 198 for (int i = 0; i < services.size(); i++) { 199 String serviceId = SessionToken2Impl.getSessionId(services.get(i)); 200 if (serviceId != null && TextUtils.equals(id, serviceId)) { 201 if (services.get(i).serviceInfo == null) { 202 continue; 203 } 204 if (serviceName != null) { 205 throw new IllegalArgumentException("Ambiguous session type. Multiple" 206 + " session services define the same id=" + id); 207 } 208 serviceName = services.get(i).serviceInfo.name; 209 } 210 } 211 } 212 return serviceName; 213 } 214 215 @Override 216 public void updatePlayer_impl(@NonNull MediaPlayerBase player, MediaPlaylistAgent playlistAgent, 217 VolumeProvider2 volumeProvider) throws IllegalArgumentException { 218 ensureCallingThread(); 219 if (player == null) { 220 throw new IllegalArgumentException("player shouldn't be null"); 221 } 222 updatePlayer(player, playlistAgent, volumeProvider); 223 } 224 225 private void updatePlayer(MediaPlayerBase player, MediaPlaylistAgent agent, 226 VolumeProvider2 volumeProvider) { 227 final MediaPlayerBase oldPlayer; 228 final MediaPlaylistAgent oldAgent; 229 final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes()); 230 synchronized (mLock) { 231 oldPlayer = mPlayer; 232 oldAgent = mPlaylistAgent; 233 mPlayer = player; 234 if (agent == null) { 235 mSessionPlaylistAgent = new SessionPlaylistAgent(this, mPlayer); 236 if (mDsmHelper != null) { 237 mSessionPlaylistAgent.setOnDataSourceMissingHelper(mDsmHelper); 238 } 239 agent = mSessionPlaylistAgent; 240 } 241 mPlaylistAgent = agent; 242 mVolumeProvider = volumeProvider; 243 mPlaybackInfo = info; 244 } 245 if (player != oldPlayer) { 246 player.registerPlayerEventCallback(mCallbackExecutor, mPlayerEventCallback); 247 if (oldPlayer != null) { 248 // Warning: Poorly implement player may ignore this 249 oldPlayer.unregisterPlayerEventCallback(mPlayerEventCallback); 250 } 251 } 252 if (agent != oldAgent) { 253 agent.registerPlaylistEventCallback(mCallbackExecutor, mPlaylistEventCallback); 254 if (oldAgent != null) { 255 // Warning: Poorly implement player may ignore this 256 oldAgent.unregisterPlaylistEventCallback(mPlaylistEventCallback); 257 } 258 } 259 260 if (oldPlayer != null) { 261 mSessionStub.notifyPlaybackInfoChanged(info); 262 notifyPlayerUpdatedNotLocked(oldPlayer); 263 } 264 // TODO(jaewan): Repeat the same thing for the playlist agent. 265 } 266 267 private PlaybackInfo createPlaybackInfo(VolumeProvider2 volumeProvider, AudioAttributes attrs) { 268 PlaybackInfo info; 269 if (volumeProvider == null) { 270 int stream; 271 if (attrs == null) { 272 stream = AudioManager.STREAM_MUSIC; 273 } else { 274 stream = attrs.getVolumeControlStream(); 275 if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) { 276 // It may happen if the AudioAttributes doesn't have usage. 277 // Change it to the STREAM_MUSIC because it's not supported by audio manager 278 // for querying volume level. 279 stream = AudioManager.STREAM_MUSIC; 280 } 281 } 282 info = MediaController2Impl.PlaybackInfoImpl.createPlaybackInfo( 283 PlaybackInfo.PLAYBACK_TYPE_LOCAL, 284 attrs, 285 mAudioManager.isVolumeFixed() 286 ? VolumeProvider2.VOLUME_CONTROL_FIXED 287 : VolumeProvider2.VOLUME_CONTROL_ABSOLUTE, 288 mAudioManager.getStreamMaxVolume(stream), 289 mAudioManager.getStreamVolume(stream)); 290 } else { 291 info = MediaController2Impl.PlaybackInfoImpl.createPlaybackInfo( 292 PlaybackInfo.PLAYBACK_TYPE_REMOTE /* ControlType */, 293 attrs, 294 volumeProvider.getControlType(), 295 volumeProvider.getMaxVolume(), 296 volumeProvider.getCurrentVolume()); 297 } 298 return info; 299 } 300 301 @Override 302 public void close_impl() { 303 // Stop system service from listening this session first. 304 MediaSessionManager manager = 305 (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE); 306 manager.destroySession2(mSessionToken); 307 308 if (mSessionStub != null) { 309 if (DEBUG) { 310 Log.d(TAG, "session is now unavailable, id=" + mId); 311 } 312 // Invalidate previously published session stub. 313 mSessionStub.destroyNotLocked(); 314 } 315 final MediaPlayerBase player; 316 final MediaPlaylistAgent agent; 317 synchronized (mLock) { 318 player = mPlayer; 319 mPlayer = null; 320 agent = mPlaylistAgent; 321 mPlaylistAgent = null; 322 mSessionPlaylistAgent = null; 323 } 324 if (player != null) { 325 player.unregisterPlayerEventCallback(mPlayerEventCallback); 326 } 327 if (agent != null) { 328 agent.unregisterPlaylistEventCallback(mPlaylistEventCallback); 329 } 330 } 331 332 @Override 333 public MediaPlayerBase getPlayer_impl() { 334 return getPlayer(); 335 } 336 337 @Override 338 public MediaPlaylistAgent getPlaylistAgent_impl() { 339 return mPlaylistAgent; 340 } 341 342 @Override 343 public VolumeProvider2 getVolumeProvider_impl() { 344 return mVolumeProvider; 345 } 346 347 @Override 348 public SessionToken2 getToken_impl() { 349 return mSessionToken; 350 } 351 352 @Override 353 public List<ControllerInfo> getConnectedControllers_impl() { 354 return mSessionStub.getControllers(); 355 } 356 357 @Override 358 public void setAudioFocusRequest_impl(AudioFocusRequest afr) { 359 // implement 360 } 361 362 @Override 363 public void play_impl() { 364 ensureCallingThread(); 365 final MediaPlayerBase player = mPlayer; 366 if (player != null) { 367 player.play(); 368 } else if (DEBUG) { 369 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 370 } 371 } 372 373 @Override 374 public void pause_impl() { 375 ensureCallingThread(); 376 final MediaPlayerBase player = mPlayer; 377 if (player != null) { 378 player.pause(); 379 } else if (DEBUG) { 380 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 381 } 382 } 383 384 @Override 385 public void stop_impl() { 386 ensureCallingThread(); 387 final MediaPlayerBase player = mPlayer; 388 if (player != null) { 389 player.reset(); 390 } else if (DEBUG) { 391 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 392 } 393 } 394 395 @Override 396 public void skipToPlaylistItem_impl(@NonNull MediaItem2 item) { 397 if (item == null) { 398 throw new IllegalArgumentException("item shouldn't be null"); 399 } 400 final MediaPlaylistAgent agent = mPlaylistAgent; 401 if (agent != null) { 402 agent.skipToPlaylistItem(item); 403 } else if (DEBUG) { 404 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 405 } 406 } 407 408 @Override 409 public void skipToPreviousItem_impl() { 410 final MediaPlaylistAgent agent = mPlaylistAgent; 411 if (agent != null) { 412 agent.skipToPreviousItem(); 413 } else if (DEBUG) { 414 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 415 } 416 } 417 418 @Override 419 public void skipToNextItem_impl() { 420 final MediaPlaylistAgent agent = mPlaylistAgent; 421 if (agent != null) { 422 agent.skipToNextItem(); 423 } else if (DEBUG) { 424 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 425 } 426 } 427 428 @Override 429 public void setCustomLayout_impl(@NonNull ControllerInfo controller, 430 @NonNull List<CommandButton> layout) { 431 ensureCallingThread(); 432 if (controller == null) { 433 throw new IllegalArgumentException("controller shouldn't be null"); 434 } 435 if (layout == null) { 436 throw new IllegalArgumentException("layout shouldn't be null"); 437 } 438 mSessionStub.notifyCustomLayoutNotLocked(controller, layout); 439 } 440 441 ////////////////////////////////////////////////////////////////////////////////////// 442 // TODO(jaewan): Implement follows 443 ////////////////////////////////////////////////////////////////////////////////////// 444 445 @Override 446 public void setAllowedCommands_impl(@NonNull ControllerInfo controller, 447 @NonNull SessionCommandGroup2 commands) { 448 if (controller == null) { 449 throw new IllegalArgumentException("controller shouldn't be null"); 450 } 451 if (commands == null) { 452 throw new IllegalArgumentException("commands shouldn't be null"); 453 } 454 mSessionStub.setAllowedCommands(controller, commands); 455 } 456 457 @Override 458 public void sendCustomCommand_impl(@NonNull ControllerInfo controller, 459 @NonNull SessionCommand2 command, Bundle args, ResultReceiver receiver) { 460 if (controller == null) { 461 throw new IllegalArgumentException("controller shouldn't be null"); 462 } 463 if (command == null) { 464 throw new IllegalArgumentException("command shouldn't be null"); 465 } 466 mSessionStub.sendCustomCommand(controller, command, args, receiver); 467 } 468 469 @Override 470 public void sendCustomCommand_impl(@NonNull SessionCommand2 command, Bundle args) { 471 if (command == null) { 472 throw new IllegalArgumentException("command shouldn't be null"); 473 } 474 mSessionStub.sendCustomCommand(command, args); 475 } 476 477 @Override 478 public void setPlaylist_impl(@NonNull List<MediaItem2> list, MediaMetadata2 metadata) { 479 if (list == null) { 480 throw new IllegalArgumentException("list shouldn't be null"); 481 } 482 ensureCallingThread(); 483 final MediaPlaylistAgent agent = mPlaylistAgent; 484 if (agent != null) { 485 agent.setPlaylist(list, metadata); 486 } else if (DEBUG) { 487 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 488 } 489 } 490 491 @Override 492 public void updatePlaylistMetadata_impl(MediaMetadata2 metadata) { 493 final MediaPlaylistAgent agent = mPlaylistAgent; 494 if (agent != null) { 495 agent.updatePlaylistMetadata(metadata); 496 } else if (DEBUG) { 497 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 498 } 499 } 500 501 @Override 502 public void addPlaylistItem_impl(int index, @NonNull MediaItem2 item) { 503 if (index < 0) { 504 throw new IllegalArgumentException("index shouldn't be negative"); 505 } 506 if (item == null) { 507 throw new IllegalArgumentException("item shouldn't be null"); 508 } 509 final MediaPlaylistAgent agent = mPlaylistAgent; 510 if (agent != null) { 511 agent.addPlaylistItem(index, item); 512 } else if (DEBUG) { 513 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 514 } 515 } 516 517 @Override 518 public void removePlaylistItem_impl(@NonNull MediaItem2 item) { 519 if (item == null) { 520 throw new IllegalArgumentException("item shouldn't be null"); 521 } 522 final MediaPlaylistAgent agent = mPlaylistAgent; 523 if (agent != null) { 524 agent.removePlaylistItem(item); 525 } else if (DEBUG) { 526 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 527 } 528 } 529 530 @Override 531 public void replacePlaylistItem_impl(int index, @NonNull MediaItem2 item) { 532 if (index < 0) { 533 throw new IllegalArgumentException("index shouldn't be negative"); 534 } 535 if (item == null) { 536 throw new IllegalArgumentException("item shouldn't be null"); 537 } 538 final MediaPlaylistAgent agent = mPlaylistAgent; 539 if (agent != null) { 540 agent.replacePlaylistItem(index, item); 541 } else if (DEBUG) { 542 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 543 } 544 } 545 546 @Override 547 public List<MediaItem2> getPlaylist_impl() { 548 final MediaPlaylistAgent agent = mPlaylistAgent; 549 if (agent != null) { 550 return agent.getPlaylist(); 551 } else if (DEBUG) { 552 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 553 } 554 return null; 555 } 556 557 @Override 558 public MediaMetadata2 getPlaylistMetadata_impl() { 559 final MediaPlaylistAgent agent = mPlaylistAgent; 560 if (agent != null) { 561 return agent.getPlaylistMetadata(); 562 } else if (DEBUG) { 563 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 564 } 565 return null; 566 } 567 568 @Override 569 public MediaItem2 getCurrentPlaylistItem_impl() { 570 // TODO(jaewan): Implement 571 return null; 572 } 573 574 @Override 575 public int getRepeatMode_impl() { 576 final MediaPlaylistAgent agent = mPlaylistAgent; 577 if (agent != null) { 578 return agent.getRepeatMode(); 579 } else if (DEBUG) { 580 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 581 } 582 return MediaPlaylistAgent.REPEAT_MODE_NONE; 583 } 584 585 @Override 586 public void setRepeatMode_impl(int repeatMode) { 587 final MediaPlaylistAgent agent = mPlaylistAgent; 588 if (agent != null) { 589 agent.setRepeatMode(repeatMode); 590 } else if (DEBUG) { 591 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 592 } 593 } 594 595 @Override 596 public int getShuffleMode_impl() { 597 final MediaPlaylistAgent agent = mPlaylistAgent; 598 if (agent != null) { 599 return agent.getShuffleMode(); 600 } else if (DEBUG) { 601 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 602 } 603 return MediaPlaylistAgent.SHUFFLE_MODE_NONE; 604 } 605 606 @Override 607 public void setShuffleMode_impl(int shuffleMode) { 608 final MediaPlaylistAgent agent = mPlaylistAgent; 609 if (agent != null) { 610 agent.setShuffleMode(shuffleMode); 611 } else if (DEBUG) { 612 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 613 } 614 } 615 616 @Override 617 public void prepare_impl() { 618 ensureCallingThread(); 619 final MediaPlayerBase player = mPlayer; 620 if (player != null) { 621 player.prepare(); 622 } else if (DEBUG) { 623 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 624 } 625 } 626 627 @Override 628 public void seekTo_impl(long pos) { 629 ensureCallingThread(); 630 final MediaPlayerBase player = mPlayer; 631 if (player != null) { 632 player.seekTo(pos); 633 } else if (DEBUG) { 634 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 635 } 636 } 637 638 @Override 639 public @PlayerState int getPlayerState_impl() { 640 final MediaPlayerBase player = mPlayer; 641 if (player != null) { 642 return mPlayer.getPlayerState(); 643 } else if (DEBUG) { 644 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 645 } 646 return MediaPlayerBase.PLAYER_STATE_ERROR; 647 } 648 649 @Override 650 public long getCurrentPosition_impl() { 651 final MediaPlayerBase player = mPlayer; 652 if (player != null) { 653 return mPlayer.getCurrentPosition(); 654 } else if (DEBUG) { 655 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 656 } 657 return MediaPlayerBase.UNKNOWN_TIME; 658 } 659 660 @Override 661 public long getBufferedPosition_impl() { 662 final MediaPlayerBase player = mPlayer; 663 if (player != null) { 664 return mPlayer.getBufferedPosition(); 665 } else if (DEBUG) { 666 Log.d(TAG, "API calls after the close()", new IllegalStateException()); 667 } 668 return MediaPlayerBase.UNKNOWN_TIME; 669 } 670 671 @Override 672 public void notifyError_impl(int errorCode, Bundle extras) { 673 mSessionStub.notifyError(errorCode, extras); 674 } 675 676 @Override 677 public void setOnDataSourceMissingHelper_impl(@NonNull OnDataSourceMissingHelper helper) { 678 if (helper == null) { 679 throw new IllegalArgumentException("helper shouldn't be null"); 680 } 681 synchronized (mLock) { 682 mDsmHelper = helper; 683 if (mSessionPlaylistAgent != null) { 684 mSessionPlaylistAgent.setOnDataSourceMissingHelper(helper); 685 } 686 } 687 } 688 689 @Override 690 public void clearOnDataSourceMissingHelper_impl() { 691 synchronized (mLock) { 692 mDsmHelper = null; 693 if (mSessionPlaylistAgent != null) { 694 mSessionPlaylistAgent.clearOnDataSourceMissingHelper(); 695 } 696 } 697 } 698 699 /////////////////////////////////////////////////// 700 // Protected or private methods 701 /////////////////////////////////////////////////// 702 703 // Enforces developers to call all the methods on the initially given thread 704 // because calls from the MediaController2 will be run on the thread. 705 // TODO(jaewan): Should we allow calls from the multiple thread? 706 // I prefer this way because allowing multiple thread may case tricky issue like 707 // b/63446360. If the {@link #setPlayer()} with {@code null} can be called from 708 // another thread, transport controls can be called after that. 709 // That's basically the developer's mistake, but they cannot understand what's 710 // happening behind until we tell them so. 711 // If enforcing callling thread doesn't look good, we can alternatively pick 712 // 1. Allow calls from random threads for all methods. 713 // 2. Allow calls from random threads for all methods, except for the 714 // {@link #setPlayer()}. 715 void ensureCallingThread() { 716 // TODO(jaewan): Uncomment or remove 717 /* 718 if (mHandler.getLooper() != Looper.myLooper()) { 719 throw new IllegalStateException("Run this on the given thread"); 720 }*/ 721 } 722 723 private void notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent, 724 List<MediaItem2> list, MediaMetadata2 metadata) { 725 if (playlistAgent != mPlaylistAgent) { 726 // Ignore calls from the old agent. 727 return; 728 } 729 mCallback.onPlaylistChanged(mInstance, playlistAgent, list, metadata); 730 mSessionStub.notifyPlaylistChangedNotLocked(list, metadata); 731 } 732 733 private void notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent, 734 MediaMetadata2 metadata) { 735 if (playlistAgent != mPlaylistAgent) { 736 // Ignore calls from the old agent. 737 return; 738 } 739 mCallback.onPlaylistMetadataChanged(mInstance, playlistAgent, metadata); 740 mSessionStub.notifyPlaylistMetadataChangedNotLocked(metadata); 741 } 742 743 private void notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, 744 int repeatMode) { 745 if (playlistAgent != mPlaylistAgent) { 746 // Ignore calls from the old agent. 747 return; 748 } 749 mCallback.onRepeatModeChanged(mInstance, playlistAgent, repeatMode); 750 mSessionStub.notifyRepeatModeChangedNotLocked(repeatMode); 751 } 752 753 private void notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, 754 int shuffleMode) { 755 if (playlistAgent != mPlaylistAgent) { 756 // Ignore calls from the old agent. 757 return; 758 } 759 mCallback.onShuffleModeChanged(mInstance, playlistAgent, shuffleMode); 760 mSessionStub.notifyShuffleModeChangedNotLocked(shuffleMode); 761 } 762 763 private void notifyPlayerUpdatedNotLocked(MediaPlayerBase oldPlayer) { 764 final MediaPlayerBase player = mPlayer; 765 // TODO(jaewan): (Can be post-P) Find better way for player.getPlayerState() // 766 // In theory, Session.getXXX() may not be the same as Player.getXXX() 767 // and we should notify information of the session.getXXX() instead of 768 // player.getXXX() 769 // Notify to controllers as well. 770 final int state = player.getPlayerState(); 771 if (state != oldPlayer.getPlayerState()) { 772 mSessionStub.notifyPlayerStateChangedNotLocked(state); 773 } 774 775 final long currentTimeMs = System.currentTimeMillis(); 776 final long position = player.getCurrentPosition(); 777 if (position != oldPlayer.getCurrentPosition()) { 778 mSessionStub.notifyPositionChangedNotLocked(currentTimeMs, position); 779 } 780 781 final float speed = player.getPlaybackSpeed(); 782 if (speed != oldPlayer.getPlaybackSpeed()) { 783 mSessionStub.notifyPlaybackSpeedChangedNotLocked(speed); 784 } 785 786 final long bufferedPosition = player.getBufferedPosition(); 787 if (bufferedPosition != oldPlayer.getBufferedPosition()) { 788 mSessionStub.notifyBufferedPositionChangedNotLocked(bufferedPosition); 789 } 790 } 791 792 Context getContext() { 793 return mContext; 794 } 795 796 MediaSession2 getInstance() { 797 return mInstance; 798 } 799 800 MediaPlayerBase getPlayer() { 801 return mPlayer; 802 } 803 804 MediaPlaylistAgent getPlaylistAgent() { 805 return mPlaylistAgent; 806 } 807 808 Executor getCallbackExecutor() { 809 return mCallbackExecutor; 810 } 811 812 SessionCallback getCallback() { 813 return mCallback; 814 } 815 816 MediaSession2Stub getSessionStub() { 817 return mSessionStub; 818 } 819 820 VolumeProvider2 getVolumeProvider() { 821 return mVolumeProvider; 822 } 823 824 PlaybackInfo getPlaybackInfo() { 825 synchronized (mLock) { 826 return mPlaybackInfo; 827 } 828 } 829 830 PendingIntent getSessionActivity() { 831 return mSessionActivity; 832 } 833 834 private static class MyPlayerEventCallback extends PlayerEventCallback { 835 private final WeakReference<MediaSession2Impl> mSession; 836 837 private MyPlayerEventCallback(MediaSession2Impl session) { 838 mSession = new WeakReference<>(session); 839 } 840 841 @Override 842 public void onCurrentDataSourceChanged(MediaPlayerBase mpb, DataSourceDesc dsd) { 843 MediaSession2Impl session = getSession(); 844 if (session == null || dsd == null) { 845 return; 846 } 847 session.getCallbackExecutor().execute(() -> { 848 MediaItem2 item = getMediaItem(session, dsd); 849 if (item == null) { 850 return; 851 } 852 session.getCallback().onCurrentMediaItemChanged(session.getInstance(), mpb, item); 853 // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936) 854 }); 855 } 856 857 @Override 858 public void onMediaPrepared(MediaPlayerBase mpb, DataSourceDesc dsd) { 859 MediaSession2Impl session = getSession(); 860 if (session == null || dsd == null) { 861 return; 862 } 863 session.getCallbackExecutor().execute(() -> { 864 MediaItem2 item = getMediaItem(session, dsd); 865 if (item == null) { 866 return; 867 } 868 session.getCallback().onMediaPrepared(session.getInstance(), mpb, item); 869 // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936) 870 }); 871 } 872 873 @Override 874 public void onPlayerStateChanged(MediaPlayerBase mpb, int state) { 875 MediaSession2Impl session = getSession(); 876 if (session == null) { 877 return; 878 } 879 session.getCallbackExecutor().execute(() -> { 880 session.getCallback().onPlayerStateChanged(session.getInstance(), mpb, state); 881 session.getSessionStub().notifyPlayerStateChangedNotLocked(state); 882 }); 883 } 884 885 @Override 886 public void onBufferingStateChanged(MediaPlayerBase mpb, DataSourceDesc dsd, int state) { 887 MediaSession2Impl session = getSession(); 888 if (session == null || dsd == null) { 889 return; 890 } 891 session.getCallbackExecutor().execute(() -> { 892 MediaItem2 item = getMediaItem(session, dsd); 893 if (item == null) { 894 return; 895 } 896 session.getCallback().onBufferingStateChanged( 897 session.getInstance(), mpb, item, state); 898 // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936) 899 }); 900 } 901 902 private MediaSession2Impl getSession() { 903 final MediaSession2Impl session = mSession.get(); 904 if (session == null && DEBUG) { 905 Log.d(TAG, "Session is closed", new IllegalStateException()); 906 } 907 return session; 908 } 909 910 private MediaItem2 getMediaItem(MediaSession2Impl session, DataSourceDesc dsd) { 911 MediaPlaylistAgent agent = session.getPlaylistAgent(); 912 if (agent == null) { 913 if (DEBUG) { 914 Log.d(TAG, "Session is closed", new IllegalStateException()); 915 } 916 return null; 917 } 918 MediaItem2 item = agent.getMediaItem(dsd); 919 if (item == null) { 920 if (DEBUG) { 921 Log.d(TAG, "Could not find matching item for dsd=" + dsd, 922 new NoSuchElementException()); 923 } 924 } 925 return item; 926 } 927 } 928 929 private static class MyPlaylistEventCallback extends PlaylistEventCallback { 930 private final WeakReference<MediaSession2Impl> mSession; 931 932 private MyPlaylistEventCallback(MediaSession2Impl session) { 933 mSession = new WeakReference<>(session); 934 } 935 936 @Override 937 public void onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list, 938 MediaMetadata2 metadata) { 939 final MediaSession2Impl session = mSession.get(); 940 if (session == null) { 941 return; 942 } 943 session.notifyPlaylistChangedOnExecutor(playlistAgent, list, metadata); 944 } 945 946 @Override 947 public void onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent, 948 MediaMetadata2 metadata) { 949 final MediaSession2Impl session = mSession.get(); 950 if (session == null) { 951 return; 952 } 953 session.notifyPlaylistMetadataChangedOnExecutor(playlistAgent, metadata); 954 } 955 956 @Override 957 public void onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode) { 958 final MediaSession2Impl session = mSession.get(); 959 if (session == null) { 960 return; 961 } 962 session.notifyRepeatModeChangedOnExecutor(playlistAgent, repeatMode); 963 } 964 965 @Override 966 public void onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode) { 967 final MediaSession2Impl session = mSession.get(); 968 if (session == null) { 969 return; 970 } 971 session.notifyShuffleModeChangedOnExecutor(playlistAgent, shuffleMode); 972 } 973 } 974 975 public static final class CommandImpl implements CommandProvider { 976 private static final String KEY_COMMAND_CODE 977 = "android.media.media_session2.command.command_code"; 978 private static final String KEY_COMMAND_CUSTOM_COMMAND 979 = "android.media.media_session2.command.custom_command"; 980 private static final String KEY_COMMAND_EXTRAS 981 = "android.media.media_session2.command.extras"; 982 983 private final SessionCommand2 mInstance; 984 private final int mCommandCode; 985 // Nonnull if it's custom command 986 private final String mCustomCommand; 987 private final Bundle mExtras; 988 989 public CommandImpl(SessionCommand2 instance, int commandCode) { 990 mInstance = instance; 991 mCommandCode = commandCode; 992 mCustomCommand = null; 993 mExtras = null; 994 } 995 996 public CommandImpl(SessionCommand2 instance, @NonNull String action, 997 @Nullable Bundle extras) { 998 if (action == null) { 999 throw new IllegalArgumentException("action shouldn't be null"); 1000 } 1001 mInstance = instance; 1002 mCommandCode = COMMAND_CODE_CUSTOM; 1003 mCustomCommand = action; 1004 mExtras = extras; 1005 } 1006 1007 @Override 1008 public int getCommandCode_impl() { 1009 return mCommandCode; 1010 } 1011 1012 @Override 1013 public @Nullable String getCustomCommand_impl() { 1014 return mCustomCommand; 1015 } 1016 1017 @Override 1018 public @Nullable Bundle getExtras_impl() { 1019 return mExtras; 1020 } 1021 1022 /** 1023 * @return a new Bundle instance from the Command 1024 */ 1025 @Override 1026 public Bundle toBundle_impl() { 1027 Bundle bundle = new Bundle(); 1028 bundle.putInt(KEY_COMMAND_CODE, mCommandCode); 1029 bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand); 1030 bundle.putBundle(KEY_COMMAND_EXTRAS, mExtras); 1031 return bundle; 1032 } 1033 1034 /** 1035 * @return a new Command instance from the Bundle 1036 */ 1037 public static SessionCommand2 fromBundle_impl(@NonNull Bundle command) { 1038 if (command == null) { 1039 throw new IllegalArgumentException("command shouldn't be null"); 1040 } 1041 int code = command.getInt(KEY_COMMAND_CODE); 1042 if (code != COMMAND_CODE_CUSTOM) { 1043 return new SessionCommand2(code); 1044 } else { 1045 String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND); 1046 if (customCommand == null) { 1047 return null; 1048 } 1049 return new SessionCommand2(customCommand, command.getBundle(KEY_COMMAND_EXTRAS)); 1050 } 1051 } 1052 1053 @Override 1054 public boolean equals_impl(Object obj) { 1055 if (!(obj instanceof CommandImpl)) { 1056 return false; 1057 } 1058 CommandImpl other = (CommandImpl) obj; 1059 // TODO(jaewan): Compare Commands with the generated UUID, as we're doing for the MI2. 1060 return mCommandCode == other.mCommandCode 1061 && TextUtils.equals(mCustomCommand, other.mCustomCommand); 1062 } 1063 1064 @Override 1065 public int hashCode_impl() { 1066 final int prime = 31; 1067 return ((mCustomCommand != null) 1068 ? mCustomCommand.hashCode() : 0) * prime + mCommandCode; 1069 } 1070 } 1071 1072 /** 1073 * Represent set of {@link SessionCommand2}. 1074 */ 1075 public static class CommandGroupImpl implements CommandGroupProvider { 1076 private static final String KEY_COMMANDS = 1077 "android.media.mediasession2.commandgroup.commands"; 1078 1079 // Prefix for all command codes 1080 private static final String PREFIX_COMMAND_CODE = "COMMAND_CODE_"; 1081 1082 // Prefix for command codes that will be sent directly to the MediaPlayerBase 1083 private static final String PREFIX_COMMAND_CODE_PLAYBACK = "COMMAND_CODE_PLAYBACK_"; 1084 1085 // Prefix for command codes that will be sent directly to the MediaPlaylistAgent 1086 private static final String PREFIX_COMMAND_CODE_PLAYLIST = "COMMAND_CODE_PLAYLIST_"; 1087 1088 private Set<SessionCommand2> mCommands = new HashSet<>(); 1089 private final SessionCommandGroup2 mInstance; 1090 1091 public CommandGroupImpl(SessionCommandGroup2 instance, Object other) { 1092 mInstance = instance; 1093 if (other != null && other instanceof CommandGroupImpl) { 1094 mCommands.addAll(((CommandGroupImpl) other).mCommands); 1095 } 1096 } 1097 1098 public CommandGroupImpl() { 1099 mInstance = new SessionCommandGroup2(this); 1100 } 1101 1102 @Override 1103 public void addCommand_impl(@NonNull SessionCommand2 command) { 1104 if (command == null) { 1105 throw new IllegalArgumentException("command shouldn't be null"); 1106 } 1107 mCommands.add(command); 1108 } 1109 1110 @Override 1111 public void addAllPredefinedCommands_impl() { 1112 addCommandsWithPrefix(PREFIX_COMMAND_CODE); 1113 } 1114 1115 void addAllPlaybackCommands() { 1116 addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYBACK); 1117 } 1118 1119 void addAllPlaylistCommands() { 1120 addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYLIST); 1121 } 1122 1123 private void addCommandsWithPrefix(String prefix) { 1124 // TODO(jaewan): (Can be post-P): Don't use reflection for this purpose. 1125 final Field[] fields = MediaSession2.class.getFields(); 1126 if (fields != null) { 1127 for (int i = 0; i < fields.length; i++) { 1128 if (fields[i].getName().startsWith(prefix)) { 1129 try { 1130 mCommands.add(new SessionCommand2(fields[i].getInt(null))); 1131 } catch (IllegalAccessException e) { 1132 Log.w(TAG, "Unexpected " + fields[i] + " in MediaSession2"); 1133 } 1134 } 1135 } 1136 } 1137 } 1138 1139 @Override 1140 public void removeCommand_impl(@NonNull SessionCommand2 command) { 1141 if (command == null) { 1142 throw new IllegalArgumentException("command shouldn't be null"); 1143 } 1144 mCommands.remove(command); 1145 } 1146 1147 @Override 1148 public boolean hasCommand_impl(@NonNull SessionCommand2 command) { 1149 if (command == null) { 1150 throw new IllegalArgumentException("command shouldn't be null"); 1151 } 1152 return mCommands.contains(command); 1153 } 1154 1155 @Override 1156 public boolean hasCommand_impl(int code) { 1157 if (code == COMMAND_CODE_CUSTOM) { 1158 throw new IllegalArgumentException("Use hasCommand(Command) for custom command"); 1159 } 1160 for (SessionCommand2 command : mCommands) { 1161 if (command.getCommandCode() == code) { 1162 return true; 1163 } 1164 } 1165 return false; 1166 } 1167 1168 @Override 1169 public Set<SessionCommand2> getCommands_impl() { 1170 return getCommands(); 1171 } 1172 1173 public Set<SessionCommand2> getCommands() { 1174 return Collections.unmodifiableSet(mCommands); 1175 } 1176 1177 /** 1178 * @return new bundle from the CommandGroup 1179 * @hide 1180 */ 1181 @Override 1182 public Bundle toBundle_impl() { 1183 ArrayList<Bundle> list = new ArrayList<>(); 1184 for (SessionCommand2 command : mCommands) { 1185 list.add(command.toBundle()); 1186 } 1187 Bundle bundle = new Bundle(); 1188 bundle.putParcelableArrayList(KEY_COMMANDS, list); 1189 return bundle; 1190 } 1191 1192 /** 1193 * @return new instance of CommandGroup from the bundle 1194 * @hide 1195 */ 1196 public static @Nullable SessionCommandGroup2 fromBundle_impl(Bundle commands) { 1197 if (commands == null) { 1198 return null; 1199 } 1200 List<Parcelable> list = commands.getParcelableArrayList(KEY_COMMANDS); 1201 if (list == null) { 1202 return null; 1203 } 1204 SessionCommandGroup2 commandGroup = new SessionCommandGroup2(); 1205 for (int i = 0; i < list.size(); i++) { 1206 Parcelable parcelable = list.get(i); 1207 if (!(parcelable instanceof Bundle)) { 1208 continue; 1209 } 1210 Bundle commandBundle = (Bundle) parcelable; 1211 SessionCommand2 command = SessionCommand2.fromBundle(commandBundle); 1212 if (command != null) { 1213 commandGroup.addCommand(command); 1214 } 1215 } 1216 return commandGroup; 1217 } 1218 } 1219 1220 public static class ControllerInfoImpl implements ControllerInfoProvider { 1221 private final ControllerInfo mInstance; 1222 private final int mUid; 1223 private final String mPackageName; 1224 private final boolean mIsTrusted; 1225 private final IMediaController2 mControllerBinder; 1226 1227 public ControllerInfoImpl(Context context, ControllerInfo instance, int uid, 1228 int pid, @NonNull String packageName, @NonNull IMediaController2 callback) { 1229 if (TextUtils.isEmpty(packageName)) { 1230 throw new IllegalArgumentException("packageName shouldn't be empty"); 1231 } 1232 if (callback == null) { 1233 throw new IllegalArgumentException("callback shouldn't be null"); 1234 } 1235 1236 mInstance = instance; 1237 mUid = uid; 1238 mPackageName = packageName; 1239 mControllerBinder = callback; 1240 MediaSessionManager manager = 1241 (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); 1242 // Ask server whether the controller is trusted. 1243 // App cannot know this because apps cannot query enabled notification listener for 1244 // another package, but system server can do. 1245 mIsTrusted = manager.isTrustedForMediaControl( 1246 new MediaSessionManager.RemoteUserInfo(packageName, pid, uid)); 1247 } 1248 1249 @Override 1250 public String getPackageName_impl() { 1251 return mPackageName; 1252 } 1253 1254 @Override 1255 public int getUid_impl() { 1256 return mUid; 1257 } 1258 1259 @Override 1260 public boolean isTrusted_impl() { 1261 return mIsTrusted; 1262 } 1263 1264 @Override 1265 public int hashCode_impl() { 1266 return mControllerBinder.hashCode(); 1267 } 1268 1269 @Override 1270 public boolean equals_impl(Object obj) { 1271 if (!(obj instanceof ControllerInfo)) { 1272 return false; 1273 } 1274 return equals(((ControllerInfo) obj).getProvider()); 1275 } 1276 1277 @Override 1278 public String toString_impl() { 1279 return "ControllerInfo {pkg=" + mPackageName + ", uid=" + mUid + ", trusted=" 1280 + mIsTrusted + "}"; 1281 } 1282 1283 @Override 1284 public int hashCode() { 1285 return mControllerBinder.hashCode(); 1286 } 1287 1288 @Override 1289 public boolean equals(Object obj) { 1290 if (!(obj instanceof ControllerInfoImpl)) { 1291 return false; 1292 } 1293 ControllerInfoImpl other = (ControllerInfoImpl) obj; 1294 return mControllerBinder.asBinder().equals(other.mControllerBinder.asBinder()); 1295 } 1296 1297 ControllerInfo getInstance() { 1298 return mInstance; 1299 } 1300 1301 IBinder getId() { 1302 return mControllerBinder.asBinder(); 1303 } 1304 1305 IMediaController2 getControllerBinder() { 1306 return mControllerBinder; 1307 } 1308 1309 static ControllerInfoImpl from(ControllerInfo controller) { 1310 return (ControllerInfoImpl) controller.getProvider(); 1311 } 1312 } 1313 1314 public static class CommandButtonImpl implements CommandButtonProvider { 1315 private static final String KEY_COMMAND 1316 = "android.media.media_session2.command_button.command"; 1317 private static final String KEY_ICON_RES_ID 1318 = "android.media.media_session2.command_button.icon_res_id"; 1319 private static final String KEY_DISPLAY_NAME 1320 = "android.media.media_session2.command_button.display_name"; 1321 private static final String KEY_EXTRAS 1322 = "android.media.media_session2.command_button.extras"; 1323 private static final String KEY_ENABLED 1324 = "android.media.media_session2.command_button.enabled"; 1325 1326 private final CommandButton mInstance; 1327 private SessionCommand2 mCommand; 1328 private int mIconResId; 1329 private String mDisplayName; 1330 private Bundle mExtras; 1331 private boolean mEnabled; 1332 1333 public CommandButtonImpl(@Nullable SessionCommand2 command, int iconResId, 1334 @Nullable String displayName, Bundle extras, boolean enabled) { 1335 mCommand = command; 1336 mIconResId = iconResId; 1337 mDisplayName = displayName; 1338 mExtras = extras; 1339 mEnabled = enabled; 1340 mInstance = new CommandButton(this); 1341 } 1342 1343 @Override 1344 public @Nullable 1345 SessionCommand2 getCommand_impl() { 1346 return mCommand; 1347 } 1348 1349 @Override 1350 public int getIconResId_impl() { 1351 return mIconResId; 1352 } 1353 1354 @Override 1355 public @Nullable String getDisplayName_impl() { 1356 return mDisplayName; 1357 } 1358 1359 @Override 1360 public @Nullable Bundle getExtras_impl() { 1361 return mExtras; 1362 } 1363 1364 @Override 1365 public boolean isEnabled_impl() { 1366 return mEnabled; 1367 } 1368 1369 @NonNull Bundle toBundle() { 1370 Bundle bundle = new Bundle(); 1371 bundle.putBundle(KEY_COMMAND, mCommand.toBundle()); 1372 bundle.putInt(KEY_ICON_RES_ID, mIconResId); 1373 bundle.putString(KEY_DISPLAY_NAME, mDisplayName); 1374 bundle.putBundle(KEY_EXTRAS, mExtras); 1375 bundle.putBoolean(KEY_ENABLED, mEnabled); 1376 return bundle; 1377 } 1378 1379 static @Nullable CommandButton fromBundle(Bundle bundle) { 1380 if (bundle == null) { 1381 return null; 1382 } 1383 CommandButton.Builder builder = new CommandButton.Builder(); 1384 builder.setCommand(SessionCommand2.fromBundle(bundle.getBundle(KEY_COMMAND))); 1385 builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0)); 1386 builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME)); 1387 builder.setExtras(bundle.getBundle(KEY_EXTRAS)); 1388 builder.setEnabled(bundle.getBoolean(KEY_ENABLED)); 1389 try { 1390 return builder.build(); 1391 } catch (IllegalStateException e) { 1392 // Malformed or version mismatch. Return null for now. 1393 return null; 1394 } 1395 } 1396 1397 /** 1398 * Builder for {@link CommandButton}. 1399 */ 1400 public static class BuilderImpl implements CommandButtonProvider.BuilderProvider { 1401 private final CommandButton.Builder mInstance; 1402 private SessionCommand2 mCommand; 1403 private int mIconResId; 1404 private String mDisplayName; 1405 private Bundle mExtras; 1406 private boolean mEnabled; 1407 1408 public BuilderImpl(CommandButton.Builder instance) { 1409 mInstance = instance; 1410 mEnabled = true; 1411 } 1412 1413 @Override 1414 public CommandButton.Builder setCommand_impl(SessionCommand2 command) { 1415 mCommand = command; 1416 return mInstance; 1417 } 1418 1419 @Override 1420 public CommandButton.Builder setIconResId_impl(int resId) { 1421 mIconResId = resId; 1422 return mInstance; 1423 } 1424 1425 @Override 1426 public CommandButton.Builder setDisplayName_impl(String displayName) { 1427 mDisplayName = displayName; 1428 return mInstance; 1429 } 1430 1431 @Override 1432 public CommandButton.Builder setEnabled_impl(boolean enabled) { 1433 mEnabled = enabled; 1434 return mInstance; 1435 } 1436 1437 @Override 1438 public CommandButton.Builder setExtras_impl(Bundle extras) { 1439 mExtras = extras; 1440 return mInstance; 1441 } 1442 1443 @Override 1444 public CommandButton build_impl() { 1445 if (mEnabled && mCommand == null) { 1446 throw new IllegalStateException("Enabled button needs Command" 1447 + " for controller to invoke the command"); 1448 } 1449 if (mCommand != null && mCommand.getCommandCode() == COMMAND_CODE_CUSTOM 1450 && (mIconResId == 0 || TextUtils.isEmpty(mDisplayName))) { 1451 throw new IllegalStateException("Custom commands needs icon and" 1452 + " and name to display"); 1453 } 1454 return new CommandButtonImpl(mCommand, mIconResId, mDisplayName, mExtras, mEnabled) 1455 .mInstance; 1456 } 1457 } 1458 } 1459 1460 public static abstract class BuilderBaseImpl<T extends MediaSession2, C extends SessionCallback> 1461 implements BuilderBaseProvider<T, C> { 1462 final Context mContext; 1463 MediaPlayerBase mPlayer; 1464 String mId; 1465 Executor mCallbackExecutor; 1466 C mCallback; 1467 MediaPlaylistAgent mPlaylistAgent; 1468 VolumeProvider2 mVolumeProvider; 1469 PendingIntent mSessionActivity; 1470 1471 /** 1472 * Constructor. 1473 * 1474 * @param context a context 1475 * @throws IllegalArgumentException if any parameter is null, or the player is a 1476 * {@link MediaSession2} or {@link MediaController2}. 1477 */ 1478 // TODO(jaewan): Also need executor 1479 public BuilderBaseImpl(@NonNull Context context) { 1480 if (context == null) { 1481 throw new IllegalArgumentException("context shouldn't be null"); 1482 } 1483 mContext = context; 1484 // Ensure non-null 1485 mId = ""; 1486 } 1487 1488 @Override 1489 public void setPlayer_impl(@NonNull MediaPlayerBase player) { 1490 if (player == null) { 1491 throw new IllegalArgumentException("player shouldn't be null"); 1492 } 1493 mPlayer = player; 1494 } 1495 1496 @Override 1497 public void setPlaylistAgent_impl(@NonNull MediaPlaylistAgent playlistAgent) { 1498 if (playlistAgent == null) { 1499 throw new IllegalArgumentException("playlistAgent shouldn't be null"); 1500 } 1501 mPlaylistAgent = playlistAgent; 1502 } 1503 1504 @Override 1505 public void setVolumeProvider_impl(VolumeProvider2 volumeProvider) { 1506 mVolumeProvider = volumeProvider; 1507 } 1508 1509 @Override 1510 public void setSessionActivity_impl(PendingIntent pi) { 1511 mSessionActivity = pi; 1512 } 1513 1514 @Override 1515 public void setId_impl(@NonNull String id) { 1516 if (id == null) { 1517 throw new IllegalArgumentException("id shouldn't be null"); 1518 } 1519 mId = id; 1520 } 1521 1522 @Override 1523 public void setSessionCallback_impl(@NonNull Executor executor, @NonNull C callback) { 1524 if (executor == null) { 1525 throw new IllegalArgumentException("executor shouldn't be null"); 1526 } 1527 if (callback == null) { 1528 throw new IllegalArgumentException("callback shouldn't be null"); 1529 } 1530 mCallbackExecutor = executor; 1531 mCallback = callback; 1532 } 1533 1534 @Override 1535 public abstract T build_impl(); 1536 } 1537 1538 public static class BuilderImpl extends BuilderBaseImpl<MediaSession2, SessionCallback> { 1539 public BuilderImpl(Context context, Builder instance) { 1540 super(context); 1541 } 1542 1543 @Override 1544 public MediaSession2 build_impl() { 1545 if (mCallbackExecutor == null) { 1546 mCallbackExecutor = mContext.getMainExecutor(); 1547 } 1548 if (mCallback == null) { 1549 mCallback = new SessionCallback() {}; 1550 } 1551 return new MediaSession2Impl(mContext, mPlayer, mId, mPlaylistAgent, 1552 mVolumeProvider, mSessionActivity, mCallbackExecutor, mCallback).getInstance(); 1553 } 1554 } 1555} 1556