1 /* 2 * Copyright 2020 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 package com.android.server.media; 17 18 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; 19 import static android.Manifest.permission.MEDIA_CONTENT_CONTROL; 20 import static android.os.UserHandle.ALL; 21 import static android.os.UserHandle.getUserHandleForUid; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.RequiresPermission; 26 import android.app.ActivityManager; 27 import android.app.NotificationManager; 28 import android.content.Context; 29 import android.content.pm.PackageManager; 30 import android.media.IMediaCommunicationService; 31 import android.media.IMediaCommunicationServiceCallback; 32 import android.media.MediaController2; 33 import android.media.MediaParceledListSlice; 34 import android.media.Session2CommandGroup; 35 import android.media.Session2Token; 36 import android.media.session.MediaSessionManager; 37 import android.os.Binder; 38 import android.os.Build; 39 import android.os.Handler; 40 import android.os.IBinder; 41 import android.os.Looper; 42 import android.os.Process; 43 import android.os.RemoteException; 44 import android.os.UserHandle; 45 import android.os.UserManager; 46 import android.util.Log; 47 import android.util.SparseArray; 48 import android.util.SparseIntArray; 49 import android.view.KeyEvent; 50 51 import androidx.annotation.RequiresApi; 52 53 import com.android.internal.annotations.GuardedBy; 54 import com.android.modules.annotation.MinSdk; 55 import com.android.server.SystemService; 56 57 import java.lang.ref.WeakReference; 58 import java.util.ArrayList; 59 import java.util.List; 60 import java.util.Objects; 61 import java.util.concurrent.Executor; 62 import java.util.concurrent.Executors; 63 64 /** 65 * A system service that manages {@link android.media.MediaSession2} creations 66 * and their ongoing media playback state. 67 * @hide 68 */ 69 @MinSdk(Build.VERSION_CODES.S) 70 @RequiresApi(Build.VERSION_CODES.S) 71 public class MediaCommunicationService extends SystemService { 72 private static final String TAG = "MediaCommunicationSrv"; 73 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 74 75 final Context mContext; 76 77 final Object mLock = new Object(); 78 final Handler mHandler = new Handler(Looper.getMainLooper()); 79 80 @GuardedBy("mLock") 81 private final SparseIntArray mFullUserIds = new SparseIntArray(); 82 @GuardedBy("mLock") 83 private final SparseArray<FullUserRecord> mUserRecords = new SparseArray<>(); 84 85 final Executor mRecordExecutor = Executors.newSingleThreadExecutor(); 86 @GuardedBy("mLock") 87 final ArrayList<CallbackRecord> mCallbackRecords = new ArrayList<>(); 88 final NotificationManager mNotificationManager; 89 MediaSessionManager mSessionManager; 90 MediaCommunicationService(Context context)91 public MediaCommunicationService(Context context) { 92 super(context); 93 mContext = context; 94 mNotificationManager = context.getSystemService(NotificationManager.class); 95 } 96 97 @Override onStart()98 public void onStart() { 99 publishBinderService(Context.MEDIA_COMMUNICATION_SERVICE, new Stub()); 100 updateUser(); 101 } 102 103 @Override onBootPhase(int phase)104 public void onBootPhase(int phase) { 105 super.onBootPhase(phase); 106 switch (phase) { 107 // This ensures MediaSessionService is started 108 case PHASE_BOOT_COMPLETED: 109 mSessionManager = mContext.getSystemService(MediaSessionManager.class); 110 break; 111 } 112 } 113 114 @Override onUserStarting(@onNull TargetUser user)115 public void onUserStarting(@NonNull TargetUser user) { 116 if (DEBUG) Log.d(TAG, "onUserStarting: " + user); 117 updateUser(); 118 } 119 120 @Override onUserSwitching(@ullable TargetUser from, @NonNull TargetUser to)121 public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) { 122 if (DEBUG) Log.d(TAG, "onUserSwitching: " + to); 123 updateUser(); 124 } 125 126 @Override onUserStopped(@onNull TargetUser targetUser)127 public void onUserStopped(@NonNull TargetUser targetUser) { 128 int userId = targetUser.getUserHandle().getIdentifier(); 129 130 if (DEBUG) Log.d(TAG, "onUserStopped: " + userId); 131 synchronized (mLock) { 132 FullUserRecord user = getFullUserRecordLocked(userId); 133 if (user != null) { 134 if (user.getFullUserId() == userId) { 135 user.destroyAllSessions(); 136 mUserRecords.remove(userId); 137 } else { 138 user.destroySessionsForUser(userId); 139 } 140 } 141 } 142 updateUser(); 143 } 144 145 @Nullable findCallbackRecordLocked(@ullable IMediaCommunicationServiceCallback callback)146 CallbackRecord findCallbackRecordLocked(@Nullable IMediaCommunicationServiceCallback callback) { 147 if (callback == null) { 148 return null; 149 } 150 for (CallbackRecord record : mCallbackRecords) { 151 if (Objects.equals(callback.asBinder(), record.mCallback.asBinder())) { 152 return record; 153 } 154 } 155 return null; 156 } 157 getSession2TokensLocked(int userId)158 ArrayList<Session2Token> getSession2TokensLocked(int userId) { 159 ArrayList<Session2Token> list = new ArrayList<>(); 160 if (userId == ALL.getIdentifier()) { 161 int size = mUserRecords.size(); 162 for (int i = 0; i < size; i++) { 163 list.addAll(mUserRecords.valueAt(i).getAllSession2Tokens()); 164 } 165 } else { 166 FullUserRecord user = getFullUserRecordLocked(userId); 167 if (user != null) { 168 list.addAll(user.getSession2Tokens(userId)); 169 } 170 } 171 return list; 172 } 173 getFullUserRecordLocked(int userId)174 private FullUserRecord getFullUserRecordLocked(int userId) { 175 int fullUserId = mFullUserIds.get(userId, -1); 176 if (fullUserId < 0) { 177 return null; 178 } 179 return mUserRecords.get(fullUserId); 180 } 181 hasMediaControlPermission(int pid, int uid)182 private boolean hasMediaControlPermission(int pid, int uid) { 183 // Check if it's system server or has MEDIA_CONTENT_CONTROL. 184 // Note that system server doesn't have MEDIA_CONTENT_CONTROL, so we need extra 185 // check here. 186 if (uid == Process.SYSTEM_UID || mContext.checkPermission( 187 android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid) 188 == PackageManager.PERMISSION_GRANTED) { 189 return true; 190 } else if (DEBUG) { 191 Log.d(TAG, "uid(" + uid + ") hasn't granted MEDIA_CONTENT_CONTROL"); 192 } 193 return false; 194 } 195 updateUser()196 private void updateUser() { 197 UserManager manager = mContext.getSystemService(UserManager.class); 198 List<UserHandle> allUsers = manager.getUserHandles(/*excludeDying=*/false); 199 200 synchronized (mLock) { 201 mFullUserIds.clear(); 202 if (allUsers != null) { 203 for (UserHandle user : allUsers) { 204 UserHandle parent = manager.getProfileParent(user); 205 if (parent != null) { 206 mFullUserIds.put(user.getIdentifier(), parent.getIdentifier()); 207 } else { 208 mFullUserIds.put(user.getIdentifier(), user.getIdentifier()); 209 if (mUserRecords.get(user.getIdentifier()) == null) { 210 mUserRecords.put(user.getIdentifier(), 211 new FullUserRecord(user.getIdentifier())); 212 } 213 } 214 } 215 } 216 // Ensure that the current full user exists. 217 int currentFullUserId = ActivityManager.getCurrentUser(); 218 FullUserRecord currentFullUserRecord = mUserRecords.get(currentFullUserId); 219 if (currentFullUserRecord == null) { 220 Log.w(TAG, "Cannot find FullUserInfo for the current user " + currentFullUserId); 221 currentFullUserRecord = new FullUserRecord(currentFullUserId); 222 mUserRecords.put(currentFullUserId, currentFullUserRecord); 223 } 224 mFullUserIds.put(currentFullUserId, currentFullUserId); 225 } 226 } 227 dispatchSession2Created(Session2Token token, int pid)228 void dispatchSession2Created(Session2Token token, int pid) { 229 synchronized (mLock) { 230 for (CallbackRecord record : mCallbackRecords) { 231 if (record.mUserId != ALL.getIdentifier() 232 && record.mUserId != getUserHandleForUid(token.getUid()).getIdentifier()) { 233 continue; 234 } 235 try { 236 record.mCallback.onSession2Created(token, pid); 237 } catch (RemoteException e) { 238 Log.w(TAG, "Failed to notify session2 token created " + record); 239 } 240 } 241 } 242 } 243 dispatchSession2Changed(int userId)244 void dispatchSession2Changed(int userId) { 245 ArrayList<Session2Token> allSession2Tokens; 246 ArrayList<Session2Token> userSession2Tokens; 247 248 synchronized (mLock) { 249 allSession2Tokens = getSession2TokensLocked(ALL.getIdentifier()); 250 userSession2Tokens = getSession2TokensLocked(userId); 251 252 for (CallbackRecord record : mCallbackRecords) { 253 if (record.mUserId == ALL.getIdentifier()) { 254 try { 255 MediaParceledListSlice<Session2Token> toSend = 256 new MediaParceledListSlice<>(allSession2Tokens); 257 toSend.setInlineCountLimit(0); 258 record.mCallback.onSession2Changed(toSend); 259 } catch (RemoteException e) { 260 Log.w(TAG, "Failed to notify session2 tokens changed " + record); 261 } 262 } else if (record.mUserId == userId) { 263 try { 264 MediaParceledListSlice<Session2Token> toSend = 265 new MediaParceledListSlice<>(userSession2Tokens); 266 toSend.setInlineCountLimit(0); 267 record.mCallback.onSession2Changed(toSend); 268 } catch (RemoteException e) { 269 Log.w(TAG, "Failed to notify session2 tokens changed " + record); 270 } 271 } 272 } 273 } 274 } 275 removeSessionRecord(Session2Record session)276 private void removeSessionRecord(Session2Record session) { 277 if (DEBUG) { 278 Log.d(TAG, "Removing " + session); 279 } 280 281 FullUserRecord user = session.getFullUser(); 282 if (user != null) { 283 user.removeSession(session); 284 } 285 } 286 onSessionPlaybackStateChanged(Session2Record session, boolean promotePriority)287 void onSessionPlaybackStateChanged(Session2Record session, boolean promotePriority) { 288 FullUserRecord user = session.getFullUser(); 289 if (user == null || !user.containsSession(session)) { 290 Log.d(TAG, "Unknown session changed playback state. Ignoring."); 291 return; 292 } 293 user.onPlaybackStateChanged(session, promotePriority); 294 } 295 296 isMediaSessionKey(int keyCode)297 static boolean isMediaSessionKey(int keyCode) { 298 switch (keyCode) { 299 case KeyEvent.KEYCODE_MEDIA_PLAY: 300 case KeyEvent.KEYCODE_MEDIA_PAUSE: 301 case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: 302 case KeyEvent.KEYCODE_MUTE: 303 case KeyEvent.KEYCODE_HEADSETHOOK: 304 case KeyEvent.KEYCODE_MEDIA_STOP: 305 case KeyEvent.KEYCODE_MEDIA_NEXT: 306 case KeyEvent.KEYCODE_MEDIA_PREVIOUS: 307 case KeyEvent.KEYCODE_MEDIA_REWIND: 308 case KeyEvent.KEYCODE_MEDIA_RECORD: 309 case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: 310 return true; 311 } 312 return false; 313 } 314 315 private class Stub extends IMediaCommunicationService.Stub { 316 @Override notifySession2Created(Session2Token sessionToken)317 public void notifySession2Created(Session2Token sessionToken) { 318 final int pid = Binder.getCallingPid(); 319 final int uid = Binder.getCallingUid(); 320 final long token = Binder.clearCallingIdentity(); 321 322 try { 323 if (DEBUG) { 324 Log.d(TAG, "Session2 is created " + sessionToken); 325 } 326 if (uid != sessionToken.getUid()) { 327 throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid 328 + " but actually=" + sessionToken.getUid()); 329 } 330 FullUserRecord user; 331 int userId = getUserHandleForUid(sessionToken.getUid()).getIdentifier(); 332 synchronized (mLock) { 333 user = getFullUserRecordLocked(userId); 334 } 335 if (user == null) { 336 Log.w(TAG, "notifySession2Created: Ignore session of an unknown user"); 337 return; 338 } 339 user.addSession( 340 new Session2Record( 341 MediaCommunicationService.this, 342 user, 343 sessionToken, 344 mRecordExecutor), 345 pid); 346 } finally { 347 Binder.restoreCallingIdentity(token); 348 } 349 } 350 351 /** 352 * Returns if the controller's package is trusted (i.e. has either MEDIA_CONTENT_CONTROL 353 * permission or an enabled notification listener) 354 * 355 * @param controllerPackageName package name of the controller app 356 * @param controllerPid pid of the controller app 357 * @param controllerUid uid of the controller app 358 */ 359 @Override isTrusted(String controllerPackageName, int controllerPid, int controllerUid)360 public boolean isTrusted(String controllerPackageName, int controllerPid, 361 int controllerUid) { 362 final int uid = Binder.getCallingUid(); 363 final UserHandle callingUser = UserHandle.getUserHandleForUid(uid); 364 if (controllerUid < 0 365 || getPackageUidForUser(controllerPackageName, callingUser) != controllerUid) { 366 return false; 367 } 368 final long token = Binder.clearCallingIdentity(); 369 try { 370 // Don't perform check between controllerPackageName and controllerUid. 371 // When an (activity|service) runs on the another apps process by specifying 372 // android:process in the AndroidManifest.xml, then PID and UID would have the 373 // running process' information instead of the (activity|service) that has created 374 // MediaController. 375 // Note that we can use Context#getOpPackageName() instead of 376 // Context#getPackageName() for getting package name that matches with the PID/UID, 377 // but it doesn't tell which package has created the MediaController, so useless. 378 return hasMediaControlPermission(controllerPid, controllerUid) 379 || hasEnabledNotificationListener( 380 callingUser.getIdentifier(), controllerPackageName, controllerUid); 381 } finally { 382 Binder.restoreCallingIdentity(token); 383 } 384 } 385 386 @Override getSession2Tokens(int userId)387 public MediaParceledListSlice getSession2Tokens(int userId) { 388 final int pid = Binder.getCallingPid(); 389 final int uid = Binder.getCallingUid(); 390 final long token = Binder.clearCallingIdentity(); 391 392 try { 393 // Check that they can make calls on behalf of the user and get the final user id 394 int resolvedUserId = handleIncomingUser(pid, uid, userId, null); 395 ArrayList<Session2Token> result; 396 synchronized (mLock) { 397 result = getSession2TokensLocked(resolvedUserId); 398 } 399 MediaParceledListSlice parceledListSlice = new MediaParceledListSlice<>(result); 400 parceledListSlice.setInlineCountLimit(1); 401 return parceledListSlice; 402 } finally { 403 Binder.restoreCallingIdentity(token); 404 } 405 } 406 407 @Override dispatchMediaKeyEvent(String packageName, KeyEvent keyEvent, boolean asSystemService)408 public void dispatchMediaKeyEvent(String packageName, KeyEvent keyEvent, 409 boolean asSystemService) { 410 if (keyEvent == null || !isMediaSessionKey(keyEvent.getKeyCode())) { 411 Log.w(TAG, "Attempted to dispatch null or non-media key event."); 412 return; 413 } 414 415 final int pid = Binder.getCallingPid(); 416 final int uid = Binder.getCallingUid(); 417 final long token = Binder.clearCallingIdentity(); 418 try { 419 //TODO: Dispatch key event to media session 2 if required 420 mSessionManager.dispatchMediaKeyEvent(keyEvent, asSystemService); 421 } finally { 422 Binder.restoreCallingIdentity(token); 423 } 424 } 425 426 @Override 427 @RequiresPermission(MEDIA_CONTENT_CONTROL) registerCallback(@onNull IMediaCommunicationServiceCallback callback, @NonNull String packageName)428 public void registerCallback(@NonNull IMediaCommunicationServiceCallback callback, 429 @NonNull String packageName) throws RemoteException { 430 Objects.requireNonNull(callback, "callback should not be null"); 431 Objects.requireNonNull(packageName, "packageName should not be null"); 432 433 final int uid = Binder.getCallingUid(); 434 final int pid = Binder.getCallingPid(); 435 if (!hasMediaControlPermission(pid, uid)){ 436 throw new SecurityException("MEDIA_CONTENT_CONTROL permission is required to" 437 + " register MediaCommunicationServiceCallback"); 438 } 439 440 synchronized (mLock) { 441 if (findCallbackRecordLocked(callback) == null) { 442 CallbackRecord record = new CallbackRecord(callback, packageName, 443 uid, pid); 444 mCallbackRecords.add(record); 445 try { 446 callback.asBinder().linkToDeath(record, 0); 447 } catch (RemoteException e) { 448 Log.w(TAG, "Failed to register callback", e); 449 mCallbackRecords.remove(record); 450 } 451 } else { 452 Log.e(TAG, "registerCallback is called with already registered callback. " 453 + "packageName=" + packageName); 454 } 455 } 456 } 457 458 @Override unregisterCallback(IMediaCommunicationServiceCallback callback)459 public void unregisterCallback(IMediaCommunicationServiceCallback callback) 460 throws RemoteException { 461 synchronized (mLock) { 462 CallbackRecord existingRecord = findCallbackRecordLocked(callback); 463 if (existingRecord != null) { 464 mCallbackRecords.remove(existingRecord); 465 callback.asBinder().unlinkToDeath(existingRecord, 0); 466 } else { 467 Log.e(TAG, "unregisterCallback is called with unregistered callback."); 468 } 469 } 470 } 471 hasEnabledNotificationListener(int callingUserId, String controllerPackageName, int controllerUid)472 private boolean hasEnabledNotificationListener(int callingUserId, 473 String controllerPackageName, int controllerUid) { 474 int controllerUserId = UserHandle.getUserHandleForUid(controllerUid).getIdentifier(); 475 if (callingUserId != controllerUserId) { 476 // Enabled notification listener only works within the same user. 477 return false; 478 } 479 480 if (mNotificationManager.hasEnabledNotificationListener(controllerPackageName, 481 UserHandle.getUserHandleForUid(controllerUid))) { 482 return true; 483 } 484 if (DEBUG) { 485 Log.d(TAG, controllerPackageName + " (uid=" + controllerUid 486 + ") doesn't have an enabled notification listener"); 487 } 488 return false; 489 } 490 491 // Handles incoming user by checking whether the caller has permission to access the 492 // given user id's information or not. Permission is not necessary if the given user id is 493 // equal to the caller's user id, but if not, the caller needs to have the 494 // INTERACT_ACROSS_USERS_FULL permission. Otherwise, a security exception will be thrown. 495 // The return value will be the given user id, unless the given user id is 496 // UserHandle.CURRENT, which will return the ActivityManager.getCurrentUser() value instead. handleIncomingUser(int pid, int uid, int userId, String packageName)497 private int handleIncomingUser(int pid, int uid, int userId, String packageName) { 498 int callingUserId = UserHandle.getUserHandleForUid(uid).getIdentifier(); 499 if (userId == callingUserId) { 500 return userId; 501 } 502 503 boolean canInteractAcrossUsersFull = mContext.checkPermission( 504 INTERACT_ACROSS_USERS_FULL, pid, uid) == PackageManager.PERMISSION_GRANTED; 505 if (canInteractAcrossUsersFull) { 506 if (userId == UserHandle.CURRENT.getIdentifier()) { 507 return ActivityManager.getCurrentUser(); 508 } 509 return userId; 510 } 511 512 throw new SecurityException("Permission denied while calling from " + packageName 513 + " with user id: " + userId + "; Need to run as either the calling user id (" 514 + callingUserId + "), or with " + INTERACT_ACROSS_USERS_FULL + " permission"); 515 } 516 517 /** 518 * Return the UID associated with the given package name and user, or -1 if no such package 519 * is available to the caller. 520 */ getPackageUidForUser(@onNull String packageName, @NonNull UserHandle user)521 private int getPackageUidForUser(@NonNull String packageName, @NonNull UserHandle user) { 522 final PackageManager packageManager = mContext.getUser().equals(user) 523 ? mContext.getPackageManager() 524 : mContext.createContextAsUser(user, 0 /* flags */).getPackageManager(); 525 try { 526 return packageManager.getPackageUid(packageName, 0 /* flags */); 527 } catch (PackageManager.NameNotFoundException e) { 528 // package is not available to the caller 529 } 530 return -1; 531 } 532 } 533 534 final class CallbackRecord implements IBinder.DeathRecipient { 535 private final IMediaCommunicationServiceCallback mCallback; 536 private final String mPackageName; 537 private final int mUid; 538 private int mPid; 539 private final int mUserId; 540 CallbackRecord(IMediaCommunicationServiceCallback callback, String packageName, int uid, int pid)541 CallbackRecord(IMediaCommunicationServiceCallback callback, 542 String packageName, int uid, int pid) { 543 mCallback = callback; 544 mPackageName = packageName; 545 mUid = uid; 546 mPid = pid; 547 mUserId = (mContext.checkPermission( 548 INTERACT_ACROSS_USERS_FULL, pid, uid) == PackageManager.PERMISSION_GRANTED) 549 ? ALL.getIdentifier() : UserHandle.getUserHandleForUid(mUid).getIdentifier(); 550 } 551 552 @Override toString()553 public String toString() { 554 return "CallbackRecord[callback=" + mCallback + ", pkg=" + mPackageName 555 + ", uid=" + mUid + ", pid=" + mPid + "]"; 556 } 557 558 @Override binderDied()559 public void binderDied() { 560 synchronized (mLock) { 561 mCallbackRecords.remove(this); 562 } 563 } 564 } 565 566 final class FullUserRecord { 567 private final int mFullUserId; 568 private final SessionPriorityList mSessionPriorityList = new SessionPriorityList(); 569 FullUserRecord(int fullUserId)570 FullUserRecord(int fullUserId) { 571 mFullUserId = fullUserId; 572 } 573 addSession(Session2Record record, int pid)574 public void addSession(Session2Record record, int pid) { 575 mSessionPriorityList.addSession(record); 576 mHandler.post(() -> dispatchSession2Created(record.mSessionToken, pid)); 577 mHandler.post(() -> dispatchSession2Changed(mFullUserId)); 578 } 579 removeSession(Session2Record record)580 private void removeSession(Session2Record record) { 581 mSessionPriorityList.removeSession(record); 582 mHandler.post(() -> dispatchSession2Changed(mFullUserId)); 583 //TODO: Handle if the removed session was the media button session. 584 } 585 getFullUserId()586 public int getFullUserId() { 587 return mFullUserId; 588 } 589 getAllSession2Tokens()590 public List<Session2Token> getAllSession2Tokens() { 591 return mSessionPriorityList.getAllTokens(); 592 } 593 getSession2Tokens(int userId)594 public List<Session2Token> getSession2Tokens(int userId) { 595 return mSessionPriorityList.getTokensByUserId(userId); 596 } 597 destroyAllSessions()598 public void destroyAllSessions() { 599 mSessionPriorityList.destroyAllSessions(); 600 mHandler.post(() -> dispatchSession2Changed(mFullUserId)); 601 } 602 destroySessionsForUser(int userId)603 public void destroySessionsForUser(int userId) { 604 if (mSessionPriorityList.destroySessionsByUserId(userId)) { 605 mHandler.post(() -> dispatchSession2Changed(mFullUserId)); 606 } 607 } 608 containsSession(Session2Record session)609 public boolean containsSession(Session2Record session) { 610 return mSessionPriorityList.contains(session); 611 } 612 onPlaybackStateChanged(Session2Record session, boolean promotePriority)613 public void onPlaybackStateChanged(Session2Record session, boolean promotePriority) { 614 mSessionPriorityList.onPlaybackStateChanged(session, promotePriority); 615 } 616 } 617 618 static final class Session2Record { 619 final Session2Token mSessionToken; 620 final Object mSession2RecordLock = new Object(); 621 final WeakReference<MediaCommunicationService> mServiceRef; 622 final WeakReference<FullUserRecord> mFullUserRef; 623 @GuardedBy("mSession2RecordLock") 624 private final MediaController2 mController; 625 626 @GuardedBy("mSession2RecordLock") 627 boolean mIsConnected; 628 @GuardedBy("mSession2RecordLock") 629 private boolean mIsClosed; 630 631 //TODO: introduce policy (See MediaSessionPolicyProvider) Session2Record(MediaCommunicationService service, FullUserRecord fullUser, Session2Token token, Executor controllerExecutor)632 Session2Record(MediaCommunicationService service, FullUserRecord fullUser, 633 Session2Token token, Executor controllerExecutor) { 634 mServiceRef = new WeakReference<>(service); 635 mFullUserRef = new WeakReference<>(fullUser); 636 mSessionToken = token; 637 mController = new MediaController2.Builder(service.getContext(), token) 638 .setControllerCallback(controllerExecutor, new Controller2Callback()) 639 .build(); 640 } 641 getUserId()642 public int getUserId() { 643 return UserHandle.getUserHandleForUid(mSessionToken.getUid()).getIdentifier(); 644 } 645 getFullUser()646 public FullUserRecord getFullUser() { 647 return mFullUserRef.get(); 648 } 649 isClosed()650 public boolean isClosed() { 651 synchronized (mSession2RecordLock) { 652 return mIsClosed; 653 } 654 } 655 close()656 public void close() { 657 synchronized (mSession2RecordLock) { 658 mIsClosed = true; 659 mController.close(); 660 } 661 } 662 getSessionToken()663 public Session2Token getSessionToken() { 664 return mSessionToken; 665 } 666 checkPlaybackActiveState(boolean expected)667 public boolean checkPlaybackActiveState(boolean expected) { 668 synchronized (mSession2RecordLock) { 669 return mIsConnected && mController.isPlaybackActive() == expected; 670 } 671 } 672 673 private class Controller2Callback extends MediaController2.ControllerCallback { 674 @Override onConnected(MediaController2 controller, Session2CommandGroup allowedCommands)675 public void onConnected(MediaController2 controller, 676 Session2CommandGroup allowedCommands) { 677 if (DEBUG) { 678 Log.d(TAG, "connected to " + mSessionToken + ", allowed=" + allowedCommands); 679 } 680 synchronized (mSession2RecordLock) { 681 mIsConnected = true; 682 } 683 } 684 685 @Override onDisconnected(MediaController2 controller)686 public void onDisconnected(MediaController2 controller) { 687 if (DEBUG) { 688 Log.d(TAG, "disconnected from " + mSessionToken); 689 } 690 synchronized (mSession2RecordLock) { 691 mIsConnected = false; 692 // As per onDisconnected documentation, we do not need to call close() after 693 // onDisconnected is called. 694 mIsClosed = true; 695 } 696 MediaCommunicationService service = mServiceRef.get(); 697 if (service != null) { 698 service.removeSessionRecord(Session2Record.this); 699 } 700 } 701 702 @Override onPlaybackActiveChanged( @onNull MediaController2 controller, boolean playbackActive)703 public void onPlaybackActiveChanged( 704 @NonNull MediaController2 controller, 705 boolean playbackActive) { 706 if (DEBUG) { 707 Log.d(TAG, "playback active changed, " + mSessionToken + ", active=" 708 + playbackActive); 709 } 710 MediaCommunicationService service = mServiceRef.get(); 711 if (service != null) { 712 service.onSessionPlaybackStateChanged(Session2Record.this, playbackActive); 713 } 714 } 715 } 716 } 717 } 718