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