[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();