From 970adf075981baac19e510cf8f1e6836af1b2aa6 Mon Sep 17 00:00:00 2001 From: Kyunglyul Hyun Date: Tue, 9 Feb 2021 17:50:50 +0000 Subject: [PATCH] [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 --- .../media/IMediaCommunicationService.aidl | 10 + .../IMediaCommunicationServiceCallback.aidl | 26 + apex/media/framework/api/current.txt | 1 + .../framework/api/module-lib-current.txt | 10 + .../java/android/media/Controller2Link.java | 2 +- .../media/MediaCommunicationManager.java | 220 +++++++- .../java/android/media/MediaSession2.java | 12 +- .../media/MediaCommunicationService.java | 517 +++++++++++++++++- core/api/current.txt | 2 +- .../media/session/ISessionManager.aidl | 2 - .../media/session/MediaSessionManager.java | 52 +- .../server/media/MediaSessionService.java | 79 ++- 12 files changed, 829 insertions(+), 104 deletions(-) create mode 100644 apex/media/aidl/private/android/media/IMediaCommunicationServiceCallback.aidl diff --git a/apex/media/aidl/private/android/media/IMediaCommunicationService.aidl b/apex/media/aidl/private/android/media/IMediaCommunicationService.aidl index 3d50d14e1b83f..fb3172b8c764e 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 0000000000000..3d5321c9d7d8d --- /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 a2366df0660a4..1beef40b9e4f3 100644 --- a/apex/media/framework/api/current.txt +++ b/apex/media/framework/api/current.txt @@ -26,6 +26,7 @@ package android.media { } public class MediaCommunicationManager { + method @NonNull public java.util.List 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 ad9114fa23cfb..eb6397a1826bf 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); + } + 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 04185e79b0ad2..8eefec73194c2 100644 --- a/apex/media/framework/java/android/media/Controller2Link.java +++ b/apex/media/framework/java/android/media/Controller2Link.java @@ -26,7 +26,7 @@ import android.os.ResultReceiver; 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 e686076c871ca..9ec25fe48a2e5 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 @@ public class MediaCommunicationManager { private final Context mContext; private final IMediaCommunicationService mService; + private final Object mLock = new Object(); + private final CopyOnWriteArrayList mTokenCallbackRecords = + new CopyOnWriteArrayList<>(); + + @GuardedBy("mLock") + private MediaCommunicationServiceCallbackStub mCallbackStub; + /** * @hide */ @@ -64,4 +89,197 @@ public class MediaCommunicationManager { 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. + *

+ * 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 AndroidX + * Media2 session + * Library for consistent behavior across all devices. + *

+ * Gets a list of {@link Session2Token} with type {@link Session2Token#TYPE_SESSION} for the + * current user. + *

+ * 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 getSession2Tokens() { + return getSession2Tokens(UserHandle.myUserId()); + } + + /** + * Adds a callback to be notified when the list of active sessions changes. + *

+ * This requires the {@link android.Manifest.permission#MEDIA_CONTENT_CONTROL} permission be + * held by the calling app. + *

+ * @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 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 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 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 6560afedab0fa..6397ba3996f35 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.annotation.Nullable; 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 @@ public class MediaSession2 implements AutoCloseable { 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 @@ public class MediaSession2 implements AutoCloseable { 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 @@ public class MediaSession2 implements AutoCloseable { final ControllerInfo controllerInfo = new ControllerInfo( remoteUserInfo, - mSessionManager.isTrustedForMediaControl(remoteUserInfo), + mCommunicationManager.isTrustedForMediaControl(remoteUserInfo), controller, connectionHints); mCallbackExecutor.execute(() -> { @@ -608,8 +606,8 @@ public class MediaSession2 implements AutoCloseable { // 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 0468fdf30ba87..06de3f8d27d02 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 mUserRecords = new SparseArray<>(); + + private final Executor mRecordExecutor = Executors.newSingleThreadExecutor(); + @GuardedBy("mLock") + private final List 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 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 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 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 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 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 ea3f50ae90d8b..6bff005f6746e 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -24760,7 +24760,7 @@ package android.media.session { method @NonNull public java.util.List getActiveSessions(@Nullable android.content.ComponentName); method @NonNull public java.util.List 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 dc476b873786e..96bffee117ea6 100644 --- a/media/java/android/media/session/ISessionManager.aidl +++ b/media/java/android/media/session/ISessionManager.aidl @@ -38,9 +38,7 @@ import android.view.KeyEvent; interface ISessionManager { ISession createSession(String packageName, in ISessionCallback sessionCb, String tag, in Bundle sessionInfo, int userId); - void notifySession2Created(in Session2Token sessionToken); List 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 aa0f7fdd70d5f..98a13cfa6f3f5 100644 --- a/media/java/android/media/session/MediaSessionManager.java +++ b/media/java/android/media/session/MediaSessionManager.java @@ -25,9 +25,9 @@ import android.annotation.SystemApi; 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 final class MediaSessionManager { 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 @@ public final class MediaSessionManager { .getMediaServiceManager() .getMediaSessionServiceRegisterer() .get()); + mCommunicationManager = (MediaCommunicationManager) context + .getSystemService(Context.MEDIA_COMMUNICATION_SERVICE); } /** @@ -164,17 +167,11 @@ public final class MediaSessionManager { * {@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 @@ public final class MediaSessionManager { */ @NonNull public List getSession2Tokens() { - return getSession2Tokens(UserHandle.myUserId()); - } - - /** - * Gets a list of {@link Session2Token} with type {@link Session2Token#TYPE_SESSION} for the - * given user. - *

- * 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 getSession2Tokens(@NonNull UserHandle userHandle) { - Objects.requireNonNull(userHandle, "userHandle shouldn't be null"); - return getSession2Tokens(userHandle.getIdentifier()); - - } - - private List 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 @@ public final class MediaSessionManager { } 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 18f2d8450246e..23d84298b41e6 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.Context; 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 @@ public class MediaSessionService extends SystemService implements Monitor { 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 @@ public class MediaSessionService extends SystemService implements Monitor { 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 @@ -1138,31 +1170,6 @@ public class MediaSessionService extends SystemService implements Monitor { } } - @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 getSessions(ComponentName componentName, int userId) { final int pid = Binder.getCallingPid(); @@ -1184,26 +1191,6 @@ public class MediaSessionService extends SystemService implements Monitor { } } - @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 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 {