1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.media; 18 19 import static android.media.SessionCommand2.COMMAND_CODE_CUSTOM; 20 import static android.media.SessionToken2.TYPE_LIBRARY_SERVICE; 21 import static android.media.SessionToken2.TYPE_SESSION; 22 import static android.media.SessionToken2.TYPE_SESSION_SERVICE; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.PendingIntent; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ResolveInfo; 31 import android.media.AudioAttributes; 32 import android.media.AudioFocusRequest; 33 import android.media.AudioManager; 34 import android.media.DataSourceDesc; 35 import android.media.MediaController2; 36 import android.media.MediaController2.PlaybackInfo; 37 import android.media.MediaItem2; 38 import android.media.MediaLibraryService2; 39 import android.media.MediaMetadata2; 40 import android.media.MediaPlayerBase; 41 import android.media.MediaPlayerBase.PlayerEventCallback; 42 import android.media.MediaPlayerBase.PlayerState; 43 import android.media.MediaPlaylistAgent; 44 import android.media.MediaPlaylistAgent.PlaylistEventCallback; 45 import android.media.MediaSession2; 46 import android.media.MediaSession2.Builder; 47 import android.media.SessionCommand2; 48 import android.media.MediaSession2.CommandButton; 49 import android.media.SessionCommandGroup2; 50 import android.media.MediaSession2.ControllerInfo; 51 import android.media.MediaSession2.OnDataSourceMissingHelper; 52 import android.media.MediaSession2.SessionCallback; 53 import android.media.MediaSessionService2; 54 import android.media.SessionToken2; 55 import android.media.VolumeProvider2; 56 import android.media.session.MediaSessionManager; 57 import android.media.update.MediaSession2Provider; 58 import android.os.Bundle; 59 import android.os.IBinder; 60 import android.os.Parcelable; 61 import android.os.Process; 62 import android.os.ResultReceiver; 63 import android.support.annotation.GuardedBy; 64 import android.text.TextUtils; 65 import android.util.Log; 66 67 import java.lang.ref.WeakReference; 68 import java.lang.reflect.Field; 69 import java.util.ArrayList; 70 import java.util.Collections; 71 import java.util.HashSet; 72 import java.util.List; 73 import java.util.NoSuchElementException; 74 import java.util.Set; 75 import java.util.concurrent.Executor; 76 77 public 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 */ MediaSession2Impl(Context context, MediaPlayerBase player, String id, MediaPlaylistAgent playlistAgent, VolumeProvider2 volumeProvider, PendingIntent sessionActivity, Executor callbackExecutor, SessionCallback callback)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 createInstance()186 MediaSession2 createInstance() { 187 return new MediaSession2(this); 188 } 189 getServiceName(Context context, String serviceAction, String id)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 updatePlayer_impl(@onNull MediaPlayerBase player, MediaPlaylistAgent playlistAgent, VolumeProvider2 volumeProvider)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 updatePlayer(MediaPlayerBase player, MediaPlaylistAgent agent, VolumeProvider2 volumeProvider)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 createPlaybackInfo(VolumeProvider2 volumeProvider, AudioAttributes attrs)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 close_impl()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 getPlayer_impl()333 public MediaPlayerBase getPlayer_impl() { 334 return getPlayer(); 335 } 336 337 @Override getPlaylistAgent_impl()338 public MediaPlaylistAgent getPlaylistAgent_impl() { 339 return mPlaylistAgent; 340 } 341 342 @Override getVolumeProvider_impl()343 public VolumeProvider2 getVolumeProvider_impl() { 344 return mVolumeProvider; 345 } 346 347 @Override getToken_impl()348 public SessionToken2 getToken_impl() { 349 return mSessionToken; 350 } 351 352 @Override getConnectedControllers_impl()353 public List<ControllerInfo> getConnectedControllers_impl() { 354 return mSessionStub.getControllers(); 355 } 356 357 @Override setAudioFocusRequest_impl(AudioFocusRequest afr)358 public void setAudioFocusRequest_impl(AudioFocusRequest afr) { 359 // implement 360 } 361 362 @Override play_impl()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 pause_impl()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 stop_impl()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 skipToPlaylistItem_impl(@onNull MediaItem2 item)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 skipToPreviousItem_impl()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 skipToNextItem_impl()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 setCustomLayout_impl(@onNull ControllerInfo controller, @NonNull List<CommandButton> layout)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 setAllowedCommands_impl(@onNull ControllerInfo controller, @NonNull SessionCommandGroup2 commands)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 sendCustomCommand_impl(@onNull ControllerInfo controller, @NonNull SessionCommand2 command, Bundle args, ResultReceiver receiver)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 sendCustomCommand_impl(@onNull SessionCommand2 command, Bundle args)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 setPlaylist_impl(@onNull List<MediaItem2> list, MediaMetadata2 metadata)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 updatePlaylistMetadata_impl(MediaMetadata2 metadata)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 addPlaylistItem_impl(int index, @NonNull MediaItem2 item)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 removePlaylistItem_impl(@onNull MediaItem2 item)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 replacePlaylistItem_impl(int index, @NonNull MediaItem2 item)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 getPlaylist_impl()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 getPlaylistMetadata_impl()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 getCurrentPlaylistItem_impl()569 public MediaItem2 getCurrentPlaylistItem_impl() { 570 // TODO(jaewan): Implement 571 return null; 572 } 573 574 @Override getRepeatMode_impl()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 setRepeatMode_impl(int repeatMode)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 getShuffleMode_impl()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 setShuffleMode_impl(int shuffleMode)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 prepare_impl()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 seekTo_impl(long pos)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 getPlayerState_impl()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 getCurrentPosition_impl()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 getBufferedPosition_impl()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 notifyError_impl(int errorCode, Bundle extras)672 public void notifyError_impl(int errorCode, Bundle extras) { 673 mSessionStub.notifyError(errorCode, extras); 674 } 675 676 @Override setOnDataSourceMissingHelper_impl(@onNull OnDataSourceMissingHelper helper)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 clearOnDataSourceMissingHelper_impl()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()}. ensureCallingThread()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 notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent, List<MediaItem2> list, MediaMetadata2 metadata)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 notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent, MediaMetadata2 metadata)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 notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, int repeatMode)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 notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent, int shuffleMode)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 notifyPlayerUpdatedNotLocked(MediaPlayerBase oldPlayer)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 getContext()792 Context getContext() { 793 return mContext; 794 } 795 getInstance()796 MediaSession2 getInstance() { 797 return mInstance; 798 } 799 getPlayer()800 MediaPlayerBase getPlayer() { 801 return mPlayer; 802 } 803 getPlaylistAgent()804 MediaPlaylistAgent getPlaylistAgent() { 805 return mPlaylistAgent; 806 } 807 getCallbackExecutor()808 Executor getCallbackExecutor() { 809 return mCallbackExecutor; 810 } 811 getCallback()812 SessionCallback getCallback() { 813 return mCallback; 814 } 815 getSessionStub()816 MediaSession2Stub getSessionStub() { 817 return mSessionStub; 818 } 819 getVolumeProvider()820 VolumeProvider2 getVolumeProvider() { 821 return mVolumeProvider; 822 } 823 getPlaybackInfo()824 PlaybackInfo getPlaybackInfo() { 825 synchronized (mLock) { 826 return mPlaybackInfo; 827 } 828 } 829 getSessionActivity()830 PendingIntent getSessionActivity() { 831 return mSessionActivity; 832 } 833 834 private static class MyPlayerEventCallback extends PlayerEventCallback { 835 private final WeakReference<MediaSession2Impl> mSession; 836 MyPlayerEventCallback(MediaSession2Impl session)837 private MyPlayerEventCallback(MediaSession2Impl session) { 838 mSession = new WeakReference<>(session); 839 } 840 841 @Override onCurrentDataSourceChanged(MediaPlayerBase mpb, DataSourceDesc dsd)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 onMediaPrepared(MediaPlayerBase mpb, DataSourceDesc dsd)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 onPlayerStateChanged(MediaPlayerBase mpb, int state)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 onBufferingStateChanged(MediaPlayerBase mpb, DataSourceDesc dsd, int state)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 getSession()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 getMediaItem(MediaSession2Impl session, DataSourceDesc dsd)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 MyPlaylistEventCallback(MediaSession2Impl session)932 private MyPlaylistEventCallback(MediaSession2Impl session) { 933 mSession = new WeakReference<>(session); 934 } 935 936 @Override onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list, MediaMetadata2 metadata)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 onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent, MediaMetadata2 metadata)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 onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode)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 onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode)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 CommandImpl(SessionCommand2 instance, int commandCode)989 public CommandImpl(SessionCommand2 instance, int commandCode) { 990 mInstance = instance; 991 mCommandCode = commandCode; 992 mCustomCommand = null; 993 mExtras = null; 994 } 995 CommandImpl(SessionCommand2 instance, @NonNull String action, @Nullable Bundle extras)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 getCommandCode_impl()1008 public int getCommandCode_impl() { 1009 return mCommandCode; 1010 } 1011 1012 @Override getCustomCommand_impl()1013 public @Nullable String getCustomCommand_impl() { 1014 return mCustomCommand; 1015 } 1016 1017 @Override getExtras_impl()1018 public @Nullable Bundle getExtras_impl() { 1019 return mExtras; 1020 } 1021 1022 /** 1023 * @return a new Bundle instance from the Command 1024 */ 1025 @Override toBundle_impl()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 */ fromBundle_impl(@onNull Bundle command)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 equals_impl(Object obj)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 hashCode_impl()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 CommandGroupImpl(SessionCommandGroup2 instance, Object other)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 CommandGroupImpl()1098 public CommandGroupImpl() { 1099 mInstance = new SessionCommandGroup2(this); 1100 } 1101 1102 @Override addCommand_impl(@onNull SessionCommand2 command)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 addAllPredefinedCommands_impl()1111 public void addAllPredefinedCommands_impl() { 1112 addCommandsWithPrefix(PREFIX_COMMAND_CODE); 1113 } 1114 addAllPlaybackCommands()1115 void addAllPlaybackCommands() { 1116 addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYBACK); 1117 } 1118 addAllPlaylistCommands()1119 void addAllPlaylistCommands() { 1120 addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYLIST); 1121 } 1122 addCommandsWithPrefix(String prefix)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 removeCommand_impl(@onNull SessionCommand2 command)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 hasCommand_impl(@onNull SessionCommand2 command)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 hasCommand_impl(int code)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 getCommands_impl()1169 public Set<SessionCommand2> getCommands_impl() { 1170 return getCommands(); 1171 } 1172 getCommands()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 toBundle_impl()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 */ fromBundle_impl(Bundle commands)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 ControllerInfoImpl(Context context, ControllerInfo instance, int uid, int pid, @NonNull String packageName, @NonNull IMediaController2 callback)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 getPackageName_impl()1250 public String getPackageName_impl() { 1251 return mPackageName; 1252 } 1253 1254 @Override getUid_impl()1255 public int getUid_impl() { 1256 return mUid; 1257 } 1258 1259 @Override isTrusted_impl()1260 public boolean isTrusted_impl() { 1261 return mIsTrusted; 1262 } 1263 1264 @Override hashCode_impl()1265 public int hashCode_impl() { 1266 return mControllerBinder.hashCode(); 1267 } 1268 1269 @Override equals_impl(Object obj)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 toString_impl()1278 public String toString_impl() { 1279 return "ControllerInfo {pkg=" + mPackageName + ", uid=" + mUid + ", trusted=" 1280 + mIsTrusted + "}"; 1281 } 1282 1283 @Override hashCode()1284 public int hashCode() { 1285 return mControllerBinder.hashCode(); 1286 } 1287 1288 @Override equals(Object obj)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 getInstance()1297 ControllerInfo getInstance() { 1298 return mInstance; 1299 } 1300 getId()1301 IBinder getId() { 1302 return mControllerBinder.asBinder(); 1303 } 1304 getControllerBinder()1305 IMediaController2 getControllerBinder() { 1306 return mControllerBinder; 1307 } 1308 from(ControllerInfo controller)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 CommandButtonImpl(@ullable SessionCommand2 command, int iconResId, @Nullable String displayName, Bundle extras, boolean enabled)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 getCommand_impl()1345 SessionCommand2 getCommand_impl() { 1346 return mCommand; 1347 } 1348 1349 @Override getIconResId_impl()1350 public int getIconResId_impl() { 1351 return mIconResId; 1352 } 1353 1354 @Override getDisplayName_impl()1355 public @Nullable String getDisplayName_impl() { 1356 return mDisplayName; 1357 } 1358 1359 @Override getExtras_impl()1360 public @Nullable Bundle getExtras_impl() { 1361 return mExtras; 1362 } 1363 1364 @Override isEnabled_impl()1365 public boolean isEnabled_impl() { 1366 return mEnabled; 1367 } 1368 toBundle()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 fromBundle(Bundle bundle)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 BuilderImpl(CommandButton.Builder instance)1408 public BuilderImpl(CommandButton.Builder instance) { 1409 mInstance = instance; 1410 mEnabled = true; 1411 } 1412 1413 @Override setCommand_impl(SessionCommand2 command)1414 public CommandButton.Builder setCommand_impl(SessionCommand2 command) { 1415 mCommand = command; 1416 return mInstance; 1417 } 1418 1419 @Override setIconResId_impl(int resId)1420 public CommandButton.Builder setIconResId_impl(int resId) { 1421 mIconResId = resId; 1422 return mInstance; 1423 } 1424 1425 @Override setDisplayName_impl(String displayName)1426 public CommandButton.Builder setDisplayName_impl(String displayName) { 1427 mDisplayName = displayName; 1428 return mInstance; 1429 } 1430 1431 @Override setEnabled_impl(boolean enabled)1432 public CommandButton.Builder setEnabled_impl(boolean enabled) { 1433 mEnabled = enabled; 1434 return mInstance; 1435 } 1436 1437 @Override setExtras_impl(Bundle extras)1438 public CommandButton.Builder setExtras_impl(Bundle extras) { 1439 mExtras = extras; 1440 return mInstance; 1441 } 1442 1443 @Override build_impl()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 BuilderBaseImpl(@onNull Context context)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 setPlayer_impl(@onNull MediaPlayerBase player)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 setPlaylistAgent_impl(@onNull MediaPlaylistAgent playlistAgent)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 setVolumeProvider_impl(VolumeProvider2 volumeProvider)1505 public void setVolumeProvider_impl(VolumeProvider2 volumeProvider) { 1506 mVolumeProvider = volumeProvider; 1507 } 1508 1509 @Override setSessionActivity_impl(PendingIntent pi)1510 public void setSessionActivity_impl(PendingIntent pi) { 1511 mSessionActivity = pi; 1512 } 1513 1514 @Override setId_impl(@onNull String id)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 setSessionCallback_impl(@onNull Executor executor, @NonNull C callback)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 build_impl()1535 public abstract T build_impl(); 1536 } 1537 1538 public static class BuilderImpl extends BuilderBaseImpl<MediaSession2, SessionCallback> { BuilderImpl(Context context, Builder instance)1539 public BuilderImpl(Context context, Builder instance) { 1540 super(context); 1541 } 1542 1543 @Override build_impl()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