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 whether 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 for (String key : out.keySet()) { 306 out.get(key); 307 } 308 } catch (BadParcelableException e) { 309 Log.d(TAG, "Custom parcelable in bundle.", e); 310 return true; 311 } finally { 312 if (parcel != null) { 313 parcel.recycle(); 314 } 315 } 316 return false; 317 } 318 isClosed()319 boolean isClosed() { 320 synchronized (mLock) { 321 return mClosed; 322 } 323 } 324 getCallback()325 SessionCallback getCallback() { 326 return mCallback; 327 } 328 isTrustedForMediaControl(RemoteUserInfo remoteUserInfo)329 boolean isTrustedForMediaControl(RemoteUserInfo remoteUserInfo) { 330 if (SdkLevel.isAtLeastS()) { 331 return mCommunicationManager.isTrustedForMediaControl(remoteUserInfo); 332 } else { 333 return mMediaSessionManager.isTrustedForMediaControl(remoteUserInfo); 334 } 335 } 336 setForegroundServiceEventCallback(ForegroundServiceEventCallback callback)337 void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) { 338 synchronized (mLock) { 339 if (mForegroundServiceEventCallback == callback) { 340 return; 341 } 342 if (mForegroundServiceEventCallback != null && callback != null) { 343 throw new IllegalStateException("A session cannot be added to multiple services"); 344 } 345 mForegroundServiceEventCallback = callback; 346 } 347 } 348 349 // Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq, Bundle connectionRequest)350 void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq, 351 Bundle connectionRequest) { 352 if (callingPid == 0) { 353 // The pid here is from Binder.getCallingPid(), which can be 0 for an oneway call from 354 // the remote process. If it's the case, use PID from the connectionRequest. 355 callingPid = connectionRequest.getInt(KEY_PID); 356 } 357 String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME); 358 359 RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid); 360 361 Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS); 362 if (connectionHints == null) { 363 Log.w(TAG, "connectionHints shouldn't be null."); 364 connectionHints = Bundle.EMPTY; 365 } else if (hasCustomParcelable(connectionHints)) { 366 Log.w(TAG, "connectionHints contain custom parcelable. Ignoring."); 367 connectionHints = Bundle.EMPTY; 368 } 369 370 final ControllerInfo controllerInfo = new ControllerInfo( 371 remoteUserInfo, 372 isTrustedForMediaControl(remoteUserInfo), 373 controller, 374 connectionHints); 375 mCallbackExecutor.execute(() -> { 376 boolean connected = false; 377 try { 378 if (isClosed()) { 379 return; 380 } 381 controllerInfo.mAllowedCommands = 382 mCallback.onConnect(MediaSession2.this, controllerInfo); 383 // Don't reject connection for the request from trusted app. 384 // Otherwise server will fail to retrieve session's information to dispatch 385 // media keys to. 386 if (controllerInfo.mAllowedCommands == null && !controllerInfo.isTrusted()) { 387 return; 388 } 389 if (controllerInfo.mAllowedCommands == null) { 390 // For trusted apps, send non-null allowed commands to keep 391 // connection. 392 controllerInfo.mAllowedCommands = 393 new Session2CommandGroup.Builder().build(); 394 } 395 if (DEBUG) { 396 Log.d(TAG, "Accepting connection: " + controllerInfo); 397 } 398 // If connection is accepted, notify the current state to the controller. 399 // It's needed because we cannot call synchronous calls between 400 // session/controller. 401 Bundle connectionResult = new Bundle(); 402 connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub); 403 connectionResult.putParcelable(KEY_ALLOWED_COMMANDS, 404 controllerInfo.mAllowedCommands); 405 connectionResult.putBoolean(KEY_PLAYBACK_ACTIVE, isPlaybackActive()); 406 connectionResult.putBundle(KEY_TOKEN_EXTRAS, mSessionToken.getExtras()); 407 408 // Double check if session is still there, because close() can be called in 409 // another thread. 410 if (isClosed()) { 411 return; 412 } 413 controllerInfo.notifyConnected(connectionResult); 414 synchronized (mLock) { 415 if (mConnectedControllers.containsKey(controller)) { 416 Log.w(TAG, "Controller " + controllerInfo + " has sent connection" 417 + " request multiple times"); 418 } 419 mConnectedControllers.put(controller, controllerInfo); 420 } 421 mCallback.onPostConnect(MediaSession2.this, controllerInfo); 422 connected = true; 423 } finally { 424 if (!connected || isClosed()) { 425 if (DEBUG) { 426 Log.d(TAG, "Rejecting connection or notifying that session is closed" 427 + ", controllerInfo=" + controllerInfo); 428 } 429 synchronized (mLock) { 430 mConnectedControllers.remove(controller); 431 } 432 controllerInfo.notifyDisconnected(); 433 } 434 } 435 }); 436 } 437 438 // Called by Session2Link.onDisconnect onDisconnect(@onNull final Controller2Link controller, int seq)439 void onDisconnect(@NonNull final Controller2Link controller, int seq) { 440 final ControllerInfo controllerInfo; 441 synchronized (mLock) { 442 controllerInfo = mConnectedControllers.remove(controller); 443 } 444 if (controllerInfo == null) { 445 return; 446 } 447 mCallbackExecutor.execute(() -> { 448 mCallback.onDisconnected(MediaSession2.this, controllerInfo); 449 }); 450 } 451 452 // Called by Session2Link.onSessionCommand onSessionCommand(@onNull final Controller2Link controller, final int seq, final Session2Command command, final Bundle args, @Nullable ResultReceiver resultReceiver)453 void onSessionCommand(@NonNull final Controller2Link controller, final int seq, 454 final Session2Command command, final Bundle args, 455 @Nullable ResultReceiver resultReceiver) { 456 if (controller == null) { 457 return; 458 } 459 final ControllerInfo controllerInfo; 460 synchronized (mLock) { 461 controllerInfo = mConnectedControllers.get(controller); 462 } 463 if (controllerInfo == null) { 464 return; 465 } 466 467 // TODO: check allowed commands. 468 synchronized (mLock) { 469 controllerInfo.addRequestedCommandSeqNumber(seq); 470 } 471 mCallbackExecutor.execute(() -> { 472 if (!controllerInfo.removeRequestedCommandSeqNumber(seq)) { 473 if (resultReceiver != null) { 474 resultReceiver.send(RESULT_INFO_SKIPPED, null); 475 } 476 return; 477 } 478 Session2Command.Result result = mCallback.onSessionCommand( 479 MediaSession2.this, controllerInfo, command, args); 480 if (resultReceiver != null) { 481 if (result == null) { 482 resultReceiver.send(RESULT_INFO_SKIPPED, null); 483 } else { 484 resultReceiver.send(result.getResultCode(), result.getResultData()); 485 } 486 } 487 }); 488 } 489 490 // Called by Session2Link.onCancelCommand onCancelCommand(@onNull final Controller2Link controller, final int seq)491 void onCancelCommand(@NonNull final Controller2Link controller, final int seq) { 492 final ControllerInfo controllerInfo; 493 synchronized (mLock) { 494 controllerInfo = mConnectedControllers.get(controller); 495 } 496 if (controllerInfo == null) { 497 return; 498 } 499 controllerInfo.removeRequestedCommandSeqNumber(seq); 500 } 501 502 /** 503 * This API is not generally intended for third party application developers. 504 * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 505 * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session 506 * Library</a> for consistent behavior across all devices. 507 * <p> 508 * Builder for {@link MediaSession2}. 509 * <p> 510 * Any incoming event from the {@link MediaController2} will be handled on the callback 511 * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default. 512 */ 513 public static final class Builder { 514 private Context mContext; 515 private String mId; 516 private PendingIntent mSessionActivity; 517 private Executor mCallbackExecutor; 518 private SessionCallback mCallback; 519 private Bundle mExtras; 520 521 /** 522 * Creates a builder for {@link MediaSession2}. 523 * 524 * @param context Context 525 * @throws IllegalArgumentException if context is {@code null}. 526 */ Builder(@onNull Context context)527 public Builder(@NonNull Context context) { 528 if (context == null) { 529 throw new IllegalArgumentException("context shouldn't be null"); 530 } 531 mContext = context; 532 } 533 534 /** 535 * Set an intent for launching UI for this Session. This can be used as a 536 * quick link to an ongoing media screen. The intent should be for an 537 * activity that may be started using {@link Context#startActivity(Intent)}. 538 * 539 * @param pi The intent to launch to show UI for this session. 540 * @return The Builder to allow chaining 541 */ 542 @NonNull setSessionActivity(@ullable PendingIntent pi)543 public Builder setSessionActivity(@Nullable PendingIntent pi) { 544 mSessionActivity = pi; 545 return this; 546 } 547 548 /** 549 * Set ID of the session. If it's not set, an empty string will be used to create a session. 550 * <p> 551 * Use this if and only if your app supports multiple playback at the same time and also 552 * wants to provide external apps to have finer controls of them. 553 * 554 * @param id id of the session. Must be unique per package. 555 * @throws IllegalArgumentException if id is {@code null}. 556 * @return The Builder to allow chaining 557 */ 558 @NonNull setId(@onNull String id)559 public Builder setId(@NonNull String id) { 560 if (id == null) { 561 throw new IllegalArgumentException("id shouldn't be null"); 562 } 563 mId = id; 564 return this; 565 } 566 567 /** 568 * Set callback for the session and its executor. 569 * 570 * @param executor callback executor 571 * @param callback session callback. 572 * @return The Builder to allow chaining 573 */ 574 @NonNull setSessionCallback(@onNull Executor executor, @NonNull SessionCallback callback)575 public Builder setSessionCallback(@NonNull Executor executor, 576 @NonNull SessionCallback callback) { 577 mCallbackExecutor = executor; 578 mCallback = callback; 579 return this; 580 } 581 582 /** 583 * Set extras for the session token. If null or not set, {@link Session2Token#getExtras()} 584 * will return an empty {@link Bundle}. An {@link IllegalArgumentException} will be thrown 585 * if the bundle contains any non-framework Parcelable objects. 586 * 587 * @return The Builder to allow chaining 588 * @see Session2Token#getExtras() 589 */ 590 @NonNull setExtras(@onNull Bundle extras)591 public Builder setExtras(@NonNull Bundle extras) { 592 if (extras == null) { 593 throw new NullPointerException("extras shouldn't be null"); 594 } 595 if (hasCustomParcelable(extras)) { 596 throw new IllegalArgumentException( 597 "extras shouldn't contain any custom parcelables"); 598 } 599 mExtras = new Bundle(extras); 600 return this; 601 } 602 603 /** 604 * Build {@link MediaSession2}. 605 * 606 * @return a new session 607 * @throws IllegalStateException if the session with the same id is already exists for the 608 * package. 609 */ 610 @NonNull build()611 public MediaSession2 build() { 612 if (mCallbackExecutor == null) { 613 mCallbackExecutor = mContext.getMainExecutor(); 614 } 615 if (mCallback == null) { 616 mCallback = new SessionCallback() {}; 617 } 618 if (mId == null) { 619 mId = ""; 620 } 621 if (mExtras == null) { 622 mExtras = Bundle.EMPTY; 623 } 624 MediaSession2 session2 = new MediaSession2(mContext, mId, mSessionActivity, 625 mCallbackExecutor, mCallback, mExtras); 626 627 // Notify framework about the newly create session after the constructor is finished. 628 // Otherwise, framework may access the session before the initialization is finished. 629 try { 630 if (SdkLevel.isAtLeastS()) { 631 MediaCommunicationManager manager = 632 mContext.getSystemService(MediaCommunicationManager.class); 633 manager.notifySession2Created(session2.getToken()); 634 } else { 635 MediaSessionManager manager = 636 mContext.getSystemService(MediaSessionManager.class); 637 manager.notifySession2Created(session2.getToken()); 638 } 639 } catch (Exception e) { 640 session2.close(); 641 throw e; 642 } 643 644 return session2; 645 } 646 } 647 648 /** 649 * This API is not generally intended for third party application developers. 650 * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 651 * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session 652 * Library</a> for consistent behavior across all devices. 653 * <p> 654 * Information of a controller. 655 */ 656 public static final class ControllerInfo { 657 private final RemoteUserInfo mRemoteUserInfo; 658 private final boolean mIsTrusted; 659 private final Controller2Link mControllerBinder; 660 private final Bundle mConnectionHints; 661 private final Object mLock = new Object(); 662 //@GuardedBy("mLock") 663 private int mNextSeqNumber; 664 //@GuardedBy("mLock") 665 private ArrayMap<ResultReceiver, Integer> mPendingCommands; 666 //@GuardedBy("mLock") 667 private ArraySet<Integer> mRequestedCommandSeqNumbers; 668 669 @SuppressWarnings("WeakerAccess") /* synthetic access */ 670 Session2CommandGroup mAllowedCommands; 671 672 /** 673 * @param remoteUserInfo remote user info 674 * @param trusted {@code true} if trusted, {@code false} otherwise 675 * @param controllerBinder Controller2Link for the connected controller. 676 * @param connectionHints a session-specific argument sent from the controller for the 677 * connection. The contents of this bundle may affect the 678 * connection result. 679 */ ControllerInfo(@onNull RemoteUserInfo remoteUserInfo, boolean trusted, @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints)680 ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted, 681 @Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints) { 682 mRemoteUserInfo = remoteUserInfo; 683 mIsTrusted = trusted; 684 mControllerBinder = controllerBinder; 685 mConnectionHints = connectionHints; 686 mPendingCommands = new ArrayMap<>(); 687 mRequestedCommandSeqNumbers = new ArraySet<>(); 688 } 689 690 /** 691 * @return remote user info of the controller. 692 */ 693 @NonNull getRemoteUserInfo()694 public RemoteUserInfo getRemoteUserInfo() { 695 return mRemoteUserInfo; 696 } 697 698 /** 699 * @return package name of the controller. 700 */ 701 @NonNull getPackageName()702 public String getPackageName() { 703 return mRemoteUserInfo.getPackageName(); 704 } 705 706 /** 707 * @return uid of the controller. Can be a negative value if the uid cannot be obtained. 708 */ getUid()709 public int getUid() { 710 return mRemoteUserInfo.getUid(); 711 } 712 713 /** 714 * @return connection hints sent from controller. 715 */ 716 @NonNull getConnectionHints()717 public Bundle getConnectionHints() { 718 return new Bundle(mConnectionHints); 719 } 720 721 /** 722 * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or 723 * has a enabled notification listener so can be trusted to accept connection and incoming 724 * command request. 725 * 726 * @return {@code true} if the controller is trusted. 727 * @hide 728 */ isTrusted()729 public boolean isTrusted() { 730 return mIsTrusted; 731 } 732 733 @Override hashCode()734 public int hashCode() { 735 return Objects.hash(mControllerBinder, mRemoteUserInfo); 736 } 737 738 @Override equals(@ullable Object obj)739 public boolean equals(@Nullable Object obj) { 740 if (!(obj instanceof ControllerInfo)) return false; 741 if (this == obj) return true; 742 743 ControllerInfo other = (ControllerInfo) obj; 744 if (mControllerBinder != null || other.mControllerBinder != null) { 745 return Objects.equals(mControllerBinder, other.mControllerBinder); 746 } 747 return mRemoteUserInfo.equals(other.mRemoteUserInfo); 748 } 749 750 @Override 751 @NonNull toString()752 public String toString() { 753 return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid=" 754 + mRemoteUserInfo.getUid() + ", allowedCommands=" + mAllowedCommands + "})"; 755 } 756 notifyConnected(Bundle connectionResult)757 void notifyConnected(Bundle connectionResult) { 758 if (mControllerBinder == null) return; 759 760 try { 761 mControllerBinder.notifyConnected(getNextSeqNumber(), connectionResult); 762 } catch (RuntimeException e) { 763 // Controller may be died prematurely. 764 } 765 } 766 notifyDisconnected()767 void notifyDisconnected() { 768 if (mControllerBinder == null) return; 769 770 try { 771 mControllerBinder.notifyDisconnected(getNextSeqNumber()); 772 } catch (RuntimeException e) { 773 // Controller may be died prematurely. 774 } 775 } 776 notifyPlaybackActiveChanged(boolean playbackActive)777 void notifyPlaybackActiveChanged(boolean playbackActive) { 778 if (mControllerBinder == null) return; 779 780 try { 781 mControllerBinder.notifyPlaybackActiveChanged(getNextSeqNumber(), playbackActive); 782 } catch (RuntimeException e) { 783 // Controller may be died prematurely. 784 } 785 } 786 sendSessionCommand(Session2Command command, Bundle args, ResultReceiver resultReceiver)787 void sendSessionCommand(Session2Command command, Bundle args, 788 ResultReceiver resultReceiver) { 789 if (mControllerBinder == null) return; 790 791 try { 792 int seq = getNextSeqNumber(); 793 synchronized (mLock) { 794 mPendingCommands.put(resultReceiver, seq); 795 } 796 mControllerBinder.sendSessionCommand(seq, command, args, resultReceiver); 797 } catch (RuntimeException e) { 798 // Controller may be died prematurely. 799 synchronized (mLock) { 800 mPendingCommands.remove(resultReceiver); 801 } 802 resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null); 803 } 804 } 805 cancelSessionCommand(@onNull Object token)806 void cancelSessionCommand(@NonNull Object token) { 807 if (mControllerBinder == null) return; 808 Integer seq; 809 synchronized (mLock) { 810 seq = mPendingCommands.remove(token); 811 } 812 if (seq != null) { 813 mControllerBinder.cancelSessionCommand(seq); 814 } 815 } 816 receiveCommandResult(ResultReceiver resultReceiver)817 void receiveCommandResult(ResultReceiver resultReceiver) { 818 synchronized (mLock) { 819 mPendingCommands.remove(resultReceiver); 820 } 821 } 822 addRequestedCommandSeqNumber(int seq)823 void addRequestedCommandSeqNumber(int seq) { 824 synchronized (mLock) { 825 mRequestedCommandSeqNumbers.add(seq); 826 } 827 } 828 removeRequestedCommandSeqNumber(int seq)829 boolean removeRequestedCommandSeqNumber(int seq) { 830 synchronized (mLock) { 831 return mRequestedCommandSeqNumbers.remove(seq); 832 } 833 } 834 getNextSeqNumber()835 private int getNextSeqNumber() { 836 synchronized (mLock) { 837 return mNextSeqNumber++; 838 } 839 } 840 } 841 842 /** 843 * This API is not generally intended for third party application developers. 844 * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> 845 * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session 846 * Library</a> for consistent behavior across all devices. 847 * <p> 848 * Callback to be called for all incoming commands from {@link MediaController2}s. 849 */ 850 public abstract static class SessionCallback { 851 /** 852 * Called when a controller is created for this session. Return allowed commands for 853 * controller. By default it returns {@code null}. 854 * <p> 855 * You can reject the connection by returning {@code null}. In that case, controller 856 * receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)} 857 * and cannot be used. 858 * <p> 859 * The controller hasn't connected yet in this method, so calls to the controller 860 * (e.g. {@link #sendSessionCommand}) would be ignored. Override {@link #onPostConnect} for 861 * the custom initialization for the controller instead. 862 * 863 * @param session the session for this event 864 * @param controller controller information. 865 * @return allowed commands. Can be {@code null} to reject connection. 866 */ 867 @Nullable onConnect(@onNull MediaSession2 session, @NonNull ControllerInfo controller)868 public Session2CommandGroup onConnect(@NonNull MediaSession2 session, 869 @NonNull ControllerInfo controller) { 870 return null; 871 } 872 873 /** 874 * Called immediately after a controller is connected. This is a convenient method to add 875 * custom initialization between the session and a controller. 876 * <p> 877 * Note that calls to the controller (e.g. {@link #sendSessionCommand}) work here but don't 878 * work in {@link #onConnect} because the controller hasn't connected yet in 879 * {@link #onConnect}. 880 * 881 * @param session the session for this event 882 * @param controller controller information. 883 */ onPostConnect(@onNull MediaSession2 session, @NonNull ControllerInfo controller)884 public void onPostConnect(@NonNull MediaSession2 session, 885 @NonNull ControllerInfo controller) { 886 } 887 888 /** 889 * Called when a controller is disconnected 890 * 891 * @param session the session for this event 892 * @param controller controller information 893 */ onDisconnected(@onNull MediaSession2 session, @NonNull ControllerInfo controller)894 public void onDisconnected(@NonNull MediaSession2 session, 895 @NonNull ControllerInfo controller) {} 896 897 /** 898 * Called when a controller sent a session command. 899 * 900 * @param session the session for this event 901 * @param controller controller information 902 * @param command the session command 903 * @param args optional arguments 904 * @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED 905 * will be sent to the session. 906 */ 907 @Nullable onSessionCommand(@onNull MediaSession2 session, @NonNull ControllerInfo controller, @NonNull Session2Command command, @Nullable Bundle args)908 public Session2Command.Result onSessionCommand(@NonNull MediaSession2 session, 909 @NonNull ControllerInfo controller, @NonNull Session2Command command, 910 @Nullable Bundle args) { 911 return null; 912 } 913 914 /** 915 * Called when the command sent to the controller is finished. 916 * 917 * @param session the session for this event 918 * @param controller controller information 919 * @param token the token got from {@link MediaSession2#sendSessionCommand} 920 * @param command the session command 921 * @param result the result of the session command 922 */ onCommandResult(@onNull MediaSession2 session, @NonNull ControllerInfo controller, @NonNull Object token, @NonNull Session2Command command, @NonNull Session2Command.Result result)923 public void onCommandResult(@NonNull MediaSession2 session, 924 @NonNull ControllerInfo controller, @NonNull Object token, 925 @NonNull Session2Command command, @NonNull Session2Command.Result result) {} 926 } 927 928 abstract static class ForegroundServiceEventCallback { onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive)929 public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {} onSessionClosed(MediaSession2 session)930 public void onSessionClosed(MediaSession2 session) {} 931 } 932 } 933