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 android.media; 18 19 import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS; 20 import static android.media.MediaConstants.KEY_CONNECTION_HINTS; 21 import static android.media.MediaConstants.KEY_PACKAGE_NAME; 22 import static android.media.MediaConstants.KEY_PID; 23 import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE; 24 import static android.media.MediaConstants.KEY_SESSION2LINK; 25 import static android.media.MediaConstants.KEY_TOKEN_EXTRAS; 26 import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR; 27 import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED; 28 import static android.media.Session2Token.TYPE_SESSION; 29 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.app.PendingIntent; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.media.session.MediaSessionManager; 36 import android.media.session.MediaSessionManager.RemoteUserInfo; 37 import android.os.BadParcelableException; 38 import android.os.Bundle; 39 import android.os.Handler; 40 import android.os.Parcel; 41 import android.os.Process; 42 import android.os.ResultReceiver; 43 import android.util.ArrayMap; 44 import android.util.ArraySet; 45 import android.util.Log; 46 47 import com.android.modules.utils.build.SdkLevel; 48 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.Objects; 54 import java.util.concurrent.Executor; 55 56 /** 57 * This API is not generally intended for third party application developers. 58 * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 59 * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session 60 * Library</a> for consistent behavior across all devices. 61 * <p> 62 * Allows a media app to expose its transport controls and playback information in a process to 63 * other processes including the Android framework and other apps. 64 */ 65 public class MediaSession2 implements AutoCloseable { 66 static final String TAG = "MediaSession2"; 67 static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 68 69 // Note: This checks the uniqueness of a session ID only in a single process. 70 // When the framework becomes able to check the uniqueness, this logic should be removed. 71 //@GuardedBy("MediaSession.class") 72 private static final List<String> SESSION_ID_LIST = new ArrayList<>(); 73 74 @SuppressWarnings("WeakerAccess") /* synthetic access */ 75 final Object mLock = new Object(); 76 //@GuardedBy("mLock") 77 @SuppressWarnings("WeakerAccess") /* synthetic access */ 78 final Map<Controller2Link, ControllerInfo> mConnectedControllers = new HashMap<>(); 79 80 @SuppressWarnings("WeakerAccess") /* synthetic access */ 81 final Context mContext; 82 @SuppressWarnings("WeakerAccess") /* synthetic access */ 83 final Executor mCallbackExecutor; 84 @SuppressWarnings("WeakerAccess") /* synthetic access */ 85 final SessionCallback mCallback; 86 @SuppressWarnings("WeakerAccess") /* synthetic access */ 87 final Session2Link mSessionStub; 88 89 private final String mSessionId; 90 private final PendingIntent mSessionActivity; 91 private final Session2Token mSessionToken; 92 private final MediaSessionManager mMediaSessionManager; 93 private final MediaCommunicationManager mCommunicationManager; 94 private final Handler mResultHandler; 95 96 //@GuardedBy("mLock") 97 private boolean mClosed; 98 //@GuardedBy("mLock") 99 private boolean mPlaybackActive; 100 //@GuardedBy("mLock") 101 private ForegroundServiceEventCallback mForegroundServiceEventCallback; 102 MediaSession2(@onNull Context context, @NonNull String id, PendingIntent sessionActivity, @NonNull Executor callbackExecutor, @NonNull SessionCallback callback, @NonNull Bundle tokenExtras)103 MediaSession2(@NonNull Context context, @NonNull String id, PendingIntent sessionActivity, 104 @NonNull Executor callbackExecutor, @NonNull SessionCallback callback, 105 @NonNull Bundle tokenExtras) { 106 synchronized (MediaSession2.class) { 107 if (SESSION_ID_LIST.contains(id)) { 108 throw new IllegalStateException("Session ID must be unique. ID=" + id); 109 } 110 SESSION_ID_LIST.add(id); 111 } 112 113 mContext = context; 114 mSessionId = id; 115 mSessionActivity = sessionActivity; 116 mCallbackExecutor = callbackExecutor; 117 mCallback = callback; 118 mSessionStub = new Session2Link(this); 119 mSessionToken = new Session2Token(Process.myUid(), TYPE_SESSION, context.getPackageName(), 120 mSessionStub, tokenExtras); 121 if (SdkLevel.isAtLeastS()) { 122 mCommunicationManager = mContext.getSystemService(MediaCommunicationManager.class); 123 mMediaSessionManager = null; 124 } else { 125 mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class); 126 mCommunicationManager = null; 127 } 128 // NOTE: mResultHandler uses main looper, so this MUST NOT be blocked. 129 mResultHandler = new Handler(context.getMainLooper()); 130 mClosed = false; 131 } 132 133 @Override close()134 public void close() { 135 try { 136 List<ControllerInfo> controllerInfos; 137 ForegroundServiceEventCallback callback; 138 synchronized (mLock) { 139 if (mClosed) { 140 return; 141 } 142 mClosed = true; 143 controllerInfos = getConnectedControllers(); 144 mConnectedControllers.clear(); 145 callback = mForegroundServiceEventCallback; 146 mForegroundServiceEventCallback = null; 147 } 148 synchronized (MediaSession2.class) { 149 SESSION_ID_LIST.remove(mSessionId); 150 } 151 if (callback != null) { 152 callback.onSessionClosed(this); 153 } 154 for (ControllerInfo info : controllerInfos) { 155 info.notifyDisconnected(); 156 } 157 } catch (Exception e) { 158 // Should not be here. 159 } 160 } 161 162 /** 163 * Returns the session ID 164 */ 165 @NonNull getId()166 public String getId() { 167 return mSessionId; 168 } 169 170 /** 171 * Returns the {@link Session2Token} for creating {@link MediaController2}. 172 */ 173 @NonNull getToken()174 public Session2Token getToken() { 175 return mSessionToken; 176 } 177 178 /** 179 * Broadcasts a session command to all the connected controllers 180 * <p> 181 * @param command the session command 182 * @param args optional arguments 183 */ broadcastSessionCommand(@onNull Session2Command command, @Nullable Bundle args)184 public void broadcastSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) { 185 if (command == null) { 186 throw new IllegalArgumentException("command shouldn't be null"); 187 } 188 List<ControllerInfo> controllerInfos = getConnectedControllers(); 189 for (ControllerInfo controller : controllerInfos) { 190 controller.sendSessionCommand(command, args, null); 191 } 192 } 193 194 /** 195 * Sends a session command to a specific controller 196 * <p> 197 * @param controller the controller to get the session command 198 * @param command the session command 199 * @param args optional arguments 200 * @return a token which will be sent together in {@link SessionCallback#onCommandResult} 201 * when its result is received. 202 */ 203 @NonNull sendSessionCommand(@onNull ControllerInfo controller, @NonNull Session2Command command, @Nullable Bundle args)204 public Object sendSessionCommand(@NonNull ControllerInfo controller, 205 @NonNull Session2Command command, @Nullable Bundle args) { 206 if (controller == null) { 207 throw new IllegalArgumentException("controller shouldn't be null"); 208 } 209 if (command == null) { 210 throw new IllegalArgumentException("command shouldn't be null"); 211 } 212 ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) { 213 protected void onReceiveResult(int resultCode, Bundle resultData) { 214 controller.receiveCommandResult(this); 215 mCallbackExecutor.execute(() -> { 216 mCallback.onCommandResult(MediaSession2.this, controller, this, 217 command, new Session2Command.Result(resultCode, resultData)); 218 }); 219 } 220 }; 221 controller.sendSessionCommand(command, args, resultReceiver); 222 return resultReceiver; 223 } 224 225 /** 226 * Cancels the session command previously sent. 227 * 228 * @param controller the controller to get the session command 229 * @param token the token which is returned from {@link #sendSessionCommand}. 230 */ cancelSessionCommand(@onNull ControllerInfo controller, @NonNull Object token)231 public void cancelSessionCommand(@NonNull ControllerInfo controller, @NonNull Object token) { 232 if (controller == null) { 233 throw new IllegalArgumentException("controller shouldn't be null"); 234 } 235 if (token == null) { 236 throw new IllegalArgumentException("token shouldn't be null"); 237 } 238 controller.cancelSessionCommand(token); 239 } 240 241 /** 242 * Sets whether the playback is active (i.e. playing something) 243 * 244 * @param playbackActive {@code true} if the playback active, {@code false} otherwise. 245 **/ setPlaybackActive(boolean playbackActive)246 public void setPlaybackActive(boolean playbackActive) { 247 final ForegroundServiceEventCallback serviceCallback; 248 synchronized (mLock) { 249 if (mPlaybackActive == playbackActive) { 250 return; 251 } 252 mPlaybackActive = playbackActive; 253 serviceCallback = mForegroundServiceEventCallback; 254 } 255 if (serviceCallback != null) { 256 serviceCallback.onPlaybackActiveChanged(this, playbackActive); 257 } 258 List<ControllerInfo> controllerInfos = getConnectedControllers(); 259 for (ControllerInfo controller : controllerInfos) { 260 controller.notifyPlaybackActiveChanged(playbackActive); 261 } 262 } 263 264 /** 265 * Returns whehther the playback is active (i.e. playing something) 266 * 267 * @return {@code true} if the playback active, {@code false} otherwise. 268 */ isPlaybackActive()269 public boolean isPlaybackActive() { 270 synchronized (mLock) { 271 return mPlaybackActive; 272 } 273 } 274 275 /** 276 * Gets the list of the connected controllers 277 * 278 * @return list of the connected controllers. 279 */ 280 @NonNull getConnectedControllers()281 public List<ControllerInfo> getConnectedControllers() { 282 List<ControllerInfo> controllers = new ArrayList<>(); 283 synchronized (mLock) { 284 controllers.addAll(mConnectedControllers.values()); 285 } 286 return controllers; 287 } 288 289 /** 290 * Returns whether the given bundle includes non-framework Parcelables. 291 */ hasCustomParcelable(@ullable Bundle bundle)292 static boolean hasCustomParcelable(@Nullable Bundle bundle) { 293 if (bundle == null) { 294 return false; 295 } 296 297 // Try writing the bundle to parcel, and read it with framework classloader. 298 Parcel parcel = null; 299 try { 300 parcel = Parcel.obtain(); 301 parcel.writeBundle(bundle); 302 parcel.setDataPosition(0); 303 Bundle out = parcel.readBundle(null); 304 305 // Calling Bundle#size() will trigger Bundle#unparcel(). 306 out.size(); 307 } catch (BadParcelableException e) { 308 Log.d(TAG, "Custom parcelable in bundle.", e); 309 return true; 310 } finally { 311 if (parcel != null) { 312 parcel.recycle(); 313 } 314 } 315 return false; 316 } 317 isClosed()318 boolean isClosed() { 319 synchronized (mLock) { 320 return mClosed; 321 } 322 } 323 getCallback()324 SessionCallback getCallback() { 325 return mCallback; 326 } 327 isTrustedForMediaControl(RemoteUserInfo remoteUserInfo)328 boolean isTrustedForMediaControl(RemoteUserInfo remoteUserInfo) { 329 if (SdkLevel.isAtLeastS()) { 330 return mCommunicationManager.isTrustedForMediaControl(remoteUserInfo); 331 } else { 332 return mMediaSessionManager.isTrustedForMediaControl(remoteUserInfo); 333 } 334 } 335 setForegroundServiceEventCallback(ForegroundServiceEventCallback callback)336 void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) { 337 synchronized (mLock) { 338 if (mForegroundServiceEventCallback == callback) { 339 return; 340 } 341 if (mForegroundServiceEventCallback != null && callback != null) { 342 throw new IllegalStateException("A session cannot be added to multiple services"); 343 } 344 mForegroundServiceEventCallback = callback; 345 } 346 } 347 348 // Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq, Bundle connectionRequest)349 void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq, 350 Bundle connectionRequest) { 351 if (callingPid == 0) { 352 // The pid here is from Binder.getCallingPid(), which can be 0 for an oneway call from 353 // the remote process. If it's the case, use PID from the connectionRequest. 354 callingPid = connectionRequest.getInt(KEY_PID); 355 } 356 String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME); 357 358 RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid); 359 360 Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS); 361 if (connectionHints == null) { 362 Log.w(TAG, "connectionHints shouldn't be null."); 363 connectionHints = Bundle.EMPTY; 364 } else if (hasCustomParcelable(connectionHints)) { 365 Log.w(TAG, "connectionHints contain custom parcelable. Ignoring."); 366 connectionHints = Bundle.EMPTY; 367 } 368 369 final ControllerInfo controllerInfo = new ControllerInfo( 370 remoteUserInfo, 371 isTrustedForMediaControl(remoteUserInfo), 372 controller, 373 connectionHints); 374 mCallbackExecutor.execute(() -> { 375 boolean connected = false; 376 try { 377 if (isClosed()) { 378 return; 379 } 380 controllerInfo.mAllowedCommands = 381 mCallback.onConnect(MediaSession2.this, controllerInfo); 382 // Don't reject connection for the request from trusted app. 383 // Otherwise server will fail to retrieve session's information to dispatch 384 // media keys to. 385 if (controllerInfo.mAllowedCommands == null && !controllerInfo.isTrusted()) { 386 return; 387 } 388 if (controllerInfo.mAllowedCommands == null) { 389 // For trusted apps, send non-null allowed commands to keep 390 // connection. 391 controllerInfo.mAllowedCommands = 392 new Session2CommandGroup.Builder().build(); 393 } 394 if (DEBUG) { 395 Log.d(TAG, "Accepting connection: " + controllerInfo); 396 } 397 // If connection is accepted, notify the current state to the controller. 398 // It's needed because we cannot call synchronous calls between 399 // session/controller. 400 Bundle connectionResult = new Bundle(); 401 connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub); 402 connectionResult.putParcelable(KEY_ALLOWED_COMMANDS, 403 controllerInfo.mAllowedCommands); 404 connectionResult.putBoolean(KEY_PLAYBACK_ACTIVE, isPlaybackActive()); 405 connectionResult.putBundle(KEY_TOKEN_EXTRAS, mSessionToken.getExtras()); 406 407 // Double check if session is still there, because close() can be called in 408 // another thread. 409 if (isClosed()) { 410 return; 411 } 412 controllerInfo.notifyConnected(connectionResult); 413 synchronized (mLock) { 414 if (mConnectedControllers.containsKey(controller)) { 415 Log.w(TAG, "Controller " + controllerInfo + " has sent connection" 416 + " request multiple times"); 417 } 418 mConnectedControllers.put(controller, controllerInfo); 419 } 420 mCallback.onPostConnect(MediaSession2.this, controllerInfo); 421 connected = true; 422 } finally { 423 if (!connected || isClosed()) { 424 if (DEBUG) { 425 Log.d(TAG, "Rejecting connection or notifying that session is closed" 426 + ", controllerInfo=" + controllerInfo); 427 } 428 synchronized (mLock) { 429 mConnectedControllers.remove(controller); 430 } 431 controllerInfo.notifyDisconnected(); 432 } 433 } 434 }); 435 } 436 437 // Called by Session2Link.onDisconnect onDisconnect(@onNull final Controller2Link controller, int seq)438 void onDisconnect(@NonNull final Controller2Link controller, int seq) { 439 final ControllerInfo controllerInfo; 440 synchronized (mLock) { 441 controllerInfo = mConnectedControllers.remove(controller); 442 } 443 if (controllerInfo == null) { 444 return; 445 } 446 mCallbackExecutor.execute(() -> { 447 mCallback.onDisconnected(MediaSession2.this, controllerInfo); 448 }); 449 } 450 451 // Called by Session2Link.onSessionCommand onSessionCommand(@onNull final Controller2Link controller, final int seq, final Session2Command command, final Bundle args, @Nullable ResultReceiver resultReceiver)452 void onSessionCommand(@NonNull final Controller2Link controller, final int seq, 453 final Session2Command command, final Bundle args, 454 @Nullable ResultReceiver resultReceiver) { 455 if (controller == null) { 456 return; 457 } 458 final ControllerInfo controllerInfo; 459 synchronized (mLock) { 460 controllerInfo = mConnectedControllers.get(controller); 461 } 462 if (controllerInfo == null) { 463 return; 464 } 465 466 // TODO: check allowed commands. 467 synchronized (mLock) { 468 controllerInfo.addRequestedCommandSeqNumber(seq); 469 } 470 mCallbackExecutor.execute(() -> { 471 if (!controllerInfo.removeRequestedCommandSeqNumber(seq)) { 472 if (resultReceiver != null) { 473 resultReceiver.send(RESULT_INFO_SKIPPED, null); 474 } 475 return; 476 } 477 Session2Command.Result result = mCallback.onSessionCommand( 478 MediaSession2.this, controllerInfo, command, args); 479 if (resultReceiver != null) { 480 if (result == null) { 481 resultReceiver.send(RESULT_INFO_SKIPPED, null); 482 } else { 483 resultReceiver.send(result.getResultCode(), result.getResultData()); 484 } 485 } 486 }); 487 } 488 489 // Called by Session2Link.onCancelCommand onCancelCommand(@onNull final Controller2Link controller, final int seq)490 void onCancelCommand(@NonNull final Controller2Link controller, final int seq) { 491 final ControllerInfo controllerInfo; 492 synchronized (mLock) { 493 controllerInfo = mConnectedControllers.get(controller); 494 } 495 if (controllerInfo == null) { 496 return; 497 } 498 controllerInfo.removeRequestedCommandSeqNumber(seq); 499 } 500 501 /** 502 * This API is not generally intended for third party application developers. 503 * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 504 * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session 505 * Library</a> for consistent behavior across all devices. 506 * <p> 507 * Builder for {@link MediaSession2}. 508 * <p> 509 * Any incoming event from the {@link MediaController2} will be handled on the callback 510 * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default. 511 */ 512 public static final class Builder { 513 private Context mContext; 514 private String mId; 515 private PendingIntent mSessionActivity; 516 private Executor mCallbackExecutor; 517 private SessionCallback mCallback; 518 private Bundle mExtras; 519 520 /** 521 * Creates a builder for {@link MediaSession2}. 522 * 523 * @param context Context 524 * @throws IllegalArgumentException if context is {@code null}. 525 */ Builder(@onNull Context context)526 public Builder(@NonNull Context context) { 527 if (context == null) { 528 throw new IllegalArgumentException("context shouldn't be null"); 529 } 530 mContext = context; 531 } 532 533 /** 534 * Set an intent for launching UI for this Session. This can be used as a 535 * quick link to an ongoing media screen. The intent should be for an 536 * activity that may be started using {@link Context#startActivity(Intent)}. 537 * 538 * @param pi The intent to launch to show UI for this session. 539 * @return The Builder to allow chaining 540 */ 541 @NonNull setSessionActivity(@ullable PendingIntent pi)542 public Builder setSessionActivity(@Nullable PendingIntent pi) { 543 mSessionActivity = pi; 544 return this; 545 } 546 547 /** 548 * Set ID of the session. If it's not set, an empty string will be used to create a session. 549 * <p> 550 * Use this if and only if your app supports multiple playback at the same time and also 551 * wants to provide external apps to have finer controls of them. 552 * 553 * @param id id of the session. Must be unique per package. 554 * @throws IllegalArgumentException if id is {@code null}. 555 * @return The Builder to allow chaining 556 */ 557 @NonNull setId(@onNull String id)558 public Builder setId(@NonNull String id) { 559 if (id == null) { 560 throw new IllegalArgumentException("id shouldn't be null"); 561 } 562 mId = id; 563 return this; 564 } 565 566 /** 567 * Set callback for the session and its executor. 568 * 569 * @param executor callback executor 570 * @param callback session callback. 571 * @return The Builder to allow chaining 572 */ 573 @NonNull setSessionCallback(@onNull Executor executor, @NonNull SessionCallback callback)574 public Builder setSessionCallback(@NonNull Executor executor, 575 @NonNull SessionCallback callback) { 576 mCallbackExecutor = executor; 577 mCallback = callback; 578 return this; 579 } 580 581 /** 582 * Set extras for the session token. If null or not set, {@link Session2Token#getExtras()} 583 * will return an empty {@link Bundle}. An {@link IllegalArgumentException} will be thrown 584 * if the bundle contains any non-framework Parcelable objects. 585 * 586 * @return The Builder to allow chaining 587 * @see Session2Token#getExtras() 588 */ 589 @NonNull setExtras(@onNull Bundle extras)590 public Builder setExtras(@NonNull Bundle extras) { 591 if (extras == null) { 592 throw new NullPointerException("extras shouldn't be null"); 593 } 594 if (hasCustomParcelable(extras)) { 595 throw new IllegalArgumentException( 596 "extras shouldn't contain any custom parcelables"); 597 } 598 mExtras = new Bundle(extras); 599 return this; 600 } 601 602 /** 603 * Build {@link MediaSession2}. 604 * 605 * @return a new session 606 * @throws IllegalStateException if the session with the same id is already exists for the 607 * package. 608 */ 609 @NonNull build()610 public MediaSession2 build() { 611 if (mCallbackExecutor == null) { 612 mCallbackExecutor = mContext.getMainExecutor(); 613 } 614 if (mCallback == null) { 615 mCallback = new SessionCallback() {}; 616 } 617 if (mId == null) { 618 mId = ""; 619 } 620 if (mExtras == null) { 621 mExtras = Bundle.EMPTY; 622 } 623 MediaSession2 session2 = new MediaSession2(mContext, mId, mSessionActivity, 624 mCallbackExecutor, mCallback, mExtras); 625 626 // Notify framework about the newly create session after the constructor is finished. 627 // Otherwise, framework may access the session before the initialization is finished. 628 try { 629 if (SdkLevel.isAtLeastS()) { 630 MediaCommunicationManager manager = 631 mContext.getSystemService(MediaCommunicationManager.class); 632 manager.notifySession2Created(session2.getToken()); 633 } else { 634 MediaSessionManager manager = 635 mContext.getSystemService(MediaSessionManager.class); 636 manager.notifySession2Created(session2.getToken()); 637 } 638 } catch (Exception e) { 639 session2.close(); 640 throw e; 641 } 642 643 return session2; 644 } 645 } 646 647 /** 648 * This API is not generally intended for third party application developers. 649 * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 650 * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session 651 * Library</a> for consistent behavior across all devices. 652 * <p> 653 * Information of a controller. 654 */ 655 public static final class ControllerInfo { 656 private final RemoteUserInfo mRemoteUserInfo; 657 private final boolean mIsTrusted; 658 private final Controller2Link mControllerBinder; 659 private final Bundle mConnectionHints; 660 private final Object mLock = new Object(); 661 //@GuardedBy("mLock") 662 private int mNextSeqNumber; 663 //@GuardedBy("mLock") 664 private ArrayMap<ResultReceiver, Integer> mPendingCommands; 665 //@GuardedBy("mLock") 666 private ArraySet<Integer> mRequestedCommandSeqNumbers; 667 668 @SuppressWarnings("WeakerAccess") /* synthetic access */ 669 Session2CommandGroup mAllowedCommands; 670 671 /** 672 * @param remoteUserInfo remote user info 673 * @param trusted {@code true} if trusted, {@code false} otherwise 674 * @param controllerBinder Controller2Link for the connected controller. 675 * @param connectionHints a session-specific argument sent from the controller for the 676 * connection. The contents of this bundle may affect the 677 * connection result. 678 */ ControllerInfo(@onNull RemoteUserInfo remoteUserInfo, boolean trusted, @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints)679 ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted, 680 @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints) { 681 mRemoteUserInfo = remoteUserInfo; 682 mIsTrusted = trusted; 683 mControllerBinder = controllerBinder; 684 mConnectionHints = connectionHints; 685 mPendingCommands = new ArrayMap<>(); 686 mRequestedCommandSeqNumbers = new ArraySet<>(); 687 } 688 689 /** 690 * @return remote user info of the controller. 691 */ 692 @NonNull getRemoteUserInfo()693 public RemoteUserInfo getRemoteUserInfo() { 694 return mRemoteUserInfo; 695 } 696 697 /** 698 * @return package name of the controller. 699 */ 700 @NonNull getPackageName()701 public String getPackageName() { 702 return mRemoteUserInfo.getPackageName(); 703 } 704 705 /** 706 * @return uid of the controller. Can be a negative value if the uid cannot be obtained. 707 */ getUid()708 public int getUid() { 709 return mRemoteUserInfo.getUid(); 710 } 711 712 /** 713 * @return connection hints sent from controller. 714 */ 715 @NonNull getConnectionHints()716 public Bundle getConnectionHints() { 717 return new Bundle(mConnectionHints); 718 } 719 720 /** 721 * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or 722 * has a enabled notification listener so can be trusted to accept connection and incoming 723 * command request. 724 * 725 * @return {@code true} if the controller is trusted. 726 * @hide 727 */ isTrusted()728 public boolean isTrusted() { 729 return mIsTrusted; 730 } 731 732 @Override hashCode()733 public int hashCode() { 734 return Objects.hash(mControllerBinder, mRemoteUserInfo); 735 } 736 737 @Override equals(@ullable Object obj)738 public boolean equals(@Nullable Object obj) { 739 if (!(obj instanceof ControllerInfo)) return false; 740 if (this == obj) return true; 741 742 ControllerInfo other = (ControllerInfo) obj; 743 if (mControllerBinder != null || other.mControllerBinder != null) { 744 return Objects.equals(mControllerBinder, other.mControllerBinder); 745 } 746 return mRemoteUserInfo.equals(other.mRemoteUserInfo); 747 } 748 749 @Override 750 @NonNull toString()751 public String toString() { 752 return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid=" 753 + mRemoteUserInfo.getUid() + ", allowedCommands=" + mAllowedCommands + "})"; 754 } 755 notifyConnected(Bundle connectionResult)756 void notifyConnected(Bundle connectionResult) { 757 if (mControllerBinder == null) return; 758 759 try { 760 mControllerBinder.notifyConnected(getNextSeqNumber(), connectionResult); 761 } catch (RuntimeException e) { 762 // Controller may be died prematurely. 763 } 764 } 765 notifyDisconnected()766 void notifyDisconnected() { 767 if (mControllerBinder == null) return; 768 769 try { 770 mControllerBinder.notifyDisconnected(getNextSeqNumber()); 771 } catch (RuntimeException e) { 772 // Controller may be died prematurely. 773 } 774 } 775 notifyPlaybackActiveChanged(boolean playbackActive)776 void notifyPlaybackActiveChanged(boolean playbackActive) { 777 if (mControllerBinder == null) return; 778 779 try { 780 mControllerBinder.notifyPlaybackActiveChanged(getNextSeqNumber(), playbackActive); 781 } catch (RuntimeException e) { 782 // Controller may be died prematurely. 783 } 784 } 785 sendSessionCommand(Session2Command command, Bundle args, ResultReceiver resultReceiver)786 void sendSessionCommand(Session2Command command, Bundle args, 787 ResultReceiver resultReceiver) { 788 if (mControllerBinder == null) return; 789 790 try { 791 int seq = getNextSeqNumber(); 792 synchronized (mLock) { 793 mPendingCommands.put(resultReceiver, seq); 794 } 795 mControllerBinder.sendSessionCommand(seq, command, args, resultReceiver); 796 } catch (RuntimeException e) { 797 // Controller may be died prematurely. 798 synchronized (mLock) { 799 mPendingCommands.remove(resultReceiver); 800 } 801 resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null); 802 } 803 } 804 cancelSessionCommand(@onNull Object token)805 void cancelSessionCommand(@NonNull Object token) { 806 if (mControllerBinder == null) return; 807 Integer seq; 808 synchronized (mLock) { 809 seq = mPendingCommands.remove(token); 810 } 811 if (seq != null) { 812 mControllerBinder.cancelSessionCommand(seq); 813 } 814 } 815 receiveCommandResult(ResultReceiver resultReceiver)816 void receiveCommandResult(ResultReceiver resultReceiver) { 817 synchronized (mLock) { 818 mPendingCommands.remove(resultReceiver); 819 } 820 } 821 addRequestedCommandSeqNumber(int seq)822 void addRequestedCommandSeqNumber(int seq) { 823 synchronized (mLock) { 824 mRequestedCommandSeqNumbers.add(seq); 825 } 826 } 827 removeRequestedCommandSeqNumber(int seq)828 boolean removeRequestedCommandSeqNumber(int seq) { 829 synchronized (mLock) { 830 return mRequestedCommandSeqNumbers.remove(seq); 831 } 832 } 833 getNextSeqNumber()834 private int getNextSeqNumber() { 835 synchronized (mLock) { 836 return mNextSeqNumber++; 837 } 838 } 839 } 840 841 /** 842 * This API is not generally intended for third party application developers. 843 * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 844 * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session 845 * Library</a> for consistent behavior across all devices. 846 * <p> 847 * Callback to be called for all incoming commands from {@link MediaController2}s. 848 */ 849 public abstract static class SessionCallback { 850 /** 851 * Called when a controller is created for this session. Return allowed commands for 852 * controller. By default it returns {@code null}. 853 * <p> 854 * You can reject the connection by returning {@code null}. In that case, controller 855 * receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)} 856 * and cannot be used. 857 * <p> 858 * The controller hasn't connected yet in this method, so calls to the controller 859 * (e.g. {@link #sendSessionCommand}) would be ignored. Override {@link #onPostConnect} for 860 * the custom initialization for the controller instead. 861 * 862 * @param session the session for this event 863 * @param controller controller information. 864 * @return allowed commands. Can be {@code null} to reject connection. 865 */ 866 @Nullable onConnect(@onNull MediaSession2 session, @NonNull ControllerInfo controller)867 public Session2CommandGroup onConnect(@NonNull MediaSession2 session, 868 @NonNull ControllerInfo controller) { 869 return null; 870 } 871 872 /** 873 * Called immediately after a controller is connected. This is a convenient method to add 874 * custom initialization between the session and a controller. 875 * <p> 876 * Note that calls to the controller (e.g. {@link #sendSessionCommand}) work here but don't 877 * work in {@link #onConnect} because the controller hasn't connected yet in 878 * {@link #onConnect}. 879 * 880 * @param session the session for this event 881 * @param controller controller information. 882 */ onPostConnect(@onNull MediaSession2 session, @NonNull ControllerInfo controller)883 public void onPostConnect(@NonNull MediaSession2 session, 884 @NonNull ControllerInfo controller) { 885 } 886 887 /** 888 * Called when a controller is disconnected 889 * 890 * @param session the session for this event 891 * @param controller controller information 892 */ onDisconnected(@onNull MediaSession2 session, @NonNull ControllerInfo controller)893 public void onDisconnected(@NonNull MediaSession2 session, 894 @NonNull ControllerInfo controller) {} 895 896 /** 897 * Called when a controller sent a session command. 898 * 899 * @param session the session for this event 900 * @param controller controller information 901 * @param command the session command 902 * @param args optional arguments 903 * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED 904 * will be sent to the session. 905 */ 906 @Nullable onSessionCommand(@onNull MediaSession2 session, @NonNull ControllerInfo controller, @NonNull Session2Command command, @Nullable Bundle args)907 public Session2Command.Result onSessionCommand(@NonNull MediaSession2 session, 908 @NonNull ControllerInfo controller, @NonNull Session2Command command, 909 @Nullable Bundle args) { 910 return null; 911 } 912 913 /** 914 * Called when the command sent to the controller is finished. 915 * 916 * @param session the session for this event 917 * @param controller controller information 918 * @param token the token got from {@link MediaSession2#sendSessionCommand} 919 * @param command the session command 920 * @param result the result of the session command 921 */ onCommandResult(@onNull MediaSession2 session, @NonNull ControllerInfo controller, @NonNull Object token, @NonNull Session2Command command, @NonNull Session2Command.Result result)922 public void onCommandResult(@NonNull MediaSession2 session, 923 @NonNull ControllerInfo controller, @NonNull Object token, 924 @NonNull Session2Command command, @NonNull Session2Command.Result result) {} 925 } 926 927 abstract static class ForegroundServiceEventCallback { onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive)928 public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {} onSessionClosed(MediaSession2 session)929 public void onSessionClosed(MediaSession2 session) {} 930 } 931 } 932