• 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.media.IMediaCommunicationService;
29 import android.media.IMediaCommunicationServiceCallback;
30 import android.media.MediaController2;
31 import android.media.MediaParceledListSlice;
32 import android.media.Session2CommandGroup;
33 import android.media.Session2Token;
34 import android.media.session.MediaSessionManager;
35 import android.os.Binder;
36 import android.os.Build;
37 import android.os.Handler;
38 import android.os.IBinder;
39 import android.os.Looper;
40 import android.os.Process;
41 import android.os.RemoteException;
42 import android.os.UserHandle;
43 import android.os.UserManager;
44 import android.util.Log;
45 import android.util.SparseArray;
46 import android.util.SparseIntArray;
47 import android.view.KeyEvent;
48 
49 import androidx.annotation.RequiresApi;
50 
51 import com.android.internal.annotations.GuardedBy;
52 import com.android.modules.annotation.MinSdk;
53 import com.android.server.SystemService;
54 
55 import java.lang.ref.WeakReference;
56 import java.util.ArrayList;
57 import java.util.List;
58 import java.util.Objects;
59 import java.util.concurrent.Executor;
60 import java.util.concurrent.Executors;
61 
62 /**
63  * A system service that manages {@link android.media.MediaSession2} creations
64  * and their ongoing media playback state.
65  * @hide
66  */
67 @MinSdk(Build.VERSION_CODES.S)
68 @RequiresApi(Build.VERSION_CODES.S)
69 public class MediaCommunicationService extends SystemService {
70     private static final String TAG = "MediaCommunicationSrv";
71     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
72 
73     final Context mContext;
74 
75     final Object mLock = new Object();
76     final Handler mHandler = new Handler(Looper.getMainLooper());
77 
78     @GuardedBy("mLock")
79     private final SparseIntArray mFullUserIds = new SparseIntArray();
80     @GuardedBy("mLock")
81     private final SparseArray<FullUserRecord> mUserRecords = new SparseArray<>();
82 
83     final Executor mRecordExecutor = Executors.newSingleThreadExecutor();
84     @GuardedBy("mLock")
85     final ArrayList<CallbackRecord> mCallbackRecords = new ArrayList<>();
86     final NotificationManager mNotificationManager;
87     MediaSessionManager mSessionManager;
88 
MediaCommunicationService(Context context)89     public MediaCommunicationService(Context context) {
90         super(context);
91         mContext = context;
92         mNotificationManager = context.getSystemService(NotificationManager.class);
93     }
94 
95     @Override
onStart()96     public void onStart() {
97         publishBinderService(Context.MEDIA_COMMUNICATION_SERVICE, new Stub());
98         updateUser();
99     }
100 
101     @Override
onBootPhase(int phase)102     public void onBootPhase(int phase) {
103         super.onBootPhase(phase);
104         switch (phase) {
105             // This ensures MediaSessionService is started
106             case PHASE_BOOT_COMPLETED:
107                 mSessionManager = mContext.getSystemService(MediaSessionManager.class);
108                 break;
109         }
110     }
111 
112     @Override
onUserStarting(@onNull TargetUser user)113     public void onUserStarting(@NonNull TargetUser user) {
114         if (DEBUG) Log.d(TAG, "onUserStarting: " + user);
115         updateUser();
116     }
117 
118     @Override
onUserSwitching(@ullable TargetUser from, @NonNull TargetUser to)119     public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
120         if (DEBUG) Log.d(TAG, "onUserSwitching: " + to);
121         updateUser();
122     }
123 
124     @Override
onUserStopped(@onNull TargetUser targetUser)125     public void onUserStopped(@NonNull TargetUser targetUser) {
126         int userId = targetUser.getUserHandle().getIdentifier();
127 
128         if (DEBUG) Log.d(TAG, "onUserStopped: " + userId);
129         synchronized (mLock) {
130             FullUserRecord user = getFullUserRecordLocked(userId);
131             if (user != null) {
132                 if (user.getFullUserId() == userId) {
133                     user.destroyAllSessions();
134                     mUserRecords.remove(userId);
135                 } else {
136                     user.destroySessionsForUser(userId);
137                 }
138             }
139         }
140         updateUser();
141     }
142 
143     @Nullable
findCallbackRecordLocked(@ullable IMediaCommunicationServiceCallback callback)144     CallbackRecord findCallbackRecordLocked(@Nullable IMediaCommunicationServiceCallback callback) {
145         if (callback == null) {
146             return null;
147         }
148         for (CallbackRecord record : mCallbackRecords) {
149             if (Objects.equals(callback.asBinder(), record.mCallback.asBinder())) {
150                 return record;
151             }
152         }
153         return null;
154     }
155 
getSession2TokensLocked(int userId)156     ArrayList<Session2Token> getSession2TokensLocked(int userId) {
157         ArrayList<Session2Token> list = new ArrayList<>();
158         if (userId == ALL.getIdentifier()) {
159             int size = mUserRecords.size();
160             for (int i = 0; i < size; i++) {
161                 list.addAll(mUserRecords.valueAt(i).getAllSession2Tokens());
162             }
163         } else {
164             FullUserRecord user = getFullUserRecordLocked(userId);
165             if (user != null) {
166                 list.addAll(user.getSession2Tokens(userId));
167             }
168         }
169         return list;
170     }
171 
getFullUserRecordLocked(int userId)172     private FullUserRecord getFullUserRecordLocked(int userId) {
173         int fullUserId = mFullUserIds.get(userId, -1);
174         if (fullUserId < 0) {
175             return null;
176         }
177         return mUserRecords.get(fullUserId);
178     }
179 
hasMediaControlPermission(int pid, int uid)180     private boolean hasMediaControlPermission(int pid, int uid) {
181         // Check if it's system server or has MEDIA_CONTENT_CONTROL.
182         // Note that system server doesn't have MEDIA_CONTENT_CONTROL, so we need extra
183         // check here.
184         if (uid == Process.SYSTEM_UID || mContext.checkPermission(
185                 android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
186                 == PackageManager.PERMISSION_GRANTED) {
187             return true;
188         } else if (DEBUG) {
189             Log.d(TAG, "uid(" + uid + ") hasn't granted MEDIA_CONTENT_CONTROL");
190         }
191         return false;
192     }
193 
updateUser()194     private void updateUser() {
195         UserManager manager = mContext.getSystemService(UserManager.class);
196         List<UserHandle> allUsers = manager.getUserHandles(/*excludeDying=*/false);
197 
198         synchronized (mLock) {
199             mFullUserIds.clear();
200             if (allUsers != null) {
201                 for (UserHandle user : allUsers) {
202                     UserHandle parent = manager.getProfileParent(user);
203                     if (parent != null) {
204                         mFullUserIds.put(user.getIdentifier(), parent.getIdentifier());
205                     } else {
206                         mFullUserIds.put(user.getIdentifier(), user.getIdentifier());
207                         if (mUserRecords.get(user.getIdentifier()) == null) {
208                             mUserRecords.put(user.getIdentifier(),
209                                     new FullUserRecord(user.getIdentifier()));
210                         }
211                     }
212                 }
213             }
214             // Ensure that the current full user exists.
215             int currentFullUserId = ActivityManager.getCurrentUser();
216             FullUserRecord currentFullUserRecord = mUserRecords.get(currentFullUserId);
217             if (currentFullUserRecord == null) {
218                 Log.w(TAG, "Cannot find FullUserInfo for the current user " + currentFullUserId);
219                 currentFullUserRecord = new FullUserRecord(currentFullUserId);
220                 mUserRecords.put(currentFullUserId, currentFullUserRecord);
221             }
222             mFullUserIds.put(currentFullUserId, currentFullUserId);
223         }
224     }
225 
dispatchSession2Created(Session2Token token)226     void dispatchSession2Created(Session2Token token) {
227         synchronized (mLock) {
228             for (CallbackRecord record : mCallbackRecords) {
229                 if (record.mUserId != ALL.getIdentifier()
230                         && record.mUserId != getUserHandleForUid(token.getUid()).getIdentifier()) {
231                     continue;
232                 }
233                 try {
234                     record.mCallback.onSession2Created(token);
235                 } catch (RemoteException e) {
236                     Log.w(TAG, "Failed to notify session2 token created " + record);
237                 }
238             }
239         }
240     }
241 
dispatchSession2Changed(int userId)242     void dispatchSession2Changed(int userId) {
243         ArrayList<Session2Token> allSession2Tokens;
244         ArrayList<Session2Token> userSession2Tokens;
245 
246         synchronized (mLock) {
247             allSession2Tokens = getSession2TokensLocked(ALL.getIdentifier());
248             userSession2Tokens = getSession2TokensLocked(userId);
249 
250             for (CallbackRecord record : mCallbackRecords) {
251                 if (record.mUserId == ALL.getIdentifier()) {
252                     try {
253                         MediaParceledListSlice<Session2Token> toSend =
254                                 new MediaParceledListSlice<>(allSession2Tokens);
255                         toSend.setInlineCountLimit(0);
256                         record.mCallback.onSession2Changed(toSend);
257                     } catch (RemoteException e) {
258                         Log.w(TAG, "Failed to notify session2 tokens changed " + record);
259                     }
260                 } else if (record.mUserId == userId) {
261                     try {
262                         MediaParceledListSlice<Session2Token> toSend =
263                                 new MediaParceledListSlice<>(userSession2Tokens);
264                         toSend.setInlineCountLimit(0);
265                         record.mCallback.onSession2Changed(toSend);
266                     } catch (RemoteException e) {
267                         Log.w(TAG, "Failed to notify session2 tokens changed " + record);
268                     }
269                 }
270             }
271         }
272     }
273 
onSessionDied(Session2Record session)274     void onSessionDied(Session2Record session) {
275         if (DEBUG) {
276             Log.d(TAG, "Destroying " + session);
277         }
278         if (session.isClosed()) {
279             Log.w(TAG, "Destroying already destroyed session. Ignoring.");
280             return;
281         }
282 
283         FullUserRecord user = session.getFullUser();
284         if (user != null) {
285             user.removeSession(session);
286         }
287         session.close();
288     }
289 
onSessionPlaybackStateChanged(Session2Record session, boolean promotePriority)290     void onSessionPlaybackStateChanged(Session2Record session, boolean promotePriority) {
291         FullUserRecord user = session.getFullUser();
292         if (user == null || !user.containsSession(session)) {
293             Log.d(TAG, "Unknown session changed playback state. Ignoring.");
294             return;
295         }
296         user.onPlaybackStateChanged(session, promotePriority);
297     }
298 
299 
isMediaSessionKey(int keyCode)300     static boolean isMediaSessionKey(int keyCode) {
301         switch (keyCode) {
302             case KeyEvent.KEYCODE_MEDIA_PLAY:
303             case KeyEvent.KEYCODE_MEDIA_PAUSE:
304             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
305             case KeyEvent.KEYCODE_MUTE:
306             case KeyEvent.KEYCODE_HEADSETHOOK:
307             case KeyEvent.KEYCODE_MEDIA_STOP:
308             case KeyEvent.KEYCODE_MEDIA_NEXT:
309             case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
310             case KeyEvent.KEYCODE_MEDIA_REWIND:
311             case KeyEvent.KEYCODE_MEDIA_RECORD:
312             case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
313                 return true;
314         }
315         return false;
316     }
317 
318     private class Stub extends IMediaCommunicationService.Stub {
319         @Override
notifySession2Created(Session2Token sessionToken)320         public void notifySession2Created(Session2Token sessionToken) {
321             final int pid = Binder.getCallingPid();
322             final int uid = Binder.getCallingUid();
323             final long token = Binder.clearCallingIdentity();
324 
325             try {
326                 if (DEBUG) {
327                     Log.d(TAG, "Session2 is created " + sessionToken);
328                 }
329                 if (uid != sessionToken.getUid()) {
330                     throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
331                             + " but actually=" + sessionToken.getUid());
332                 }
333                 FullUserRecord user;
334                 int userId = getUserHandleForUid(sessionToken.getUid()).getIdentifier();
335                 synchronized (mLock) {
336                     user = getFullUserRecordLocked(userId);
337                 }
338                 if (user == null) {
339                     Log.w(TAG, "notifySession2Created: Ignore session of an unknown user");
340                     return;
341                 }
342                 user.addSession(new Session2Record(MediaCommunicationService.this,
343                         user, sessionToken, mRecordExecutor));
344             } finally {
345                 Binder.restoreCallingIdentity(token);
346             }
347         }
348 
349         /**
350          * Returns if the controller's package is trusted (i.e. has either MEDIA_CONTENT_CONTROL
351          * permission or an enabled notification listener)
352          *
353          * @param controllerPackageName package name of the controller app
354          * @param controllerPid pid of the controller app
355          * @param controllerUid uid of the controller app
356          */
357         @Override
isTrusted(String controllerPackageName, int controllerPid, int controllerUid)358         public boolean isTrusted(String controllerPackageName, int controllerPid,
359                 int controllerUid) {
360             final int uid = Binder.getCallingUid();
361             final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
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                         userId, 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     final class CallbackRecord implements IBinder.DeathRecipient {
506         private final IMediaCommunicationServiceCallback mCallback;
507         private final String mPackageName;
508         private final int mUid;
509         private int mPid;
510         private final int mUserId;
511 
CallbackRecord(IMediaCommunicationServiceCallback callback, String packageName, int uid, int pid)512         CallbackRecord(IMediaCommunicationServiceCallback callback,
513                 String packageName, int uid, int pid) {
514             mCallback = callback;
515             mPackageName = packageName;
516             mUid = uid;
517             mPid = pid;
518             mUserId = (mContext.checkPermission(
519                     INTERACT_ACROSS_USERS_FULL, pid, uid) == PackageManager.PERMISSION_GRANTED)
520                     ? ALL.getIdentifier() : UserHandle.getUserHandleForUid(mUid).getIdentifier();
521         }
522 
523         @Override
toString()524         public String toString() {
525             return "CallbackRecord[callback=" + mCallback + ", pkg=" + mPackageName
526                     + ", uid=" + mUid + ", pid=" + mPid + "]";
527         }
528 
529         @Override
binderDied()530         public void binderDied() {
531             synchronized (mLock) {
532                 mCallbackRecords.remove(this);
533             }
534         }
535     }
536 
537     final class FullUserRecord {
538         private final int mFullUserId;
539         private final SessionPriorityList mSessionPriorityList = new SessionPriorityList();
540 
FullUserRecord(int fullUserId)541         FullUserRecord(int fullUserId) {
542             mFullUserId = fullUserId;
543         }
544 
addSession(Session2Record record)545         public void addSession(Session2Record record) {
546             mSessionPriorityList.addSession(record);
547             mHandler.post(() -> dispatchSession2Created(record.mSessionToken));
548             mHandler.post(() -> dispatchSession2Changed(mFullUserId));
549         }
550 
removeSession(Session2Record record)551         private void removeSession(Session2Record record) {
552             mSessionPriorityList.removeSession(record);
553             mHandler.post(() -> dispatchSession2Changed(mFullUserId));
554             //TODO: Handle if the removed session was the media button session.
555         }
556 
getFullUserId()557         public int getFullUserId() {
558             return mFullUserId;
559         }
560 
getAllSession2Tokens()561         public List<Session2Token> getAllSession2Tokens() {
562             return mSessionPriorityList.getAllTokens();
563         }
564 
getSession2Tokens(int userId)565         public List<Session2Token> getSession2Tokens(int userId) {
566             return mSessionPriorityList.getTokensByUserId(userId);
567         }
568 
destroyAllSessions()569         public void destroyAllSessions() {
570             mSessionPriorityList.destroyAllSessions();
571             mHandler.post(() -> dispatchSession2Changed(mFullUserId));
572         }
573 
destroySessionsForUser(int userId)574         public void destroySessionsForUser(int userId) {
575             if (mSessionPriorityList.destroySessionsByUserId(userId)) {
576                 mHandler.post(() -> dispatchSession2Changed(mFullUserId));
577             }
578         }
579 
containsSession(Session2Record session)580         public boolean containsSession(Session2Record session) {
581             return mSessionPriorityList.contains(session);
582         }
583 
onPlaybackStateChanged(Session2Record session, boolean promotePriority)584         public void onPlaybackStateChanged(Session2Record session, boolean promotePriority) {
585             mSessionPriorityList.onPlaybackStateChanged(session, promotePriority);
586         }
587     }
588 
589     static final class Session2Record {
590         final Session2Token mSessionToken;
591         final Object mSession2RecordLock = new Object();
592         final WeakReference<MediaCommunicationService> mServiceRef;
593         final WeakReference<FullUserRecord> mFullUserRef;
594         @GuardedBy("mSession2RecordLock")
595         private final MediaController2 mController;
596 
597         @GuardedBy("mSession2RecordLock")
598         boolean mIsConnected;
599         @GuardedBy("mSession2RecordLock")
600         private boolean mIsClosed;
601 
602         //TODO: introduce policy (See MediaSessionPolicyProvider)
Session2Record(MediaCommunicationService service, FullUserRecord fullUser, Session2Token token, Executor controllerExecutor)603         Session2Record(MediaCommunicationService service, FullUserRecord fullUser,
604                 Session2Token token, Executor controllerExecutor) {
605             mServiceRef = new WeakReference<>(service);
606             mFullUserRef = new WeakReference<>(fullUser);
607             mSessionToken = token;
608             mController = new MediaController2.Builder(service.getContext(), token)
609                     .setControllerCallback(controllerExecutor, new Controller2Callback())
610                     .build();
611         }
612 
getUserId()613         public int getUserId() {
614             return UserHandle.getUserHandleForUid(mSessionToken.getUid()).getIdentifier();
615         }
616 
getFullUser()617         public FullUserRecord getFullUser() {
618             return mFullUserRef.get();
619         }
620 
isClosed()621         public boolean isClosed() {
622             synchronized (mSession2RecordLock) {
623                 return mIsClosed;
624             }
625         }
626 
close()627         public void close() {
628             synchronized (mSession2RecordLock) {
629                 mIsClosed = true;
630                 mController.close();
631             }
632         }
633 
getSessionToken()634         public Session2Token getSessionToken() {
635             return mSessionToken;
636         }
637 
checkPlaybackActiveState(boolean expected)638         public boolean checkPlaybackActiveState(boolean expected) {
639             synchronized (mSession2RecordLock) {
640                 return mIsConnected && mController.isPlaybackActive() == expected;
641             }
642         }
643 
644         private class Controller2Callback extends MediaController2.ControllerCallback {
645             @Override
onConnected(MediaController2 controller, Session2CommandGroup allowedCommands)646             public void onConnected(MediaController2 controller,
647                     Session2CommandGroup allowedCommands) {
648                 if (DEBUG) {
649                     Log.d(TAG, "connected to " + mSessionToken + ", allowed=" + allowedCommands);
650                 }
651                 synchronized (mSession2RecordLock) {
652                     mIsConnected = true;
653                 }
654             }
655 
656             @Override
onDisconnected(MediaController2 controller)657             public void onDisconnected(MediaController2 controller) {
658                 if (DEBUG) {
659                     Log.d(TAG, "disconnected from " + mSessionToken);
660                 }
661                 synchronized (mSession2RecordLock) {
662                     mIsConnected = false;
663                 }
664                 MediaCommunicationService service = mServiceRef.get();
665                 if (service != null) {
666                     service.onSessionDied(Session2Record.this);
667                 }
668             }
669 
670             @Override
onPlaybackActiveChanged( @onNull MediaController2 controller, boolean playbackActive)671             public void onPlaybackActiveChanged(
672                     @NonNull MediaController2 controller,
673                     boolean playbackActive) {
674                 if (DEBUG) {
675                     Log.d(TAG, "playback active changed, " + mSessionToken + ", active="
676                             + playbackActive);
677                 }
678                 MediaCommunicationService service = mServiceRef.get();
679                 if (service != null) {
680                     service.onSessionPlaybackStateChanged(Session2Record.this, playbackActive);
681                 }
682             }
683         }
684     }
685 }
686