[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
This commit is contained in:
Kyunglyul Hyun
2021-02-09 17:50:50 +00:00
parent fdcfd2a2d5
commit 970adf0759
12 changed files with 829 additions and 104 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -26,6 +26,7 @@ package android.media {
}
public class MediaCommunicationManager {
method @NonNull public java.util.List<android.media.Session2Token> getSession2Tokens();
method @IntRange(from=1) public int getVersion();
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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<SessionCallbackRecord> 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.
* <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));
}
}
}
}

View File

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

View File

@@ -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);
}
}
}
}
}

View File

@@ -24760,7 +24760,7 @@ package android.media.session {
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);
}

View File

@@ -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<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,

View File

@@ -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<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 @@ 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);
}

View File

@@ -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<MediaSession.Token> 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<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 {