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_SET_VOLUME; 20import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM; 21import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM; 22import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM; 23import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST; 24import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA; 25import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE; 26import static android.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE; 27import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID; 28import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH; 29import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI; 30import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID; 31import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH; 32import static android.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI; 33 34import android.app.PendingIntent; 35import android.content.ComponentName; 36import android.content.Context; 37import android.content.Intent; 38import android.content.ServiceConnection; 39import android.media.AudioAttributes; 40import android.media.MediaController2; 41import android.media.MediaController2.ControllerCallback; 42import android.media.MediaController2.PlaybackInfo; 43import android.media.MediaItem2; 44import android.media.MediaMetadata2; 45import android.media.MediaPlaylistAgent.RepeatMode; 46import android.media.MediaPlaylistAgent.ShuffleMode; 47import android.media.SessionCommand2; 48import android.media.MediaSession2.CommandButton; 49import android.media.SessionCommandGroup2; 50import android.media.MediaSessionService2; 51import android.media.Rating2; 52import android.media.SessionToken2; 53import android.media.update.MediaController2Provider; 54import android.net.Uri; 55import android.os.Bundle; 56import android.os.IBinder; 57import android.os.Process; 58import android.os.RemoteException; 59import android.os.ResultReceiver; 60import android.os.UserHandle; 61import android.support.annotation.GuardedBy; 62import android.text.TextUtils; 63import android.util.Log; 64 65import java.util.ArrayList; 66import java.util.List; 67import java.util.concurrent.Executor; 68 69public class MediaController2Impl implements MediaController2Provider { 70 private static final String TAG = "MediaController2"; 71 private static final boolean DEBUG = true; // TODO(jaewan): Change 72 73 private final MediaController2 mInstance; 74 private final Context mContext; 75 private final Object mLock = new Object(); 76 77 private final MediaController2Stub mControllerStub; 78 private final SessionToken2 mToken; 79 private final ControllerCallback mCallback; 80 private final Executor mCallbackExecutor; 81 private final IBinder.DeathRecipient mDeathRecipient; 82 83 @GuardedBy("mLock") 84 private SessionServiceConnection mServiceConnection; 85 @GuardedBy("mLock") 86 private boolean mIsReleased; 87 @GuardedBy("mLock") 88 private List<MediaItem2> mPlaylist; 89 @GuardedBy("mLock") 90 private MediaMetadata2 mPlaylistMetadata; 91 @GuardedBy("mLock") 92 private @RepeatMode int mRepeatMode; 93 @GuardedBy("mLock") 94 private @ShuffleMode int mShuffleMode; 95 @GuardedBy("mLock") 96 private int mPlayerState; 97 @GuardedBy("mLock") 98 private long mPositionEventTimeMs; 99 @GuardedBy("mLock") 100 private long mPositionMs; 101 @GuardedBy("mLock") 102 private float mPlaybackSpeed; 103 @GuardedBy("mLock") 104 private long mBufferedPositionMs; 105 @GuardedBy("mLock") 106 private PlaybackInfo mPlaybackInfo; 107 @GuardedBy("mLock") 108 private PendingIntent mSessionActivity; 109 @GuardedBy("mLock") 110 private SessionCommandGroup2 mAllowedCommands; 111 112 // Assignment should be used with the lock hold, but should be used without a lock to prevent 113 // potential deadlock. 114 // Postfix -Binder is added to explicitly show that it's potentially remote process call. 115 // Technically -Interface is more correct, but it may misread that it's interface (vs class) 116 // so let's keep this postfix until we find better postfix. 117 @GuardedBy("mLock") 118 private volatile IMediaSession2 mSessionBinder; 119 120 // TODO(jaewan): Require session activeness changed listener, because controller can be 121 // available when the session's player is null. 122 public MediaController2Impl(Context context, MediaController2 instance, SessionToken2 token, 123 Executor executor, ControllerCallback callback) { 124 mInstance = instance; 125 if (context == null) { 126 throw new IllegalArgumentException("context shouldn't be null"); 127 } 128 if (token == null) { 129 throw new IllegalArgumentException("token shouldn't be null"); 130 } 131 if (callback == null) { 132 throw new IllegalArgumentException("callback shouldn't be null"); 133 } 134 if (executor == null) { 135 throw new IllegalArgumentException("executor shouldn't be null"); 136 } 137 mContext = context; 138 mControllerStub = new MediaController2Stub(this); 139 mToken = token; 140 mCallback = callback; 141 mCallbackExecutor = executor; 142 mDeathRecipient = () -> { 143 mInstance.close(); 144 }; 145 146 mSessionBinder = null; 147 } 148 149 @Override 150 public void initialize() { 151 // TODO(jaewan): More sanity checks. 152 if (mToken.getType() == SessionToken2.TYPE_SESSION) { 153 // Session 154 mServiceConnection = null; 155 connectToSession(SessionToken2Impl.from(mToken).getSessionBinder()); 156 } else { 157 // Session service 158 if (Process.myUid() == Process.SYSTEM_UID) { 159 // It's system server (MediaSessionService) that wants to monitor session. 160 // Don't bind if able.. 161 IMediaSession2 binder = SessionToken2Impl.from(mToken).getSessionBinder(); 162 if (binder != null) { 163 // Use binder in the session token instead of bind by its own. 164 // Otherwise server will holds the binding to the service *forever* and service 165 // will never stop. 166 mServiceConnection = null; 167 connectToSession(SessionToken2Impl.from(mToken).getSessionBinder()); 168 return; 169 } else if (DEBUG) { 170 // Should happen only when system server wants to dispatch media key events to 171 // a dead service. 172 Log.d(TAG, "System server binds to a session service. Should unbind" 173 + " immediately after the use."); 174 } 175 } 176 mServiceConnection = new SessionServiceConnection(); 177 connectToService(); 178 } 179 } 180 181 private void connectToService() { 182 // Service. Needs to get fresh binder whenever connection is needed. 183 SessionToken2Impl impl = SessionToken2Impl.from(mToken); 184 final Intent intent = new Intent(MediaSessionService2.SERVICE_INTERFACE); 185 intent.setClassName(mToken.getPackageName(), impl.getServiceName()); 186 187 // Use bindService() instead of startForegroundService() to start session service for three 188 // reasons. 189 // 1. Prevent session service owner's stopSelf() from destroying service. 190 // With the startForegroundService(), service's call of stopSelf() will trigger immediate 191 // onDestroy() calls on the main thread even when onConnect() is running in another 192 // thread. 193 // 2. Minimize APIs for developers to take care about. 194 // With bindService(), developers only need to take care about Service.onBind() 195 // but Service.onStartCommand() should be also taken care about with the 196 // startForegroundService(). 197 // 3. Future support for UI-less playback 198 // If a service wants to keep running, it should be either foreground service or 199 // bounded service. But there had been request for the feature for system apps 200 // and using bindService() will be better fit with it. 201 boolean result; 202 if (Process.myUid() == Process.SYSTEM_UID) { 203 // Use bindServiceAsUser() for binding from system service to avoid following warning. 204 // ContextImpl: Calling a method in the system process without a qualified user 205 result = mContext.bindServiceAsUser(intent, mServiceConnection, Context.BIND_AUTO_CREATE, 206 UserHandle.getUserHandleForUid(mToken.getUid())); 207 } else { 208 result = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); 209 } 210 if (!result) { 211 Log.w(TAG, "bind to " + mToken + " failed"); 212 } else if (DEBUG) { 213 Log.d(TAG, "bind to " + mToken + " success"); 214 } 215 } 216 217 private void connectToSession(IMediaSession2 sessionBinder) { 218 try { 219 sessionBinder.connect(mControllerStub, mContext.getPackageName()); 220 } catch (RemoteException e) { 221 Log.w(TAG, "Failed to call connection request. Framework will retry" 222 + " automatically"); 223 } 224 } 225 226 @Override 227 public void close_impl() { 228 if (DEBUG) { 229 Log.d(TAG, "release from " + mToken); 230 } 231 final IMediaSession2 binder; 232 synchronized (mLock) { 233 if (mIsReleased) { 234 // Prevent re-enterance from the ControllerCallback.onDisconnected() 235 return; 236 } 237 mIsReleased = true; 238 if (mServiceConnection != null) { 239 mContext.unbindService(mServiceConnection); 240 mServiceConnection = null; 241 } 242 binder = mSessionBinder; 243 mSessionBinder = null; 244 mControllerStub.destroy(); 245 } 246 if (binder != null) { 247 try { 248 binder.asBinder().unlinkToDeath(mDeathRecipient, 0); 249 binder.release(mControllerStub); 250 } catch (RemoteException e) { 251 // No-op. 252 } 253 } 254 mCallbackExecutor.execute(() -> { 255 mCallback.onDisconnected(mInstance); 256 }); 257 } 258 259 IMediaSession2 getSessionBinder() { 260 return mSessionBinder; 261 } 262 263 MediaController2Stub getControllerStub() { 264 return mControllerStub; 265 } 266 267 Executor getCallbackExecutor() { 268 return mCallbackExecutor; 269 } 270 271 Context getContext() { 272 return mContext; 273 } 274 275 MediaController2 getInstance() { 276 return mInstance; 277 } 278 279 // Returns session binder if the controller can send the command. 280 IMediaSession2 getSessionBinderIfAble(int commandCode) { 281 synchronized (mLock) { 282 if (!mAllowedCommands.hasCommand(commandCode)) { 283 // Cannot send because isn't allowed to. 284 Log.w(TAG, "Controller isn't allowed to call command, commandCode=" 285 + commandCode); 286 return null; 287 } 288 } 289 // TODO(jaewan): Should we do this with the lock hold? 290 final IMediaSession2 binder = mSessionBinder; 291 if (binder == null) { 292 // Cannot send because disconnected. 293 Log.w(TAG, "Session is disconnected"); 294 } 295 return binder; 296 } 297 298 // Returns session binder if the controller can send the command. 299 IMediaSession2 getSessionBinderIfAble(SessionCommand2 command) { 300 synchronized (mLock) { 301 if (!mAllowedCommands.hasCommand(command)) { 302 Log.w(TAG, "Controller isn't allowed to call command, command=" + command); 303 return null; 304 } 305 } 306 // TODO(jaewan): Should we do this with the lock hold? 307 final IMediaSession2 binder = mSessionBinder; 308 if (binder == null) { 309 // Cannot send because disconnected. 310 Log.w(TAG, "Session is disconnected"); 311 } 312 return binder; 313 } 314 315 @Override 316 public SessionToken2 getSessionToken_impl() { 317 return mToken; 318 } 319 320 @Override 321 public boolean isConnected_impl() { 322 final IMediaSession2 binder = mSessionBinder; 323 return binder != null; 324 } 325 326 @Override 327 public void play_impl() { 328 sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY); 329 } 330 331 @Override 332 public void pause_impl() { 333 sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE); 334 } 335 336 @Override 337 public void stop_impl() { 338 sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_STOP); 339 } 340 341 @Override 342 public void skipToPlaylistItem_impl(MediaItem2 item) { 343 if (item == null) { 344 throw new IllegalArgumentException("item shouldn't be null"); 345 } 346 final IMediaSession2 binder = mSessionBinder; 347 if (binder != null) { 348 try { 349 binder.skipToPlaylistItem(mControllerStub, item.toBundle()); 350 } catch (RemoteException e) { 351 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 352 } 353 } else { 354 Log.w(TAG, "Session isn't active", new IllegalStateException()); 355 } 356 } 357 358 @Override 359 public void skipToPreviousItem_impl() { 360 final IMediaSession2 binder = mSessionBinder; 361 if (binder != null) { 362 try { 363 binder.skipToPreviousItem(mControllerStub); 364 } catch (RemoteException e) { 365 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 366 } 367 } else { 368 Log.w(TAG, "Session isn't active", new IllegalStateException()); 369 } 370 } 371 372 @Override 373 public void skipToNextItem_impl() { 374 final IMediaSession2 binder = mSessionBinder; 375 if (binder != null) { 376 try { 377 binder.skipToNextItem(mControllerStub); 378 } catch (RemoteException e) { 379 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 380 } 381 } else { 382 Log.w(TAG, "Session isn't active", new IllegalStateException()); 383 } 384 } 385 386 private void sendTransportControlCommand(int commandCode) { 387 sendTransportControlCommand(commandCode, null); 388 } 389 390 private void sendTransportControlCommand(int commandCode, Bundle args) { 391 final IMediaSession2 binder = mSessionBinder; 392 if (binder != null) { 393 try { 394 binder.sendTransportControlCommand(mControllerStub, commandCode, args); 395 } catch (RemoteException e) { 396 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 397 } 398 } else { 399 Log.w(TAG, "Session isn't active", new IllegalStateException()); 400 } 401 } 402 403 @Override 404 public PendingIntent getSessionActivity_impl() { 405 return mSessionActivity; 406 } 407 408 @Override 409 public void setVolumeTo_impl(int value, int flags) { 410 // TODO(hdmoon): sanity check 411 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SET_VOLUME); 412 if (binder != null) { 413 try { 414 binder.setVolumeTo(mControllerStub, value, flags); 415 } catch (RemoteException e) { 416 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 417 } 418 } else { 419 Log.w(TAG, "Session isn't active", new IllegalStateException()); 420 } 421 } 422 423 @Override 424 public void adjustVolume_impl(int direction, int flags) { 425 // TODO(hdmoon): sanity check 426 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SET_VOLUME); 427 if (binder != null) { 428 try { 429 binder.adjustVolume(mControllerStub, direction, flags); 430 } catch (RemoteException e) { 431 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 432 } 433 } else { 434 Log.w(TAG, "Session isn't active", new IllegalStateException()); 435 } 436 } 437 438 @Override 439 public void prepareFromUri_impl(Uri uri, Bundle extras) { 440 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SESSION_PREPARE_FROM_URI); 441 if (uri == null) { 442 throw new IllegalArgumentException("uri shouldn't be null"); 443 } 444 if (binder != null) { 445 try { 446 binder.prepareFromUri(mControllerStub, uri, extras); 447 } catch (RemoteException e) { 448 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 449 } 450 } else { 451 // TODO(jaewan): Handle. 452 } 453 } 454 455 @Override 456 public void prepareFromSearch_impl(String query, Bundle extras) { 457 final IMediaSession2 binder = getSessionBinderIfAble( 458 COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH); 459 if (TextUtils.isEmpty(query)) { 460 throw new IllegalArgumentException("query shouldn't be empty"); 461 } 462 if (binder != null) { 463 try { 464 binder.prepareFromSearch(mControllerStub, query, extras); 465 } catch (RemoteException e) { 466 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 467 } 468 } else { 469 // TODO(jaewan): Handle. 470 } 471 } 472 473 @Override 474 public void prepareFromMediaId_impl(String mediaId, Bundle extras) { 475 final IMediaSession2 binder = getSessionBinderIfAble( 476 COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID); 477 if (mediaId == null) { 478 throw new IllegalArgumentException("mediaId shouldn't be null"); 479 } 480 if (binder != null) { 481 try { 482 binder.prepareFromMediaId(mControllerStub, mediaId, extras); 483 } catch (RemoteException e) { 484 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 485 } 486 } else { 487 // TODO(jaewan): Handle. 488 } 489 } 490 491 @Override 492 public void playFromUri_impl(Uri uri, Bundle extras) { 493 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SESSION_PLAY_FROM_URI); 494 if (uri == null) { 495 throw new IllegalArgumentException("uri shouldn't be null"); 496 } 497 if (binder != null) { 498 try { 499 binder.playFromUri(mControllerStub, uri, extras); 500 } catch (RemoteException e) { 501 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 502 } 503 } else { 504 // TODO(jaewan): Handle. 505 } 506 } 507 508 @Override 509 public void playFromSearch_impl(String query, Bundle extras) { 510 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH); 511 if (TextUtils.isEmpty(query)) { 512 throw new IllegalArgumentException("query shouldn't be empty"); 513 } 514 if (binder != null) { 515 try { 516 binder.playFromSearch(mControllerStub, query, extras); 517 } catch (RemoteException e) { 518 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 519 } 520 } else { 521 // TODO(jaewan): Handle. 522 } 523 } 524 525 @Override 526 public void playFromMediaId_impl(String mediaId, Bundle extras) { 527 final IMediaSession2 binder = getSessionBinderIfAble( 528 COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID); 529 if (mediaId == null) { 530 throw new IllegalArgumentException("mediaId shouldn't be null"); 531 } 532 if (binder != null) { 533 try { 534 binder.playFromMediaId(mControllerStub, mediaId, extras); 535 } catch (RemoteException e) { 536 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 537 } 538 } else { 539 // TODO(jaewan): Handle. 540 } 541 } 542 543 @Override 544 public void setRating_impl(String mediaId, Rating2 rating) { 545 if (mediaId == null) { 546 throw new IllegalArgumentException("mediaId shouldn't be null"); 547 } 548 if (rating == null) { 549 throw new IllegalArgumentException("rating shouldn't be null"); 550 } 551 552 final IMediaSession2 binder = mSessionBinder; 553 if (binder != null) { 554 try { 555 binder.setRating(mControllerStub, mediaId, rating.toBundle()); 556 } catch (RemoteException e) { 557 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 558 } 559 } else { 560 // TODO(jaewan): Handle. 561 } 562 } 563 564 @Override 565 public void sendCustomCommand_impl(SessionCommand2 command, Bundle args, ResultReceiver cb) { 566 if (command == null) { 567 throw new IllegalArgumentException("command shouldn't be null"); 568 } 569 final IMediaSession2 binder = getSessionBinderIfAble(command); 570 if (binder != null) { 571 try { 572 binder.sendCustomCommand(mControllerStub, command.toBundle(), args, cb); 573 } catch (RemoteException e) { 574 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 575 } 576 } else { 577 Log.w(TAG, "Session isn't active", new IllegalStateException()); 578 } 579 } 580 581 @Override 582 public List<MediaItem2> getPlaylist_impl() { 583 synchronized (mLock) { 584 return mPlaylist; 585 } 586 } 587 588 @Override 589 public void setPlaylist_impl(List<MediaItem2> list, MediaMetadata2 metadata) { 590 if (list == null) { 591 throw new IllegalArgumentException("list shouldn't be null"); 592 } 593 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_SET_LIST); 594 if (binder != null) { 595 List<Bundle> bundleList = new ArrayList<>(); 596 for (int i = 0; i < list.size(); i++) { 597 bundleList.add(list.get(i).toBundle()); 598 } 599 Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle(); 600 try { 601 binder.setPlaylist(mControllerStub, bundleList, metadataBundle); 602 } catch (RemoteException e) { 603 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 604 } 605 } else { 606 Log.w(TAG, "Session isn't active", new IllegalStateException()); 607 } 608 } 609 610 @Override 611 public MediaMetadata2 getPlaylistMetadata_impl() { 612 synchronized (mLock) { 613 return mPlaylistMetadata; 614 } 615 } 616 617 @Override 618 public void updatePlaylistMetadata_impl(MediaMetadata2 metadata) { 619 final IMediaSession2 binder = getSessionBinderIfAble( 620 COMMAND_CODE_PLAYLIST_SET_LIST_METADATA); 621 if (binder != null) { 622 Bundle metadataBundle = (metadata == null) ? null : metadata.toBundle(); 623 try { 624 binder.updatePlaylistMetadata(mControllerStub, metadataBundle); 625 } catch (RemoteException e) { 626 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 627 } 628 } else { 629 Log.w(TAG, "Session isn't active", new IllegalStateException()); 630 } 631 } 632 633 @Override 634 public void prepare_impl() { 635 sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE); 636 } 637 638 @Override 639 public void fastForward_impl() { 640 // TODO(jaewan): Implement this. Note that fast forward isn't a transport command anymore 641 //sendTransportControlCommand(MediaSession2.COMMAND_CODE_SESSION_FAST_FORWARD); 642 } 643 644 @Override 645 public void rewind_impl() { 646 // TODO(jaewan): Implement this. Note that rewind isn't a transport command anymore 647 //sendTransportControlCommand(MediaSession2.COMMAND_CODE_SESSION_REWIND); 648 } 649 650 @Override 651 public void seekTo_impl(long pos) { 652 if (pos < 0) { 653 throw new IllegalArgumentException("position shouldn't be negative"); 654 } 655 Bundle args = new Bundle(); 656 args.putLong(MediaSession2Stub.ARGUMENT_KEY_POSITION, pos); 657 sendTransportControlCommand(SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO, args); 658 } 659 660 @Override 661 public void addPlaylistItem_impl(int index, MediaItem2 item) { 662 if (index < 0) { 663 throw new IllegalArgumentException("index shouldn't be negative"); 664 } 665 if (item == null) { 666 throw new IllegalArgumentException("item shouldn't be null"); 667 } 668 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_ADD_ITEM); 669 if (binder != null) { 670 try { 671 binder.addPlaylistItem(mControllerStub, index, item.toBundle()); 672 } catch (RemoteException e) { 673 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 674 } 675 } else { 676 Log.w(TAG, "Session isn't active", new IllegalStateException()); 677 } 678 } 679 680 @Override 681 public void removePlaylistItem_impl(MediaItem2 item) { 682 if (item == null) { 683 throw new IllegalArgumentException("item shouldn't be null"); 684 } 685 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_REMOVE_ITEM); 686 if (binder != null) { 687 try { 688 binder.removePlaylistItem(mControllerStub, item.toBundle()); 689 } catch (RemoteException e) { 690 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 691 } 692 } else { 693 Log.w(TAG, "Session isn't active", new IllegalStateException()); 694 } 695 } 696 697 @Override 698 public void replacePlaylistItem_impl(int index, MediaItem2 item) { 699 if (index < 0) { 700 throw new IllegalArgumentException("index shouldn't be negative"); 701 } 702 if (item == null) { 703 throw new IllegalArgumentException("item shouldn't be null"); 704 } 705 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_REPLACE_ITEM); 706 if (binder != null) { 707 try { 708 binder.replacePlaylistItem(mControllerStub, index, item.toBundle()); 709 } catch (RemoteException e) { 710 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 711 } 712 } else { 713 Log.w(TAG, "Session isn't active", new IllegalStateException()); 714 } 715 } 716 717 @Override 718 public int getShuffleMode_impl() { 719 return mShuffleMode; 720 } 721 722 @Override 723 public void setShuffleMode_impl(int shuffleMode) { 724 final IMediaSession2 binder = getSessionBinderIfAble( 725 COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE); 726 if (binder != null) { 727 try { 728 binder.setShuffleMode(mControllerStub, shuffleMode); 729 } catch (RemoteException e) { 730 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 731 } 732 } else { 733 Log.w(TAG, "Session isn't active", new IllegalStateException()); 734 } 735 } 736 737 @Override 738 public int getRepeatMode_impl() { 739 return mRepeatMode; 740 } 741 742 @Override 743 public void setRepeatMode_impl(int repeatMode) { 744 final IMediaSession2 binder = getSessionBinderIfAble(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE); 745 if (binder != null) { 746 try { 747 binder.setRepeatMode(mControllerStub, repeatMode); 748 } catch (RemoteException e) { 749 Log.w(TAG, "Cannot connect to the service or the session is gone", e); 750 } 751 } else { 752 Log.w(TAG, "Session isn't active", new IllegalStateException()); 753 } 754 } 755 756 @Override 757 public PlaybackInfo getPlaybackInfo_impl() { 758 synchronized (mLock) { 759 return mPlaybackInfo; 760 } 761 } 762 763 @Override 764 public int getPlayerState_impl() { 765 synchronized (mLock) { 766 return mPlayerState; 767 } 768 } 769 770 @Override 771 public long getCurrentPosition_impl() { 772 synchronized (mLock) { 773 long timeDiff = System.currentTimeMillis() - mPositionEventTimeMs; 774 long expectedPosition = mPositionMs + (long) (mPlaybackSpeed * timeDiff); 775 return Math.max(0, expectedPosition); 776 } 777 } 778 779 @Override 780 public float getPlaybackSpeed_impl() { 781 synchronized (mLock) { 782 return mPlaybackSpeed; 783 } 784 } 785 786 @Override 787 public long getBufferedPosition_impl() { 788 synchronized (mLock) { 789 return mBufferedPositionMs; 790 } 791 } 792 793 @Override 794 public MediaItem2 getCurrentMediaItem_impl() { 795 // TODO(jaewan): Implement 796 return null; 797 } 798 799 void pushPlayerStateChanges(final int state) { 800 synchronized (mLock) { 801 mPlayerState = state; 802 } 803 mCallbackExecutor.execute(() -> { 804 if (!mInstance.isConnected()) { 805 return; 806 } 807 mCallback.onPlayerStateChanged(mInstance, state); 808 }); 809 } 810 811 // TODO(jaewan): Rename to seek completed 812 void pushPositionChanges(final long eventTimeMs, final long positionMs) { 813 synchronized (mLock) { 814 mPositionEventTimeMs = eventTimeMs; 815 mPositionMs = positionMs; 816 } 817 mCallbackExecutor.execute(() -> { 818 if (!mInstance.isConnected()) { 819 return; 820 } 821 mCallback.onSeekCompleted(mInstance, positionMs); 822 }); 823 } 824 825 void pushPlaybackSpeedChanges(final float speed) { 826 synchronized (mLock) { 827 mPlaybackSpeed = speed; 828 } 829 mCallbackExecutor.execute(() -> { 830 if (!mInstance.isConnected()) { 831 return; 832 } 833 mCallback.onPlaybackSpeedChanged(mInstance, speed); 834 }); 835 } 836 837 void pushBufferedPositionChanges(final long bufferedPositionMs) { 838 synchronized (mLock) { 839 mBufferedPositionMs = bufferedPositionMs; 840 } 841 mCallbackExecutor.execute(() -> { 842 if (!mInstance.isConnected()) { 843 return; 844 } 845 // TODO(jaewan): Fix this -- it's now buffered state 846 //mCallback.onBufferedPositionChanged(mInstance, bufferedPositionMs); 847 }); 848 } 849 850 void pushPlaybackInfoChanges(final PlaybackInfo info) { 851 synchronized (mLock) { 852 mPlaybackInfo = info; 853 } 854 mCallbackExecutor.execute(() -> { 855 if (!mInstance.isConnected()) { 856 return; 857 } 858 mCallback.onPlaybackInfoChanged(mInstance, info); 859 }); 860 } 861 862 void pushPlaylistChanges(final List<MediaItem2> playlist, final MediaMetadata2 metadata) { 863 synchronized (mLock) { 864 mPlaylist = playlist; 865 mPlaylistMetadata = metadata; 866 } 867 mCallbackExecutor.execute(() -> { 868 if (!mInstance.isConnected()) { 869 return; 870 } 871 mCallback.onPlaylistChanged(mInstance, playlist, metadata); 872 }); 873 } 874 875 void pushPlaylistMetadataChanges(MediaMetadata2 metadata) { 876 synchronized (mLock) { 877 mPlaylistMetadata = metadata; 878 } 879 mCallbackExecutor.execute(() -> { 880 if (!mInstance.isConnected()) { 881 return; 882 } 883 mCallback.onPlaylistMetadataChanged(mInstance, metadata); 884 }); 885 } 886 887 void pushShuffleModeChanges(int shuffleMode) { 888 synchronized (mLock) { 889 mShuffleMode = shuffleMode; 890 } 891 mCallbackExecutor.execute(() -> { 892 if (!mInstance.isConnected()) { 893 return; 894 } 895 mCallback.onShuffleModeChanged(mInstance, shuffleMode); 896 }); 897 } 898 899 void pushRepeatModeChanges(int repeatMode) { 900 synchronized (mLock) { 901 mRepeatMode = repeatMode; 902 } 903 mCallbackExecutor.execute(() -> { 904 if (!mInstance.isConnected()) { 905 return; 906 } 907 mCallback.onRepeatModeChanged(mInstance, repeatMode); 908 }); 909 } 910 911 void pushError(int errorCode, Bundle extras) { 912 mCallbackExecutor.execute(() -> { 913 if (!mInstance.isConnected()) { 914 return; 915 } 916 mCallback.onError(mInstance, errorCode, extras); 917 }); 918 } 919 920 // Should be used without a lock to prevent potential deadlock. 921 void onConnectedNotLocked(IMediaSession2 sessionBinder, 922 final SessionCommandGroup2 allowedCommands, 923 final int playerState, 924 final long positionEventTimeMs, 925 final long positionMs, 926 final float playbackSpeed, 927 final long bufferedPositionMs, 928 final PlaybackInfo info, 929 final int repeatMode, 930 final int shuffleMode, 931 final List<MediaItem2> playlist, 932 final PendingIntent sessionActivity) { 933 if (DEBUG) { 934 Log.d(TAG, "onConnectedNotLocked sessionBinder=" + sessionBinder 935 + ", allowedCommands=" + allowedCommands); 936 } 937 boolean close = false; 938 try { 939 if (sessionBinder == null || allowedCommands == null) { 940 // Connection rejected. 941 close = true; 942 return; 943 } 944 synchronized (mLock) { 945 if (mIsReleased) { 946 return; 947 } 948 if (mSessionBinder != null) { 949 Log.e(TAG, "Cannot be notified about the connection result many times." 950 + " Probably a bug or malicious app."); 951 close = true; 952 return; 953 } 954 mAllowedCommands = allowedCommands; 955 mPlayerState = playerState; 956 mPositionEventTimeMs = positionEventTimeMs; 957 mPositionMs = positionMs; 958 mPlaybackSpeed = playbackSpeed; 959 mBufferedPositionMs = bufferedPositionMs; 960 mPlaybackInfo = info; 961 mRepeatMode = repeatMode; 962 mShuffleMode = shuffleMode; 963 mPlaylist = playlist; 964 mSessionActivity = sessionActivity; 965 mSessionBinder = sessionBinder; 966 try { 967 // Implementation for the local binder is no-op, 968 // so can be used without worrying about deadlock. 969 mSessionBinder.asBinder().linkToDeath(mDeathRecipient, 0); 970 } catch (RemoteException e) { 971 if (DEBUG) { 972 Log.d(TAG, "Session died too early.", e); 973 } 974 close = true; 975 return; 976 } 977 } 978 // TODO(jaewan): Keep commands to prevents illegal API calls. 979 mCallbackExecutor.execute(() -> { 980 // Note: We may trigger ControllerCallbacks with the initial values 981 // But it's hard to define the order of the controller callbacks 982 // Only notify about the 983 mCallback.onConnected(mInstance, allowedCommands); 984 }); 985 } finally { 986 if (close) { 987 // Trick to call release() without holding the lock, to prevent potential deadlock 988 // with the developer's custom lock within the ControllerCallback.onDisconnected(). 989 mInstance.close(); 990 } 991 } 992 } 993 994 void onCustomCommand(final SessionCommand2 command, final Bundle args, 995 final ResultReceiver receiver) { 996 if (DEBUG) { 997 Log.d(TAG, "onCustomCommand cmd=" + command); 998 } 999 mCallbackExecutor.execute(() -> { 1000 // TODO(jaewan): Double check if the controller exists. 1001 mCallback.onCustomCommand(mInstance, command, args, receiver); 1002 }); 1003 } 1004 1005 void onAllowedCommandsChanged(final SessionCommandGroup2 commands) { 1006 mCallbackExecutor.execute(() -> { 1007 mCallback.onAllowedCommandsChanged(mInstance, commands); 1008 }); 1009 } 1010 1011 void onCustomLayoutChanged(final List<CommandButton> layout) { 1012 mCallbackExecutor.execute(() -> { 1013 mCallback.onCustomLayoutChanged(mInstance, layout); 1014 }); 1015 } 1016 1017 // This will be called on the main thread. 1018 private class SessionServiceConnection implements ServiceConnection { 1019 @Override 1020 public void onServiceConnected(ComponentName name, IBinder service) { 1021 // Note that it's always main-thread. 1022 if (DEBUG) { 1023 Log.d(TAG, "onServiceConnected " + name + " " + this); 1024 } 1025 // Sanity check 1026 if (!mToken.getPackageName().equals(name.getPackageName())) { 1027 Log.wtf(TAG, name + " was connected, but expected pkg=" 1028 + mToken.getPackageName() + " with id=" + mToken.getId()); 1029 return; 1030 } 1031 final IMediaSession2 sessionBinder = IMediaSession2.Stub.asInterface(service); 1032 connectToSession(sessionBinder); 1033 } 1034 1035 @Override 1036 public void onServiceDisconnected(ComponentName name) { 1037 // Temporal lose of the binding because of the service crash. System will automatically 1038 // rebind, so just no-op. 1039 // TODO(jaewan): Really? Either disconnect cleanly or 1040 if (DEBUG) { 1041 Log.w(TAG, "Session service " + name + " is disconnected."); 1042 } 1043 } 1044 1045 @Override 1046 public void onBindingDied(ComponentName name) { 1047 // Permanent lose of the binding because of the service package update or removed. 1048 // This SessionServiceRecord will be removed accordingly, but forget session binder here 1049 // for sure. 1050 mInstance.close(); 1051 } 1052 } 1053 1054 public static final class PlaybackInfoImpl implements PlaybackInfoProvider { 1055 1056 private static final String KEY_PLAYBACK_TYPE = 1057 "android.media.playbackinfo_impl.playback_type"; 1058 private static final String KEY_CONTROL_TYPE = 1059 "android.media.playbackinfo_impl.control_type"; 1060 private static final String KEY_MAX_VOLUME = 1061 "android.media.playbackinfo_impl.max_volume"; 1062 private static final String KEY_CURRENT_VOLUME = 1063 "android.media.playbackinfo_impl.current_volume"; 1064 private static final String KEY_AUDIO_ATTRIBUTES = 1065 "android.media.playbackinfo_impl.audio_attrs"; 1066 1067 private final PlaybackInfo mInstance; 1068 1069 private final int mPlaybackType; 1070 private final int mControlType; 1071 private final int mMaxVolume; 1072 private final int mCurrentVolume; 1073 private final AudioAttributes mAudioAttrs; 1074 1075 private PlaybackInfoImpl(int playbackType, AudioAttributes attrs, int controlType, 1076 int max, int current) { 1077 mPlaybackType = playbackType; 1078 mAudioAttrs = attrs; 1079 mControlType = controlType; 1080 mMaxVolume = max; 1081 mCurrentVolume = current; 1082 mInstance = new PlaybackInfo(this); 1083 } 1084 1085 @Override 1086 public int getPlaybackType_impl() { 1087 return mPlaybackType; 1088 } 1089 1090 @Override 1091 public AudioAttributes getAudioAttributes_impl() { 1092 return mAudioAttrs; 1093 } 1094 1095 @Override 1096 public int getControlType_impl() { 1097 return mControlType; 1098 } 1099 1100 @Override 1101 public int getMaxVolume_impl() { 1102 return mMaxVolume; 1103 } 1104 1105 @Override 1106 public int getCurrentVolume_impl() { 1107 return mCurrentVolume; 1108 } 1109 1110 PlaybackInfo getInstance() { 1111 return mInstance; 1112 } 1113 1114 Bundle toBundle() { 1115 Bundle bundle = new Bundle(); 1116 bundle.putInt(KEY_PLAYBACK_TYPE, mPlaybackType); 1117 bundle.putInt(KEY_CONTROL_TYPE, mControlType); 1118 bundle.putInt(KEY_MAX_VOLUME, mMaxVolume); 1119 bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume); 1120 bundle.putParcelable(KEY_AUDIO_ATTRIBUTES, mAudioAttrs); 1121 return bundle; 1122 } 1123 1124 static PlaybackInfo createPlaybackInfo(int playbackType, AudioAttributes attrs, 1125 int controlType, int max, int current) { 1126 return new PlaybackInfoImpl(playbackType, attrs, controlType, max, current) 1127 .getInstance(); 1128 } 1129 1130 static PlaybackInfo fromBundle(Bundle bundle) { 1131 if (bundle == null) { 1132 return null; 1133 } 1134 final int volumeType = bundle.getInt(KEY_PLAYBACK_TYPE); 1135 final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE); 1136 final int maxVolume = bundle.getInt(KEY_MAX_VOLUME); 1137 final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME); 1138 final AudioAttributes attrs = bundle.getParcelable(KEY_AUDIO_ATTRIBUTES); 1139 1140 return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume, currentVolume); 1141 } 1142 } 1143} 1144