Allow to register multiple callbacks in MediaSessionManager
Bug: 126758528
Test: manually
Change-Id: I613593a88c115a5ac694b8c984f3ae36e9cc1c78
Merged-In: I613593a88c115a5ac694b8c984f3ae36e9cc1c78
(cherry picked from commit 14ddf75c473a6cea391bd74d1638d9d28bcc9b01)
diff --git a/api/system-current.txt b/api/system-current.txt
index ebfd55e..b6512de 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -3864,8 +3864,18 @@
package android.media.session {
public final class MediaSessionManager {
+ method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void registerCallback(@NonNull android.media.session.MediaSessionManager.Callback, @Nullable android.os.Handler);
method @RequiresPermission(android.Manifest.permission.SET_MEDIA_KEY_LISTENER) public void setOnMediaKeyListener(android.media.session.MediaSessionManager.OnMediaKeyListener, @Nullable android.os.Handler);
method @RequiresPermission(android.Manifest.permission.SET_VOLUME_KEY_LONG_PRESS_LISTENER) public void setOnVolumeKeyLongPressListener(android.media.session.MediaSessionManager.OnVolumeKeyLongPressListener, @Nullable android.os.Handler);
+ method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void unregisterCallback(@NonNull android.media.session.MediaSessionManager.Callback);
+ }
+
+ public abstract static class MediaSessionManager.Callback {
+ ctor public MediaSessionManager.Callback();
+ method public abstract void onAddressedPlayerChanged(android.media.session.MediaSession.Token);
+ method public abstract void onAddressedPlayerChanged(android.content.ComponentName);
+ method public abstract void onMediaKeyEventDispatched(android.view.KeyEvent, android.media.session.MediaSession.Token);
+ method public abstract void onMediaKeyEventDispatched(android.view.KeyEvent, android.content.ComponentName);
}
public static interface MediaSessionManager.OnMediaKeyListener {
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index a67a37e..01e6ed5 100644
--- a/media/java/android/media/session/ISessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -62,7 +62,8 @@
// For PhoneWindowManager to precheck media keys
boolean isGlobalPriorityActive();
- void setCallback(in ICallback callback);
+ void registerCallback(in ICallback callback);
+ void unregisterCallback(in ICallback callback);
void setOnVolumeKeyLongPressListener(in IOnVolumeKeyLongPressListener listener);
void setOnMediaKeyListener(in IOnMediaKeyListener listener);
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index 9685c4b..ecfb818 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -46,7 +46,9 @@
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
/**
@@ -72,6 +74,7 @@
* @hide
*/
public static final int RESULT_MEDIA_KEY_HANDLED = 1;
+ private final ISessionManager mService;
private final Object mLock = new Object();
@GuardedBy("mLock")
@@ -80,13 +83,21 @@
@GuardedBy("mLock")
private final ArrayMap<OnSession2TokensChangedListener, Session2TokensChangedWrapper>
mSession2TokensListeners = new ArrayMap<>();
- private final ISessionManager mService;
+ @GuardedBy("mLock")
+ private final CallbackStub mCbStub = new CallbackStub();
+ @GuardedBy("mLock")
+ private final Map<Callback, Handler> mCallbacks = new HashMap<>();
+ @GuardedBy("mLock")
+ private MediaSession.Token mCurMediaButtonSession;
+ @GuardedBy("mLock")
+ private ComponentName mCurMediaButtonReceiver;
private Context mContext;
-
- private CallbackImpl mCallback;
private OnVolumeKeyLongPressListenerImpl mOnVolumeKeyLongPressListener;
private OnMediaKeyListenerImpl mOnMediaKeyListener;
+ // TODO: Remove mLegacyCallback once Bluetooth app stop calling setCallback() method.
+ @GuardedBy("mLock")
+ private Callback mLegacyCallback;
/**
* @hide
@@ -752,18 +763,71 @@
* if the callback should be invoked on the calling thread's looper.
* @hide
*/
+ // TODO: Remove this method once Bluetooth app stop calling it.
public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
synchronized (mLock) {
+ if (mLegacyCallback != null) {
+ unregisterCallback(mLegacyCallback);
+ }
+ mLegacyCallback = callback;
+ if (callback != null) {
+ registerCallback(callback, handler);
+ }
+ }
+ }
+
+ /**
+ * Register a {@link Callback}.
+ *
+ * @param callback A {@link Callback}.
+ * @param handler The handler on which the callback should be invoked, or {@code null}
+ * if the callback should be invoked on the calling thread's looper.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(value = android.Manifest.permission.MEDIA_CONTENT_CONTROL)
+ public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
+ if (callback == null) {
+ throw new NullPointerException("callback shouldn't be null");
+ }
+ synchronized (mLock) {
try {
- if (callback == null) {
- mCallback = null;
- mService.setCallback(null);
- } else {
- if (handler == null) {
- handler = new Handler();
- }
- mCallback = new CallbackImpl(callback, handler);
- mService.setCallback(mCallback);
+ if (handler == null) {
+ handler = new Handler();
+ }
+ mCallbacks.put(callback, handler);
+ if (mCurMediaButtonSession != null) {
+ handler.post(() -> callback.onAddressedPlayerChanged(mCurMediaButtonSession));
+ } else if (mCurMediaButtonReceiver != null) {
+ handler.post(() -> callback.onAddressedPlayerChanged(mCurMediaButtonReceiver));
+ }
+
+ if (mCallbacks.size() == 1) {
+ mService.registerCallback(mCbStub);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to set media key callback", e);
+ }
+ }
+ }
+
+ /**
+ * Unregister a {@link Callback}.
+ *
+ * @param callback A {@link Callback}.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(value = android.Manifest.permission.MEDIA_CONTENT_CONTROL)
+ public void unregisterCallback(@NonNull Callback callback) {
+ if (callback == null) {
+ throw new NullPointerException("callback shouldn't be null");
+ }
+ synchronized (mLock) {
+ try {
+ mCallbacks.remove(callback);
+ if (mCallbacks.size() == 0) {
+ mService.unregisterCallback(mCbStub);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to set media key callback", e);
@@ -835,6 +899,7 @@
* receive media key events.
* @hide
*/
+ @SystemApi
public static abstract class Callback {
/**
* Called when a media key event is dispatched to the media session
@@ -861,7 +926,7 @@
/**
* Called when the addressed player is changed to a media session.
* <p>One of the {@ #onAddressedPlayerChanged} will be also called immediately after
- * {@link #setCallback} if the addressed player exists.
+ * {@link #registerCallback} if the addressed player exists.
*
* @param sessionToken The media session's token.
*/
@@ -870,7 +935,7 @@
/**
* Called when the addressed player is changed to the media button receiver.
* <p>One of the {@ #onAddressedPlayerChanged} will be also called immediately after
- * {@link #setCallback} if the addressed player exists.
+ * {@link #registerCallback} if the addressed player exists.
*
* @param mediaButtonReceiver The media button receiver.
*/
@@ -1076,56 +1141,52 @@
}
}
- private static final class CallbackImpl extends ICallback.Stub {
- private final Callback mCallback;
- private final Handler mHandler;
-
- public CallbackImpl(@NonNull Callback callback, @NonNull Handler handler) {
- mCallback = callback;
- mHandler = handler;
- }
+ private final class CallbackStub extends ICallback.Stub {
@Override
public void onMediaKeyEventDispatchedToMediaSession(KeyEvent event,
MediaSession.Token sessionToken) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mCallback.onMediaKeyEventDispatched(event, sessionToken);
+ synchronized (mLock) {
+ for (Map.Entry<Callback, Handler> e : mCallbacks.entrySet()) {
+ e.getValue().post(
+ () -> e.getKey().onMediaKeyEventDispatched(event, sessionToken));
}
- });
+ }
}
@Override
public void onMediaKeyEventDispatchedToMediaButtonReceiver(KeyEvent event,
ComponentName mediaButtonReceiver) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mCallback.onMediaKeyEventDispatched(event, mediaButtonReceiver);
+ synchronized (mLock) {
+ for (Map.Entry<Callback, Handler> e : mCallbacks.entrySet()) {
+ e.getValue().post(
+ () -> e.getKey().onMediaKeyEventDispatched(event, mediaButtonReceiver));
}
- });
+ }
}
@Override
public void onAddressedPlayerChangedToMediaSession(MediaSession.Token sessionToken) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mCallback.onAddressedPlayerChanged(sessionToken);
+ synchronized (mLock) {
+ mCurMediaButtonSession = sessionToken;
+ mCurMediaButtonReceiver = null;
+ for (Map.Entry<Callback, Handler> e : mCallbacks.entrySet()) {
+ e.getValue().post(() -> e.getKey().onAddressedPlayerChanged(sessionToken));
}
- });
+ }
}
@Override
public void onAddressedPlayerChangedToMediaButtonReceiver(
ComponentName mediaButtonReceiver) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mCallback.onAddressedPlayerChanged(mediaButtonReceiver);
+ synchronized (mLock) {
+ mCurMediaButtonSession = null;
+ mCurMediaButtonReceiver = mediaButtonReceiver;
+ for (Map.Entry<Callback, Handler> e : mCallbacks.entrySet()) {
+ e.getValue().post(() -> e.getKey().onAddressedPlayerChanged(
+ mediaButtonReceiver));
}
- });
+ }
}
}
}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 5bd4b20..c05655a 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -92,6 +92,7 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
/**
@@ -748,6 +749,8 @@
private final int mFullUserId;
private final MediaSessionStack mPriorityStack;
+ private final HashMap<IBinder, CallbackRecord> mCallbacks = new HashMap<>();
+
private PendingIntent mLastMediaButtonReceiver;
private ComponentName mRestoredMediaButtonReceiver;
private int mRestoredMediaButtonReceiverComponentType;
@@ -761,7 +764,6 @@
private IOnMediaKeyListener mOnMediaKeyListener;
private int mOnMediaKeyListenerUid;
- private ICallback mCallback;
FullUserRecord(int fullUserId) {
mFullUserId = fullUserId;
@@ -793,6 +795,24 @@
}
}
+ public void registerCallbackLocked(ICallback callback, int uid) {
+ IBinder cbBinder = callback.asBinder();
+ CallbackRecord cr = new CallbackRecord(callback, uid);
+ mCallbacks.put(cbBinder, cr);
+ try {
+ cbBinder.linkToDeath(cr, 0);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to register callback", e);
+ mCallbacks.remove(cbBinder);
+ }
+ }
+
+ public void unregisterCallbackLocked(ICallback callback) {
+ IBinder cbBinder = callback.asBinder();
+ CallbackRecord cr = mCallbacks.remove(cbBinder);
+ cbBinder.unlinkToDeath(cr, 0);
+ }
+
public void dumpLocked(PrintWriter pw, String prefix) {
pw.print(prefix + "Record for full_user=" + mFullUserId);
// Dump managed profile user ids associated with this user.
@@ -811,7 +831,10 @@
pw.println(indent + "Media key listener: " + mOnMediaKeyListener);
pw.println(indent + "Media key listener package: "
+ getCallingPackageName(mOnMediaKeyListenerUid));
- pw.println(indent + "Callback: " + mCallback);
+ pw.println(indent + "Callbacks: registered " + mCallbacks.size() + " callback(s)");
+ for (CallbackRecord cr : mCallbacks.values()) {
+ pw.println(indent + " from " + getCallingPackageName(cr.uid));
+ }
pw.println(indent + "Last MediaButtonReceiver: " + mLastMediaButtonReceiver);
pw.println(indent + "Restored MediaButtonReceiver: " + mRestoredMediaButtonReceiver);
pw.println(indent + "Restored MediaButtonReceiverComponentType: "
@@ -871,21 +894,18 @@
mFullUserId);
}
- private void pushAddressedPlayerChangedLocked() {
- if (mCallback == null) {
- return;
- }
+ private void pushAddressedPlayerChangedLocked(ICallback callback) {
try {
MediaSessionRecord mediaButtonSession = getMediaButtonSessionLocked();
if (mediaButtonSession != null) {
- mCallback.onAddressedPlayerChangedToMediaSession(
+ callback.onAddressedPlayerChangedToMediaSession(
mediaButtonSession.getSessionToken());
} else if (mCurrentFullUserRecord.mLastMediaButtonReceiver != null) {
- mCallback.onAddressedPlayerChangedToMediaButtonReceiver(
+ callback.onAddressedPlayerChangedToMediaButtonReceiver(
mCurrentFullUserRecord.mLastMediaButtonReceiver
.getIntent().getComponent());
} else if (mCurrentFullUserRecord.mRestoredMediaButtonReceiver != null) {
- mCallback.onAddressedPlayerChangedToMediaButtonReceiver(
+ callback.onAddressedPlayerChangedToMediaButtonReceiver(
mCurrentFullUserRecord.mRestoredMediaButtonReceiver);
}
} catch (RemoteException e) {
@@ -893,6 +913,12 @@
}
}
+ private void pushAddressedPlayerChangedLocked() {
+ for (CallbackRecord cr : mCallbacks.values()) {
+ pushAddressedPlayerChangedLocked(cr.callback);
+ }
+ }
+
private MediaSessionRecord getMediaButtonSessionLocked() {
return isGlobalPriorityActiveLocked()
? mGlobalPrioritySession : mPriorityStack.getMediaButtonSession();
@@ -926,6 +952,23 @@
// Pick legacy behavior for BroadcastReceiver or unknown.
return COMPONENT_TYPE_BROADCAST;
}
+
+ final class CallbackRecord implements IBinder.DeathRecipient {
+ public final ICallback callback;
+ public final int uid;
+
+ CallbackRecord(ICallback callback, int uid) {
+ this.callback = callback;
+ this.uid = uid;
+ }
+
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ mCallbacks.remove(callback.asBinder());
+ }
+ }
+ }
}
final class SessionsListenerRecord implements IBinder.DeathRecipient {
@@ -1305,44 +1348,53 @@
}
@Override
- public void setCallback(ICallback callback) {
+ public void registerCallback(final ICallback callback) {
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(uid);
final long token = Binder.clearCallingIdentity();
try {
- if (!UserHandle.isSameApp(uid, Process.BLUETOOTH_UID)) {
- throw new SecurityException("Only Bluetooth service processes can set"
- + " Callback");
+ if (!hasMediaControlPermission(pid, uid)) {
+ throw new SecurityException("MEDIA_CONTENT_CONTROL permission is required to"
+ + " register Callback");
}
synchronized (mLock) {
- int userId = UserHandle.getUserId(uid);
FullUserRecord user = getFullUserRecordLocked(userId);
if (user == null || user.mFullUserId != userId) {
- Log.w(TAG, "Only the full user can set the callback"
+ Log.w(TAG, "Only the full user can register the callback"
+ ", userId=" + userId);
return;
}
- user.mCallback = callback;
- Log.d(TAG, "The callback " + user.mCallback
- + " is set by " + getCallingPackageName(uid));
- if (user.mCallback == null) {
+ user.registerCallbackLocked(callback, uid);
+ Log.d(TAG, "The callback (" + callback.asBinder()
+ + ") is registered by " + getCallingPackageName(uid));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void unregisterCallback(final ICallback callback) {
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(uid);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (!hasMediaControlPermission(pid, uid)) {
+ throw new SecurityException("MEDIA_CONTENT_CONTROL permission is required to"
+ + " unregister Callback");
+ }
+ synchronized (mLock) {
+ FullUserRecord user = getFullUserRecordLocked(userId);
+ if (user == null || user.mFullUserId != userId) {
+ Log.w(TAG, "Only the full user can unregister the callback"
+ + ", userId=" + userId);
return;
}
- try {
- user.mCallback.asBinder().linkToDeath(
- new IBinder.DeathRecipient() {
- @Override
- public void binderDied() {
- synchronized (mLock) {
- user.mCallback = null;
- }
- }
- }, 0);
- user.pushAddressedPlayerChangedLocked();
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to set callback", e);
- user.mCallback = null;
- }
+ user.unregisterCallbackLocked(callback);
+ Log.d(TAG, "The callback (" + callback.asBinder()
+ + ") is unregistered by " + getCallingPackageName(uid));
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -1771,6 +1823,7 @@
public boolean isTrusted(String controllerPackageName, int controllerPid, int controllerUid)
throws RemoteException {
final int uid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(uid);
final long token = Binder.clearCallingIdentity();
try {
// Don't perform sanity check between controllerPackageName and controllerUid.
@@ -1781,8 +1834,8 @@
// 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(UserHandle.getUserId(uid), controllerPackageName,
- controllerPid, controllerUid);
+ return hasMediaControlPermission(controllerPid, controllerUid)
+ || hasEnabledNotificationListener(userId, controllerPackageName);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1808,13 +1861,7 @@
return resolvedUserId;
}
- private boolean hasMediaControlPermission(int resolvedUserId, String packageName,
- int pid, int uid) throws RemoteException {
- // Allow API calls from the System UI and Settings
- if (hasStatusBarServicePermission(pid, uid)) {
- return true;
- }
-
+ 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.
@@ -1823,11 +1870,15 @@
== PackageManager.PERMISSION_GRANTED) {
return true;
} else if (DEBUG) {
- Log.d(TAG, packageName + " (uid=" + uid + ") hasn't granted MEDIA_CONTENT_CONTROL");
+ Log.d(TAG, "uid(" + uid + ") hasn't granted MEDIA_CONTENT_CONTROL");
}
+ return false;
+ }
+ private boolean hasEnabledNotificationListener(int resolvedUserId, String packageName)
+ throws RemoteException {
// You may not access another user's content as an enabled listener.
- final int userId = UserHandle.getUserId(uid);
+ final int userId = UserHandle.getUserId(resolvedUserId);
if (resolvedUserId != userId) {
return false;
}
@@ -1845,7 +1896,7 @@
}
}
if (DEBUG) {
- Log.d(TAG, packageName + " (uid=" + uid + ") doesn't have an enabled "
+ Log.d(TAG, packageName + " (uid=" + resolvedUserId + ") doesn't have an enabled "
+ "notification listener");
}
return false;
@@ -1950,13 +2001,14 @@
session.sendMediaButton(packageName, pid, uid, asSystemService, keyEvent,
needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1,
mKeyEventReceiver);
- if (mCurrentFullUserRecord.mCallback != null) {
- try {
- mCurrentFullUserRecord.mCallback.onMediaKeyEventDispatchedToMediaSession(
+ try {
+ for (FullUserRecord.CallbackRecord cr
+ : mCurrentFullUserRecord.mCallbacks.values()) {
+ cr.callback.onMediaKeyEventDispatchedToMediaSession(
keyEvent, session.getSessionToken());
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to send callback", e);
}
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to send callback", e);
}
} else if (mCurrentFullUserRecord.mLastMediaButtonReceiver != null
|| mCurrentFullUserRecord.mRestoredMediaButtonReceiver != null) {
@@ -1980,12 +2032,12 @@
receiver.send(mContext,
needWakeLock ? mKeyEventReceiver.mLastTimeoutId : -1,
mediaButtonIntent, mKeyEventReceiver, mHandler);
- if (mCurrentFullUserRecord.mCallback != null) {
- ComponentName componentName = mCurrentFullUserRecord
- .mLastMediaButtonReceiver.getIntent().getComponent();
- if (componentName != null) {
- mCurrentFullUserRecord.mCallback
- .onMediaKeyEventDispatchedToMediaButtonReceiver(
+ ComponentName componentName = mCurrentFullUserRecord
+ .mLastMediaButtonReceiver.getIntent().getComponent();
+ if (componentName != null) {
+ for (FullUserRecord.CallbackRecord cr
+ : mCurrentFullUserRecord.mCallbacks.values()) {
+ cr.callback.onMediaKeyEventDispatchedToMediaButtonReceiver(
keyEvent, componentName);
}
}
@@ -2018,9 +2070,9 @@
Log.w(TAG, "Error sending media button to the restored intent "
+ receiver + ", type=" + componentType, e);
}
- if (mCurrentFullUserRecord.mCallback != null) {
- mCurrentFullUserRecord.mCallback
- .onMediaKeyEventDispatchedToMediaButtonReceiver(
+ for (FullUserRecord.CallbackRecord cr
+ : mCurrentFullUserRecord.mCallbacks.values()) {
+ cr.callback.onMediaKeyEventDispatchedToMediaButtonReceiver(
keyEvent, receiver);
}
}