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