[Media ML] Let MCS manage MediaSession2

This CL adds
 - MediaCommunicationManager#notifySession2Created
 - MediaCommunicationManager#getSession2Tokens
, which replaces the same methods in MediaSessionManager

to let MediacommunicationService manage MediaSession2.

MediaSessionService gets notified of created MediaSession2 instances
by adding a callback to MCM.

Bug: 180417011
Test: atest MediaSessionManagerTest MediaCommunicationManagerTest
Change-Id: Ia5ffdcd15573d1223ca520cfa8eca3b976874118
diff --git a/apex/media/aidl/private/android/media/IMediaCommunicationService.aidl b/apex/media/aidl/private/android/media/IMediaCommunicationService.aidl
index 3d50d14..fb3172b 100644
--- a/apex/media/aidl/private/android/media/IMediaCommunicationService.aidl
+++ b/apex/media/aidl/private/android/media/IMediaCommunicationService.aidl
@@ -15,7 +15,17 @@
  */
 package android.media;
 
+import android.media.Session2Token;
+import android.media.IMediaCommunicationServiceCallback;
+import android.media.MediaParceledListSlice;
+
 /** {@hide} */
 interface IMediaCommunicationService {
+    void notifySession2Created(in Session2Token sessionToken);
+    boolean isTrusted(String controllerPackageName, int controllerPid, int controllerUid);
+    MediaParceledListSlice getSession2Tokens(int userId);
+
+    void registerCallback(IMediaCommunicationServiceCallback callback, String packageName);
+    void unregisterCallback(IMediaCommunicationServiceCallback callback);
 }
 
diff --git a/apex/media/aidl/private/android/media/IMediaCommunicationServiceCallback.aidl b/apex/media/aidl/private/android/media/IMediaCommunicationServiceCallback.aidl
new file mode 100644
index 0000000..3d5321c
--- /dev/null
+++ b/apex/media/aidl/private/android/media/IMediaCommunicationServiceCallback.aidl
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media;
+
+import android.media.Session2Token;
+import android.media.MediaParceledListSlice;
+
+/** {@hide} */
+interface IMediaCommunicationServiceCallback {
+    void onSession2Created(in Session2Token token);
+    void onSession2Changed(in MediaParceledListSlice tokens);
+}
+
diff --git a/apex/media/framework/api/current.txt b/apex/media/framework/api/current.txt
index a2366df..1beef40 100644
--- a/apex/media/framework/api/current.txt
+++ b/apex/media/framework/api/current.txt
@@ -26,6 +26,7 @@
   }
 
   public class MediaCommunicationManager {
+    method @NonNull public java.util.List<android.media.Session2Token> getSession2Tokens();
     method @IntRange(from=1) public int getVersion();
   }
 
