• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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