diff --git a/apex/media/framework/api/module-lib-current.txt b/apex/media/framework/api/module-lib-current.txt
index ad9114f..eb6397a1 100644
--- a/apex/media/framework/api/module-lib-current.txt
+++ b/apex/media/framework/api/module-lib-current.txt
@@ -1,6 +1,16 @@
 // Signature format: 2.0
 package android.media {
 
+  public class MediaCommunicationManager {
+    method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void registerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaCommunicationManager.SessionCallback);
+    method public void unregisterSessionCallback(@NonNull android.media.MediaCommunicationManager.SessionCallback);
+  }
+
+  public static interface MediaCommunicationManager.SessionCallback {
+    method public default void onSession2TokenCreated(@NonNull android.media.Session2Token);
+    method public default void onSession2TokensChanged(@NonNull java.util.List<android.media.Session2Token>);
+  }
+
   public class MediaFrameworkInitializer {
     method public static void registerServiceWrappers();
     method public static void setMediaServiceManager(@NonNull android.media.MediaServiceManager);
diff --git a/apex/media/framework/java/android/media/Controller2Link.java b/apex/media/framework/java/android/media/Controller2Link.java
index 04185e7..8eefec7 100644
--- a/apex/media/framework/java/android/media/Controller2Link.java
+++ b/apex/media/framework/java/android/media/Controller2Link.java
@@ -26,7 +26,7 @@
 import java.util.Objects;
 
 /**
- * Handles incoming commands from {@link MediaSession2} to both {@link MediaController2}.
+ * Handles incoming commands from {@link MediaSession2} to {@link MediaController2}.
  * @hide
  */
 // @SystemApi
diff --git a/apex/media/framework/java/android/media/MediaCommunicationManager.java b/apex/media/framework/java/android/media/MediaCommunicationManager.java
index e686076..9ec25fe 100644
--- a/apex/media/framework/java/android/media/MediaCommunicationManager.java
+++ b/apex/media/framework/java/android/media/MediaCommunicationManager.java
@@ -15,18 +15,36 @@
  */
 package android.media;
 
+import static android.Manifest.permission.MEDIA_CONTENT_CONTROL;
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+import android.annotation.CallbackExecutor;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.content.Context;
+import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.media.MediaBrowserService;
+import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.modules.utils.build.SdkLevel;
 
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+
 /**
  * Provides support for interacting with {@link android.media.MediaSession2 MediaSession2s}
  * that applications have published to express their ongoing media playback state.
  */
-// TODO: Add notifySession2Created() and sendMessage().
 @SystemService(Context.MEDIA_COMMUNICATION_SERVICE)
 public class MediaCommunicationManager {
     private static final String TAG = "MediaCommunicationManager";
@@ -44,6 +62,13 @@
     private final Context mContext;
     private final IMediaCommunicationService mService;
 
+    private final Object mLock = new Object();
+    private final CopyOnWriteArrayList<SessionCallbackRecord> mTokenCallbackRecords =
+            new CopyOnWriteArrayList<>();
+
+    @GuardedBy("mLock")
+    private MediaCommunicationServiceCallbackStub mCallbackStub;
+
     /**
      * @hide
      */
@@ -64,4 +89,197 @@
     public @IntRange(from = 1) int getVersion() {
         return CURRENT_VERSION;
     }
+
+    /**
+     * Notifies that a new {@link MediaSession2} with type {@link Session2Token#TYPE_SESSION} is
+     * created.
+     * @param token newly created session2 token
+     * @hide
+     */
+    public void notifySession2Created(@NonNull Session2Token token) {
+        Objects.requireNonNull(token, "token shouldn't be null");
+        if (token.getType() != Session2Token.TYPE_SESSION) {
+            throw new IllegalArgumentException("token's type should be TYPE_SESSION");
+        }
+        try {
+            mService.notifySession2Created(token);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Checks whether the remote user is a trusted app.
+     * <p>
+     * An app is trusted if the app holds the
+     * {@link android.Manifest.permission#MEDIA_CONTENT_CONTROL} permission or has an enabled
+     * notification listener.
+     *
+     * @param userInfo The remote user info from either
+     *            {@link MediaSession#getCurrentControllerInfo()} or
+     *            {@link MediaBrowserService#getCurrentBrowserInfo()}.
+     * @return {@code true} if the remote user is trusted or {@code false} otherwise.
+     * @hide
+     */
+    public boolean isTrustedForMediaControl(@NonNull MediaSessionManager.RemoteUserInfo userInfo) {
+        Objects.requireNonNull(userInfo, "userInfo shouldn't be null");
+        if (userInfo.getPackageName() == null) {
+            return false;
+        }
+        try {
+            return mService.isTrusted(
+                    userInfo.getPackageName(), userInfo.getPid(), userInfo.getUid());
+        } catch (RemoteException e) {
+            Log.w(TAG, "Cannot communicate with the service.", e);
+        }
+        return false;
+    }
+
+    /**
+     * This API is not generally intended for third party application developers.
+     * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
+     * <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
+     * Library</a> for consistent behavior across all devices.
+     * <p>
+     * Gets a list of {@link Session2Token} with type {@link Session2Token#TYPE_SESSION} for the
+     * current user.
+     * <p>
+     * Although this API can be used without any restriction, each session owners can accept or
+     * reject your uses of {@link MediaSession2}.
+     *
+     * @return A list of {@link Session2Token}.
+     */
+    @NonNull
+    public List<Session2Token> getSession2Tokens() {
+        return getSession2Tokens(UserHandle.myUserId());
+    }
+
+    /**
+     * Adds a callback to be notified when the list of active sessions changes.
+     * <p>
+     * This requires the {@link android.Manifest.permission#MEDIA_CONTENT_CONTROL} permission be
+     * held by the calling app.
+     * </p>
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(MEDIA_CONTENT_CONTROL)
+    public void registerSessionCallback(@CallbackExecutor @NonNull Executor executor,
+            @NonNull SessionCallback callback) {
+        Objects.requireNonNull(executor, "executor must not be null");
+        Objects.requireNonNull(callback, "callback must not be null");
+
+        if (!mTokenCallbackRecords.addIfAbsent(
+                new SessionCallbackRecord(executor, callback))) {
+            Log.w(TAG, "registerSession2TokenCallback: Ignoring the same callback");
+            return;
+        }
+        synchronized (mLock) {
+            if (mCallbackStub == null) {
+                MediaCommunicationServiceCallbackStub callbackStub =
+                        new MediaCommunicationServiceCallbackStub();
+                try {
+                    mService.registerCallback(callbackStub, mContext.getPackageName());
+                    mCallbackStub = callbackStub;
+                } catch (RemoteException ex) {
+                    Log.e(TAG, "Failed to register callback.", ex);
+                }
+            }
+        }
+    }
+
+    /**
+     * Stops receiving active sessions updates on the specified callback.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void unregisterSessionCallback(@NonNull SessionCallback callback) {
+        if (!mTokenCallbackRecords.remove(
+                new SessionCallbackRecord(null, callback))) {
+            Log.w(TAG, "unregisterSession2TokenCallback: Ignoring an unknown callback.");
+            return;
+        }
+        synchronized (mLock) {
+            if (mCallbackStub != null && mTokenCallbackRecords.isEmpty()) {
+                try {
+                    mService.unregisterCallback(mCallbackStub);
+                } catch (RemoteException ex) {
+                    Log.e(TAG, "Failed to unregister callback.", ex);
+                }
+                mCallbackStub = null;
+            }
+        }
+    }
+
+    private List<Session2Token> getSession2Tokens(int userId) {
+        try {
+            MediaParceledListSlice slice = mService.getSession2Tokens(userId);
+            return slice == null ? Collections.emptyList() : slice.getList();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to get session tokens", e);
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * Callback for listening to changes to the sessions.
+     * @see #registerSessionCallback(Executor, SessionCallback)
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public interface SessionCallback {
+        /**
+         * Called when a new {@link MediaSession2 media session2} is created.
+         * @param token the newly created token
+         */
+        default void onSession2TokenCreated(@NonNull Session2Token token) {}
+
+        /**
+         * Called when {@link #getSession2Tokens() session tokens} are changed.
+         */
+        default void onSession2TokensChanged(@NonNull List<Session2Token> tokens) {}
+    }
+
+    private static final class SessionCallbackRecord {
+        public final Executor executor;
+        public final SessionCallback callback;
+
+        SessionCallbackRecord(Executor executor, SessionCallback callback) {
+            this.executor = executor;
+            this.callback = callback;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(callback);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof SessionCallbackRecord)) {
+                return false;
+            }
+            return Objects.equals(this.callback, ((SessionCallbackRecord) obj).callback);
+        }
+    }
+
+    class MediaCommunicationServiceCallbackStub extends IMediaCommunicationServiceCallback.Stub {
+        @Override
+        public void onSession2Created(Session2Token token) throws RemoteException {
+            for (SessionCallbackRecord record : mTokenCallbackRecords) {
+                record.executor.execute(() -> record.callback.onSession2TokenCreated(token));
+            }
+        }
+
+        @Override
+        public void onSession2Changed(MediaParceledListSlice tokens) throws RemoteException {
+            List<Session2Token> tokenList = tokens.getList();
+            for (SessionCallbackRecord record : mTokenCallbackRecords) {
+                record.executor.execute(() -> record.callback.onSession2TokensChanged(tokenList));
+            }
+        }
+    }
 }
diff --git a/apex/media/framework/java/android/media/MediaSession2.java b/apex/media/framework/java/android/media/MediaSession2.java
index 6560afe..6397ba3 100644
--- a/apex/media/framework/java/android/media/MediaSession2.java
+++ b/apex/media/framework/java/android/media/MediaSession2.java
@@ -32,7 +32,6 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.media.session.MediaSessionManager;
 import android.media.session.MediaSessionManager.RemoteUserInfo;
 import android.os.BadParcelableException;
 import android.os.Bundle;
@@ -87,7 +86,7 @@
     private final String mSessionId;
     private final PendingIntent mSessionActivity;
     private final Session2Token mSessionToken;
-    private final MediaSessionManager mSessionManager;
+    private final MediaCommunicationManager mCommunicationManager;
     private final Handler mResultHandler;
 
     //@GuardedBy("mLock")
@@ -115,8 +114,7 @@
         mSessionStub = new Session2Link(this);
         mSessionToken = new Session2Token(Process.myUid(), TYPE_SESSION, context.getPackageName(),
                 mSessionStub, tokenExtras);
-        mSessionManager = (MediaSessionManager) mContext.getSystemService(
-                Context.MEDIA_SESSION_SERVICE);
+        mCommunicationManager = mContext.getSystemService(MediaCommunicationManager.class);
         // NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
         mResultHandler = new Handler(context.getMainLooper());
         mClosed = false;
@@ -352,7 +350,7 @@
 
         final ControllerInfo controllerInfo = new ControllerInfo(
                 remoteUserInfo,
-                mSessionManager.isTrustedForMediaControl(remoteUserInfo),
+                mCommunicationManager.isTrustedForMediaControl(remoteUserInfo),
                 controller,
                 connectionHints);
         mCallbackExecutor.execute(() -> {
@@ -608,8 +606,8 @@
             // Notify framework about the newly create session after the constructor is finished.
             // Otherwise, framework may access the session before the initialization is finished.
             try {
-                MediaSessionManager manager = (MediaSessionManager) mContext.getSystemService(
-                        Context.MEDIA_SESSION_SERVICE);
+                MediaCommunicationManager manager =
+                        mContext.getSystemService(MediaCommunicationManager.class);
                 manager.notifySession2Created(session2.getToken());
             } catch (Exception e) {
                 session2.close();
diff --git a/apex/media/service/java/com/android/server/media/MediaCommunicationService.java b/apex/media/service/java/com/android/server/media/MediaCommunicationService.java
index 0468fdf..06de3f8 100644
--- a/apex/media/service/java/com/android/server/media/MediaCommunicationService.java
+++ b/apex/media/service/java/com/android/server/media/MediaCommunicationService.java
@@ -15,27 +15,538 @@
  */
 package com.android.server.media;
 
-import android.content.Context;
-import android.media.IMediaCommunicationService;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.os.UserHandle.ALL;
+import static android.os.UserHandle.getUserHandleForUid;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.media.IMediaCommunicationService;
+import android.media.IMediaCommunicationServiceCallback;
+import android.media.MediaController2;
+import android.media.MediaParceledListSlice;
+import android.media.Session2CommandGroup;
+import android.media.Session2Token;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
 import com.android.server.SystemService;
 
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
 /**
- * A system service that managers {@link android.media.MediaSession2} creations
+ * A system service that manages {@link android.media.MediaSession2} creations
  * and their ongoing media playback state.
  * @hide
  */
 public class MediaCommunicationService extends SystemService {
+    private static final String TAG = "MediaCommunicationService";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    final Context mContext;
+
+    private final Object mLock = new Object();
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    @GuardedBy("mLock")
+    private final SparseIntArray mFullUserIds = new SparseIntArray();
+    @GuardedBy("mLock")
+    private final SparseArray<FullUserRecord> mUserRecords = new SparseArray<>();
+
+    private final Executor mRecordExecutor = Executors.newSingleThreadExecutor();
+    @GuardedBy("mLock")
+    private final List<CallbackRecord> mCallbackRecords = new ArrayList<>();
+    final NotificationManager mNotificationManager;
 
     public MediaCommunicationService(Context context) {
         super(context);
+        mContext = context;
+        mNotificationManager = context.getSystemService(NotificationManager.class);
     }
 
     @Override
     public void onStart() {
         publishBinderService(Context.MEDIA_COMMUNICATION_SERVICE, new Stub());
+        updateUser();
+    }
+
+    @Override
+    public void onUserStarting(@NonNull TargetUser user) {
+        if (DEBUG) Log.d(TAG, "onUserStarting: " + user);
+        updateUser();
+    }
+
+    @Override
+    public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
+        if (DEBUG) Log.d(TAG, "onUserSwitching: " + to);
+        updateUser();
+    }
+
+    @Override
+    public void onUserStopped(@NonNull TargetUser targetUser) {
+        int userId = targetUser.getUserHandle().getIdentifier();
+
+        if (DEBUG) Log.d(TAG, "onUserStopped: " + userId);
+        synchronized (mLock) {
+            FullUserRecord user = getFullUserRecordLocked(userId);
+            if (user != null) {
+                if (user.getFullUserId() == userId) {
+                    user.destroySessionsForUserLocked(UserHandle.ALL.getIdentifier());
+                    mUserRecords.remove(userId);
+                } else {
+                    user.destroySessionsForUserLocked(userId);
+                }
+            }
+            updateUser();
+        }
+    }
+
+    @Nullable
+    CallbackRecord findCallbackRecordLocked(@Nullable IMediaCommunicationServiceCallback callback) {
+        if (callback == null) {
+            return null;
+        }
+        for (CallbackRecord record : mCallbackRecords) {
+            if (Objects.equals(callback.asBinder(), record.mCallback.asBinder())) {
+                return record;
+            }
+        }
+        return null;
+    }
+
+    private FullUserRecord getFullUserRecordLocked(int userId) {
+        int fullUserId = mFullUserIds.get(userId, -1);
+        if (fullUserId < 0) {
+            return null;
+        }
+        return mUserRecords.get(fullUserId);
+    }
+
+    private boolean hasMediaControlPermission(int pid, int uid) {
+        // Check if it's system server or has MEDIA_CONTENT_CONTROL.
+        // Note that system server doesn't have MEDIA_CONTENT_CONTROL, so we need extra
+        // check here.
+        if (uid == Process.SYSTEM_UID || mContext.checkPermission(
+                android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
+                == PackageManager.PERMISSION_GRANTED) {
+            return true;
+        } else if (DEBUG) {
+            Log.d(TAG, "uid(" + uid + ") hasn't granted MEDIA_CONTENT_CONTROL");
+        }
+        return false;
+    }
+
+    private void updateUser() {
+        UserManager manager = mContext.getSystemService(UserManager.class);
+        List<UserHandle> allUsers = manager.getUserHandles(/*excludeDying=*/false);
+
+        synchronized (mLock) {
+            mFullUserIds.clear();
+            if (allUsers != null) {
+                for (UserHandle user : allUsers) {
+                    UserHandle parent = manager.getProfileParent(user);
+                    if (parent != null) {
+                        mFullUserIds.put(user.getIdentifier(), parent.getIdentifier());
+                    } else {
+                        mFullUserIds.put(user.getIdentifier(), user.getIdentifier());
+                        if (mUserRecords.get(user.getIdentifier()) == null) {
+                            mUserRecords.put(user.getIdentifier(),
+                                    new FullUserRecord(user.getIdentifier()));
+                        }
+                    }
+                }
+            }
+            // Ensure that the current full user exists.
+            int currentFullUserId = ActivityManager.getCurrentUser();
+            FullUserRecord currentFullUserRecord = mUserRecords.get(currentFullUserId);
+            if (currentFullUserRecord == null) {
+                Log.w(TAG, "Cannot find FullUserInfo for the current user " + currentFullUserId);
+                currentFullUserRecord = new FullUserRecord(currentFullUserId);
+                mUserRecords.put(currentFullUserId, currentFullUserRecord);
+            }
+            mFullUserIds.put(currentFullUserId, currentFullUserId);
+        }
+    }
+
+    void dispatchSessionCreated(Session2Token token) {
+        for (CallbackRecord record : mCallbackRecords) {
+            if (record.mUserId != ALL.getIdentifier()
+                    && record.mUserId != getUserHandleForUid(token.getUid()).getIdentifier()) {
+                continue;
+            }
+            try {
+                record.mCallback.onSession2Created(token);
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    void onSessionDied(Session2Record record) {
+        synchronized (mLock) {
+            destroySessionLocked(record);
+        }
+    }
+
+    private void destroySessionLocked(Session2Record session) {
+        if (DEBUG) {
+            Log.d(TAG, "Destroying " + session);
+        }
+        if (session.isClosed()) {
+            Log.w(TAG, "Destroying already destroyed session. Ignoring.");
+            return;
+        }
+
+        FullUserRecord user = getFullUserRecordLocked(session.getUserId());
+
+        if (user != null) {
+            user.removeSession(session);
+        }
+
+        session.close();
     }
 
     private class Stub extends IMediaCommunicationService.Stub {
+        @Override
+        public void notifySession2Created(Session2Token sessionToken) {
+            final int pid = Binder.getCallingPid();
+            final int uid = Binder.getCallingUid();
+            final long token = Binder.clearCallingIdentity();
+
+            try {
+                if (DEBUG) {
+                    Log.d(TAG, "Session2 is created " + sessionToken);
+                }
+                if (uid != sessionToken.getUid()) {
+                    throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
+                            + " but actually=" + sessionToken.getUid());
+                }
+                synchronized (mLock) {
+                    int userId = getUserHandleForUid(sessionToken.getUid()).getIdentifier();
+                    FullUserRecord user = getFullUserRecordLocked(userId);
+                    if (user == null) {
+                        Log.w(TAG, "notifySession2Created: Ignore session of an unknown user");
+                        return;
+                    }
+                    user.addSession(new Session2Record(MediaCommunicationService.this,
+                            sessionToken, mRecordExecutor));
+                    mHandler.post(() -> dispatchSessionCreated(sessionToken));
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /**
+         * Returns if the controller's package is trusted (i.e. has either MEDIA_CONTENT_CONTROL
+         * permission or an enabled notification listener)
+         *
+         * @param controllerPackageName package name of the controller app
+         * @param controllerPid pid of the controller app
+         * @param controllerUid uid of the controller app
+         */
+        @Override
+        public boolean isTrusted(String controllerPackageName, int controllerPid,
+                int controllerUid) {
+            final int uid = Binder.getCallingUid();
+            final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+            final long token = Binder.clearCallingIdentity();
+            try {
+                // Don't perform check between controllerPackageName and controllerUid.
+                // When an (activity|service) runs on the another apps process by specifying
+                // android:process in the AndroidManifest.xml, then PID and UID would have the
+                // running process' information instead of the (activity|service) that has created
+                // MediaController.
+                // Note that we can use Context#getOpPackageName() instead of
+                // Context#getPackageName() for getting package name that matches with the PID/UID,
+                // but it doesn't tell which package has created the MediaController, so useless.
+                return hasMediaControlPermission(controllerPid, controllerUid)
+                        || hasEnabledNotificationListener(
+                        userId, controllerPackageName, controllerUid);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public MediaParceledListSlice getSession2Tokens(int userId) {
+            final int pid = Binder.getCallingPid();
+            final int uid = Binder.getCallingUid();
+            final long token = Binder.clearCallingIdentity();
+
+            try {
+                // Check that they can make calls on behalf of the user and get the final user id
+                int resolvedUserId = handleIncomingUser(pid, uid, userId, null);
+                List<Session2Token> result;
+                synchronized (mLock) {
+                    FullUserRecord user = getFullUserRecordLocked(userId);
+                    result = user.getSession2Tokens(resolvedUserId);
+                }
+                return new MediaParceledListSlice(result);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public void registerCallback(IMediaCommunicationServiceCallback callback,
+                String packageName) throws RemoteException {
+            Objects.requireNonNull(callback, "callback should not be null");
+            Objects.requireNonNull(packageName, "packageName should not be null");
+
+            synchronized (mLock) {
+                if (findCallbackRecordLocked(callback) == null) {
+
+                    CallbackRecord record = new CallbackRecord(callback, packageName,
+                            Binder.getCallingUid(), Binder.getCallingPid());
+                    mCallbackRecords.add(record);
+                    try {
+                        callback.asBinder().linkToDeath(record, 0);
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "Failed to register callback", e);
+                        mCallbackRecords.remove(record);
+                    }
+                } else {
+                    Log.e(TAG, "registerCallback is called with already registered callback. "
+                            + "packageName=" + packageName);
+                }
+            }
+        }
+
+        @Override
+        public void unregisterCallback(IMediaCommunicationServiceCallback callback)
+                throws RemoteException {
+            synchronized (mLock) {
+                CallbackRecord existingRecord = findCallbackRecordLocked(callback);
+                if (existingRecord != null) {
+                    mCallbackRecords.remove(existingRecord);
+                    callback.asBinder().unlinkToDeath(existingRecord, 0);
+                } else {
+                    Log.e(TAG, "unregisterCallback is called with unregistered callback.");
+                }
+            }
+        }
+
+        private boolean hasEnabledNotificationListener(int callingUserId,
+                String controllerPackageName, int controllerUid) {
+            int controllerUserId = UserHandle.getUserHandleForUid(controllerUid).getIdentifier();
+            if (callingUserId != controllerUserId) {
+                // Enabled notification listener only works within the same user.
+                return false;
+            }
+
+            if (mNotificationManager.hasEnabledNotificationListener(controllerPackageName,
+                    UserHandle.getUserHandleForUid(controllerUid))) {
+                return true;
+            }
+            if (DEBUG) {
+                Log.d(TAG, controllerPackageName + " (uid=" + controllerUid
+                        + ") doesn't have an enabled notification listener");
+            }
+            return false;
+        }
+
+        // Handles incoming user by checking whether the caller has permission to access the
+        // given user id's information or not. Permission is not necessary if the given user id is
+        // equal to the caller's user id, but if not, the caller needs to have the
+        // INTERACT_ACROSS_USERS_FULL permission. Otherwise, a security exception will be thrown.
+        // The return value will be the given user id, unless the given user id is
+        // UserHandle.CURRENT, which will return the ActivityManager.getCurrentUser() value instead.
+        private int handleIncomingUser(int pid, int uid, int userId, String packageName) {
+            int callingUserId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+            if (userId == callingUserId) {
+                return userId;
+            }
+
+            boolean canInteractAcrossUsersFull = mContext.checkPermission(
+                    INTERACT_ACROSS_USERS_FULL, pid, uid) == PackageManager.PERMISSION_GRANTED;
+            if (canInteractAcrossUsersFull) {
+                if (userId == UserHandle.CURRENT.getIdentifier()) {
+                    return ActivityManager.getCurrentUser();
+                }
+                return userId;
+            }
+
+            throw new SecurityException("Permission denied while calling from " + packageName
+                    + " with user id: " + userId + "; Need to run as either the calling user id ("
+                    + callingUserId + "), or with " + INTERACT_ACROSS_USERS_FULL + " permission");
+        }
+    }
+
+    final class CallbackRecord implements IBinder.DeathRecipient {
+        private final IMediaCommunicationServiceCallback mCallback;
+        private final String mPackageName;
+        private final int mUid;
+        private int mPid;
+        private final int mUserId;
+
+        CallbackRecord(IMediaCommunicationServiceCallback callback,
+                String packageName, int uid, int pid) {
+            mCallback = callback;
+            mPackageName = packageName;
+            mUid = uid;
+            mPid = pid;
+            mUserId = (mContext.checkPermission(
+                    INTERACT_ACROSS_USERS_FULL, pid, uid) == PackageManager.PERMISSION_GRANTED)
+                    ? ALL.getIdentifier() : UserHandle.getUserHandleForUid(mUid).getIdentifier();
+        }
+
+        @Override
+        public String toString() {
+            return "CallbackRecord[callback=" + mCallback + ", pkg=" + mPackageName
+                    + ", uid=" + mUid + ", pid=" + mPid + "]";
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mLock) {
+                mCallbackRecords.remove(this);
+            }
+        }
+    }
+
+    final class FullUserRecord {
+        private final int mFullUserId;
+        /** Sorted list of media sessions */
+        private final List<Session2Record> mSessionRecords = new ArrayList<>();
+
+        FullUserRecord(int fullUserId) {
+            mFullUserId = fullUserId;
+        }
+
+        public void addSession(Session2Record record) {
+            mSessionRecords.add(record);
+        }
+
+        public void removeSession(Session2Record record) {
+            mSessionRecords.remove(record);
+            //TODO: Handle if the removed session was the media button session.
+        }
+
+        public int getFullUserId() {
+            return mFullUserId;
+        }
+
+        public List<Session2Token> getSession2Tokens(int userId) {
+            return mSessionRecords.stream()
+                    .filter(record -> record.isActive()
+                            && (userId == UserHandle.ALL.getIdentifier()
+                                    || record.getUserId() == userId))
+                    .map(Session2Record::getSessionToken)
+                    .collect(Collectors.toList());
+        }
+
+        public void destroySessionsForUserLocked(int userId) {
+            synchronized (mLock) {
+                for (Session2Record record : mSessionRecords) {
+                    if (userId == UserHandle.ALL.getIdentifier()
+                            || record.getUserId() == userId) {
+                        destroySessionLocked(record);
+                    }
+                }
+            }
+        }
+    }
+
+    static final class Session2Record {
+        private final Session2Token mSessionToken;
+        private final Object mLock = new Object();
+        private final WeakReference<MediaCommunicationService> mServiceRef;
+        @GuardedBy("mLock")
+        private final MediaController2 mController;
+
+        @GuardedBy("mLock")
+        private boolean mIsConnected;
+        @GuardedBy("mLock")
+        private boolean mIsClosed;
+
+        Session2Record(MediaCommunicationService service, Session2Token token,
+                Executor controllerExecutor) {
+            mServiceRef = new WeakReference<>(service);
+            mSessionToken = token;
+            mController = new MediaController2.Builder(service.getContext(), token)
+                    .setControllerCallback(controllerExecutor, new Controller2Callback())
+                    .build();
+        }
+
+        public int getUserId() {
+            return UserHandle.getUserHandleForUid(mSessionToken.getUid()).getIdentifier();
+        }
+
+        public boolean isActive() {
+            synchronized (mLock) {
+                return mIsConnected;
+            }
+        }
+
+        public boolean isClosed() {
+            synchronized (mLock) {
+                return mIsClosed;
+            }
+        }
+
+        public void close() {
+            synchronized (mLock) {
+                mIsClosed = true;
+                // Call close regardless of the mIsConnected. This may be called when it's not yet
+                // connected.
+                mController.close();
+            }
+        }
+
+        public Session2Token getSessionToken() {
+            return mSessionToken;
+        }
+
+        private class Controller2Callback extends MediaController2.ControllerCallback {
+            @Override
+            public void onConnected(MediaController2 controller,
+                    Session2CommandGroup allowedCommands) {
+                if (DEBUG) {
+                    Log.d(TAG, "connected to " + mSessionToken + ", allowed=" + allowedCommands);
+                }
+                synchronized (mLock) {
+                    mIsConnected = true;
+                }
+                MediaCommunicationService service = mServiceRef.get();
+                if (service != null) {
+                    //TODO: notify session state changed
+                }
+            }
+
+            @Override
+            public void onDisconnected(MediaController2 controller) {
+                if (DEBUG) {
+                    Log.d(TAG, "disconnected from " + mSessionToken);
+                }
+                synchronized (mLock) {
+                    mIsConnected = false;
+                }
+                MediaCommunicationService service = mServiceRef.get();
+                if (service != null) {
+                    service.onSessionDied(Session2Record.this);
+                }
+            }
+        }
     }
 }
diff --git a/core/api/current.txt b/core/api/current.txt
index ea3f50a..6bff005 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -24760,7 +24760,7 @@
     method @NonNull public java.util.List<android.media.session.MediaController> getActiveSessions(@Nullable android.content.ComponentName);
     method @NonNull public java.util.List<android.media.Session2Token> getSession2Tokens();
     method public boolean isTrustedForMediaControl(@NonNull android.media.session.MediaSessionManager.RemoteUserInfo);
-    method public void notifySession2Created(@NonNull android.media.Session2Token);
+    method @Deprecated public void notifySession2Created(@NonNull android.media.Session2Token);
     method public void removeOnActiveSessionsChangedListener(@NonNull android.media.session.MediaSessionManager.OnActiveSessionsChangedListener);
     method public void removeOnSession2TokensChangedListener(@NonNull android.media.session.MediaSessionManager.OnSession2TokensChangedListener);
   }
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index dc476b8..96bffee 100644
--- a/media/java/android/media/session/ISessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -38,9 +38,7 @@
 interface ISessionManager {
     ISession createSession(String packageName, in ISessionCallback sessionCb, String tag,
             in Bundle sessionInfo, int userId);
-    void notifySession2Created(in Session2Token sessionToken);
     List<MediaSession.Token> getSessions(in ComponentName compName, int userId);
-    ParceledListSlice getSession2Tokens(int userId);
     void dispatchMediaKeyEvent(String packageName, boolean asSystemService, in KeyEvent keyEvent,
             boolean needWakeLock);
     boolean dispatchMediaKeyEventToSessionAsSystemService(String packageName,
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index aa0f7fd..98a13cf 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -25,9 +25,9 @@
 import android.annotation.SystemService;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.ParceledListSlice;
 import android.media.AudioManager;
 import android.media.IRemoteSessionCallback;
+import android.media.MediaCommunicationManager;
 import android.media.MediaFrameworkPlatformInitializer;
 import android.media.MediaSession2;
 import android.media.Session2Token;
@@ -84,6 +84,7 @@
     public static final int RESULT_MEDIA_KEY_HANDLED = 1;
 
     private final ISessionManager mService;
+    private final MediaCommunicationManager mCommunicationManager;
     private final OnMediaKeyEventDispatchedListenerStub mOnMediaKeyEventDispatchedListenerStub =
             new OnMediaKeyEventDispatchedListenerStub();
     private final OnMediaKeyEventSessionChangedListenerStub
@@ -128,6 +129,8 @@
                 .getMediaServiceManager()
                 .getMediaSessionServiceRegisterer()
                 .get());
+        mCommunicationManager = (MediaCommunicationManager) context
+                .getSystemService(Context.MEDIA_COMMUNICATION_SERVICE);
     }
 
     /**
@@ -164,17 +167,11 @@
      * {@link MediaSession2.Builder} instead.
      *
      * @param token newly created session2 token
+     * @deprecated Don't use this method. A new media session is notified automatically.
      */
+    @Deprecated
     public void notifySession2Created(@NonNull Session2Token token) {
-        Objects.requireNonNull(token, "token shouldn't be null");
-        if (token.getType() != Session2Token.TYPE_SESSION) {
-            throw new IllegalArgumentException("token's type should be TYPE_SESSION");
-        }
-        try {
-            mService.notifySession2Created(token);
-        } catch (RemoteException e) {
-            e.rethrowFromSystemServer();
-        }
+        // Does nothing
     }
 
     /**
@@ -255,37 +252,7 @@
      */
     @NonNull
     public List<Session2Token> getSession2Tokens() {
-        return getSession2Tokens(UserHandle.myUserId());
-    }
-
-    /**
-     * Gets a list of {@link Session2Token} with type {@link Session2Token#TYPE_SESSION} for the
-     * given user.
-     * <p>
-     * The calling application needs to hold the
-     * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission in order to
-     * retrieve session tokens for user ids that do not belong to current process.
-     *
-     * @param userHandle The user handle to fetch sessions for.
-     * @return A list of {@link Session2Token}
-     * @hide
-     */
-    @NonNull
-    @SuppressLint("UserHandle")
-    public List<Session2Token> getSession2Tokens(@NonNull UserHandle userHandle) {
-        Objects.requireNonNull(userHandle, "userHandle shouldn't be null");
-        return getSession2Tokens(userHandle.getIdentifier());
-
-    }
-
-    private List<Session2Token> getSession2Tokens(int userId) {
-        try {
-            ParceledListSlice slice = mService.getSession2Tokens(userId);
-            return slice == null ? new ArrayList<>() : slice.getList();
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to get session tokens", e);
-        }
-        return new ArrayList<>();
+        return mCommunicationManager.getSession2Tokens();
     }
 
     /**
@@ -534,8 +501,7 @@
         }
         if (shouldRegisterCallback) {
             try {
-                mService.registerRemoteSessionCallback(
-                        mRemoteSessionCallbackStub);
+                mService.registerRemoteSessionCallback(mRemoteSessionCallbackStub);
             } catch (RemoteException e) {
                 Log.e(TAG, "Failed to register remote volume controller callback", e);
             }
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 18f2d84..23d8429 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -40,10 +40,10 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.content.pm.ParceledListSlice;
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
 import android.media.IRemoteSessionCallback;
+import android.media.MediaCommunicationManager;
 import android.media.Session2Token;
 import android.media.session.IActiveSessionsListener;
 import android.media.session.IOnMediaKeyEventDispatchedListener;
@@ -151,6 +151,25 @@
     private MediaSessionPolicyProvider mCustomMediaSessionPolicyProvider;
     private MediaKeyDispatcher mCustomMediaKeyDispatcher;
 
+    private MediaCommunicationManager mCommunicationManager;
+    private final MediaCommunicationManager.SessionCallback mSession2TokenCallback =
+            new MediaCommunicationManager.SessionCallback() {
+                @Override
+                public void onSession2TokenCreated(Session2Token token) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Session2 is created " + token);
+                    }
+                    MediaSession2Record record = new MediaSession2Record(token,
+                            MediaSessionService.this, mRecordThread.getLooper(), 0);
+                    synchronized (mLock) {
+                        FullUserRecord user = getFullUserRecordLocked(record.getUserId());
+                        if (user != null) {
+                            user.mPriorityStack.addSession(record);
+                        }
+                    }
+                }
+            };
+
     public MediaSessionService(Context context) {
         super(context);
         mContext = context;
@@ -202,6 +221,19 @@
         mContext.registerReceiver(mNotificationListenerEnabledChangedReceiver, filter);
     }
 
+    @Override
+    public void onBootPhase(int phase) {
+        super.onBootPhase(phase);
+        switch (phase) {
+            // This ensures MediaCommunicationService is started
+            case PHASE_BOOT_COMPLETED:
+                mCommunicationManager = mContext.getSystemService(MediaCommunicationManager.class);
+                mCommunicationManager.registerSessionCallback(new HandlerExecutor(mHandler),
+                        mSession2TokenCallback);
+                break;
+        }
+    }
+
     private final BroadcastReceiver mNotificationListenerEnabledChangedReceiver =
             new BroadcastReceiver() {
                 @Override
@@ -1139,31 +1171,6 @@
         }
 
         @Override
-        public void notifySession2Created(Session2Token sessionToken) throws RemoteException {
-            final int pid = Binder.getCallingPid();
-            final int uid = Binder.getCallingUid();
-            final long token = Binder.clearCallingIdentity();
-            try {
-                if (DEBUG) {
-                    Log.d(TAG, "Session2 is created " + sessionToken);
-                }
-                if (uid != sessionToken.getUid()) {
-                    throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
-                            + " but actually=" + sessionToken.getUid());
-                }
-                MediaSession2Record record = new MediaSession2Record(sessionToken,
-                        MediaSessionService.this, mRecordThread.getLooper(), 0);
-                synchronized (mLock) {
-                    FullUserRecord user = getFullUserRecordLocked(record.getUserId());
-                    user.mPriorityStack.addSession(record);
-                }
-                // Do not immediately notify changes -- do so when framework can dispatch command
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
-        }
-
-        @Override
         public List<MediaSession.Token> getSessions(ComponentName componentName, int userId) {
             final int pid = Binder.getCallingPid();
             final int uid = Binder.getCallingUid();
@@ -1185,26 +1192,6 @@
         }
 
         @Override
-        public ParceledListSlice getSession2Tokens(int userId) {
-            final int pid = Binder.getCallingPid();
-            final int uid = Binder.getCallingUid();
-            final long token = Binder.clearCallingIdentity();
-
-            try {
-                // Check that they can make calls on behalf of the user and get the final user id
-                int resolvedUserId = handleIncomingUser(pid, uid, userId, null);
-                List<Session2Token> result;
-                synchronized (mLock) {
-                    FullUserRecord user = getFullUserRecordLocked(userId);
-                    result = user.mPriorityStack.getSession2Tokens(resolvedUserId);
-                }
-                return new ParceledListSlice(result);
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
-        }
-
-        @Override
         public void addSessionsListener(IActiveSessionsListener listener,
                 ComponentName componentName, int userId) throws RemoteException {
             final int pid = Binder.getCallingPid();