diff --git a/api/current.txt b/api/current.txt index 88c0aae76c2c5..af842b5c76bc1 100644 --- a/api/current.txt +++ b/api/current.txt @@ -24646,8 +24646,6 @@ package android.media { } public class MediaController2 implements java.lang.AutoCloseable { - ctor public MediaController2(@NonNull android.content.Context, @NonNull android.media.Session2Token); - ctor public MediaController2(@NonNull android.content.Context, @NonNull android.media.Session2Token, @NonNull java.util.concurrent.Executor, @NonNull android.media.MediaController2.ControllerCallback); method public void cancelSessionCommand(@NonNull Object); method public void close(); method @Nullable public android.media.Session2Token getConnectedSessionToken(); @@ -24655,6 +24653,13 @@ package android.media { method @NonNull public Object sendSessionCommand(@NonNull android.media.Session2Command, @Nullable android.os.Bundle); } + public static final class MediaController2.Builder { + ctor public MediaController2.Builder(@NonNull android.content.Context, @NonNull android.media.Session2Token); + method @NonNull public android.media.MediaController2 build(); + method @NonNull public android.media.MediaController2.Builder setConnectionHints(@NonNull android.os.Bundle); + method @NonNull public android.media.MediaController2.Builder setControllerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaController2.ControllerCallback); + } + public abstract static class MediaController2.ControllerCallback { ctor public MediaController2.ControllerCallback(); method public void onCommandResult(@NonNull android.media.MediaController2, @NonNull Object, @NonNull android.media.Session2Command, @NonNull android.media.Session2Command.Result); @@ -25844,6 +25849,7 @@ package android.media { } public static final class MediaSession2.ControllerInfo { + method @NonNull public android.os.Bundle getConnectionHints(); method @NonNull public String getPackageName(); method @NonNull public android.media.session.MediaSessionManager.RemoteUserInfo getRemoteUserInfo(); method public int getUid(); @@ -25863,7 +25869,7 @@ package android.media { method public final void addSession(@NonNull android.media.MediaSession2); method @NonNull public final java.util.List getSessions(); method @CallSuper @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent); - method @NonNull public abstract android.media.MediaSession2 onGetPrimarySession(); + method @Nullable public abstract android.media.MediaSession2 onGetSession(@NonNull android.media.MediaSession2.ControllerInfo); method @Nullable public abstract android.media.MediaSession2Service.MediaNotification onUpdateNotification(@NonNull android.media.MediaSession2); method public final void removeSession(@NonNull android.media.MediaSession2); field public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service"; diff --git a/media/apex/java/android/media/MediaConstants.java b/media/apex/java/android/media/MediaConstants.java index 776c1ba3f82fb..ce108894b9a5f 100644 --- a/media/apex/java/android/media/MediaConstants.java +++ b/media/apex/java/android/media/MediaConstants.java @@ -28,6 +28,7 @@ class MediaConstants { static final String KEY_ALLOWED_COMMANDS = "android.media.key.ALLOWED_COMMANDS"; static final String KEY_PLAYBACK_ACTIVE = "android.media.key.PLAYBACK_ACTIVE"; static final String KEY_TOKEN_EXTRAS = "android.media.key.TOKEN_EXTRAS"; + static final String KEY_CONNECTION_HINTS = "android.media.key.CONNECTION_HINTS"; private MediaConstants() { } diff --git a/media/apex/java/android/media/MediaController2.java b/media/apex/java/android/media/MediaController2.java index 1e8438ec9fe87..fb4e6ac239379 100644 --- a/media/apex/java/android/media/MediaController2.java +++ b/media/apex/java/android/media/MediaController2.java @@ -17,6 +17,7 @@ package android.media; import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS; +import static android.media.MediaConstants.KEY_CONNECTION_HINTS; import static android.media.MediaConstants.KEY_PACKAGE_NAME; import static android.media.MediaConstants.KEY_PID; import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE; @@ -91,24 +92,16 @@ public class MediaController2 implements AutoCloseable { * Create a {@link MediaController2} from the {@link Session2Token}. * This connects to the session and may wake up the service if it's not available. * - * @param context Context - * @param token token to connect to - */ - public MediaController2(@NonNull Context context, @NonNull Session2Token token) { - this(context, token, context.getMainExecutor(), new ControllerCallback() {}); - } - - /** - * Create a {@link MediaController2} from the {@link Session2Token}. - * This connects to the session and may wake up the service if it's not available. - * - * @param context Context + * @param context context * @param token token to connect to + * @param connectionHints a session-specific argument to send to the session when connecting. + * The contents of this bundle may affect the connection result. * @param executor executor to run callbacks on. * @param callback controller callback to receive changes in. */ - public MediaController2(@NonNull Context context, @NonNull Session2Token token, - @NonNull Executor executor, @NonNull ControllerCallback callback) { + MediaController2(@NonNull Context context, @NonNull Session2Token token, + @Nullable Bundle connectionHints, @NonNull Executor executor, + @NonNull ControllerCallback callback) { if (context == null) { throw new IllegalArgumentException("context shouldn't be null"); } @@ -130,9 +123,9 @@ public class MediaController2 implements AutoCloseable { boolean connectRequested; if (token.getType() == TYPE_SESSION) { mServiceConnection = null; - connectRequested = requestConnectToSession(); + connectRequested = requestConnectToSession(connectionHints); } else { - mServiceConnection = new SessionServiceConnection(); + mServiceConnection = new SessionServiceConnection(connectionHints); connectRequested = requestConnectToService(); } if (!connectRequested) { @@ -350,16 +343,17 @@ public class MediaController2 implements AutoCloseable { } } - private Bundle createConnectionRequest() { + private Bundle createConnectionRequest(@Nullable Bundle connectionHints) { Bundle connectionRequest = new Bundle(); connectionRequest.putString(KEY_PACKAGE_NAME, mContext.getPackageName()); connectionRequest.putInt(KEY_PID, Process.myPid()); + connectionRequest.putBundle(KEY_CONNECTION_HINTS, connectionHints); return connectionRequest; } - private boolean requestConnectToSession() { + private boolean requestConnectToSession(@Nullable Bundle connectionHints) { Session2Link sessionBinder = mSessionToken.getSessionLink(); - Bundle connectionRequest = createConnectionRequest(); + Bundle connectionRequest = createConnectionRequest(connectionHints); try { sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest); } catch (RuntimeException e) { @@ -401,6 +395,93 @@ public class MediaController2 implements AutoCloseable { return true; } + /** + * Builder for {@link MediaController2}. + *

+ * Any incoming event from the {@link MediaSession2} will be handled on the callback + * executor. If it's not set, {@link Context#getMainExecutor()} will be used by default. + */ + public static final class Builder { + private Context mContext; + private Session2Token mToken; + private Bundle mConnectionHints; + private Executor mCallbackExecutor; + private ControllerCallback mCallback; + + /** + * Creates a builder for {@link MediaController2}. + * + * @param context context + * @param token token of the session to connect to + */ + public Builder(@NonNull Context context, @NonNull Session2Token token) { + if (context == null) { + throw new IllegalArgumentException("context shouldn't be null"); + } + if (token == null) { + throw new IllegalArgumentException("token shouldn't be null"); + } + mContext = context; + mToken = token; + } + + /** + * Set the connection hints for the controller. + *

+ * {@code connectionHints} is a session-specific argument to send to the session when + * connecting. The contents of this bundle may affect the connection result. + * + * @param connectionHints a bundle which contains the connection hints + * @return The Builder to allow chaining + */ + @NonNull + public Builder setConnectionHints(@NonNull Bundle connectionHints) { + if (connectionHints == null) { + throw new IllegalArgumentException("connectionHints shouldn't be null"); + } + mConnectionHints = new Bundle(connectionHints); + return this; + } + + /** + * Set callback for the controller and its executor. + * + * @param executor callback executor + * @param callback session callback. + * @return The Builder to allow chaining + */ + @NonNull + public Builder setControllerCallback(@NonNull Executor executor, + @NonNull ControllerCallback callback) { + if (executor == null) { + throw new IllegalArgumentException("executor shouldn't be null"); + } + if (callback == null) { + throw new IllegalArgumentException("callback shouldn't be null"); + } + mCallbackExecutor = executor; + mCallback = callback; + return this; + } + + /** + * Build {@link MediaController2}. + * + * @return a new controller + */ + @NonNull + public MediaController2 build() { + if (mCallbackExecutor == null) { + mCallbackExecutor = mContext.getMainExecutor(); + } + if (mCallback == null) { + mCallback = new ControllerCallback() {}; + } + return new MediaController2( + mContext, mToken, mConnectionHints, mCallbackExecutor, mCallback); + } + } + /** * Interface for listening to change in activeness of the {@link MediaSession2}. *

@@ -469,7 +550,10 @@ public class MediaController2 implements AutoCloseable { // This will be called on the main thread. private class SessionServiceConnection implements ServiceConnection { - SessionServiceConnection() { + private final Bundle mConnectionHints; + + SessionServiceConnection(@Nullable Bundle connectionHints) { + mConnectionHints = connectionHints; } @Override @@ -491,7 +575,7 @@ public class MediaController2 implements AutoCloseable { Log.wtf(TAG, "Service interface is missing."); return; } - Bundle connectionRequest = createConnectionRequest(); + Bundle connectionRequest = createConnectionRequest(mConnectionHints); iService.connect(mControllerStub, getNextSeqNumber(), connectionRequest); connectRequested = true; } catch (RemoteException e) { diff --git a/media/apex/java/android/media/MediaSession2.java b/media/apex/java/android/media/MediaSession2.java index a900d87450067..6b56ae086a0d3 100644 --- a/media/apex/java/android/media/MediaSession2.java +++ b/media/apex/java/android/media/MediaSession2.java @@ -17,6 +17,7 @@ package android.media; import static android.media.MediaConstants.KEY_ALLOWED_COMMANDS; +import static android.media.MediaConstants.KEY_CONNECTION_HINTS; import static android.media.MediaConstants.KEY_PACKAGE_NAME; import static android.media.MediaConstants.KEY_PID; import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE; @@ -308,8 +309,11 @@ public class MediaSession2 implements AutoCloseable { String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME); RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid); - final ControllerInfo controllerInfo = new ControllerInfo(remoteUserInfo, - mSessionManager.isTrustedForMediaControl(remoteUserInfo), controller); + final ControllerInfo controllerInfo = new ControllerInfo( + remoteUserInfo, + mSessionManager.isTrustedForMediaControl(remoteUserInfo), + controller, + connectionRequest.getBundle(KEY_CONNECTION_HINTS)); mCallbackExecutor.execute(() -> { boolean connected = false; try { @@ -568,6 +572,7 @@ public class MediaSession2 implements AutoCloseable { private final RemoteUserInfo mRemoteUserInfo; private final boolean mIsTrusted; private final Controller2Link mControllerBinder; + private final Bundle mConnectionHints; private final Object mLock = new Object(); //@GuardedBy("mLock") private int mNextSeqNumber; @@ -583,12 +588,16 @@ public class MediaSession2 implements AutoCloseable { * @param remoteUserInfo remote user info * @param trusted {@code true} if trusted, {@code false} otherwise * @param controllerBinder Controller2Link for the connected controller. + * @param connectionHints a session-specific argument sent from the controller for the + * connection. The contents of this bundle may affect the + * connection result. */ ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted, - @Nullable Controller2Link controllerBinder) { + @Nullable Controller2Link controllerBinder, @Nullable Bundle connectionHints) { mRemoteUserInfo = remoteUserInfo; mIsTrusted = trusted; mControllerBinder = controllerBinder; + mConnectionHints = connectionHints; mPendingCommands = new ArrayMap<>(); mRequestedCommandSeqNumbers = new ArraySet<>(); } @@ -616,6 +625,14 @@ public class MediaSession2 implements AutoCloseable { return mRemoteUserInfo.getUid(); } + /** + * @return connection hints sent from controller, or {@link Bundle#EMPTY} if none. + */ + @NonNull + public Bundle getConnectionHints() { + return mConnectionHints == null ? Bundle.EMPTY : new Bundle(mConnectionHints); + } + /** * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or * has a enabled notification listener so can be trusted to accept connection and incoming diff --git a/media/apex/java/android/media/MediaSession2Service.java b/media/apex/java/android/media/MediaSession2Service.java index 5bb746a7f9e33..28ead99357bcb 100644 --- a/media/apex/java/android/media/MediaSession2Service.java +++ b/media/apex/java/android/media/MediaSession2Service.java @@ -16,6 +16,10 @@ package android.media; +import static android.media.MediaConstants.KEY_CONNECTION_HINTS; +import static android.media.MediaConstants.KEY_PACKAGE_NAME; +import static android.media.MediaConstants.KEY_PID; + import android.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.Nullable; @@ -24,6 +28,9 @@ import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.media.MediaSession2.ControllerInfo; +import android.media.session.MediaSessionManager; +import android.media.session.MediaSessionManager.RemoteUserInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -70,6 +77,8 @@ public abstract class MediaSession2Service extends Service { //@GuardedBy("mLock") private NotificationManager mNotificationManager; //@GuardedBy("mLock") + private MediaSessionManager mMediaSessionManager; + //@GuardedBy("mLock") private Intent mStartSelfIntent; //@GuardedBy("mLock") private Map mSessions = new ArrayMap<>(); @@ -93,6 +102,8 @@ public abstract class MediaSession2Service extends Service { mStartSelfIntent = new Intent(this, this.getClass()); mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + mMediaSessionManager = + (MediaSessionManager) getSystemService(Context.MEDIA_SESSION_SERVICE); } } @@ -132,34 +143,22 @@ public abstract class MediaSession2Service extends Service { /** * Called when a {@link MediaController2} is created with the this service's - * {@link Session2Token}. Return the primary session for telling the controller which session to - * connect. - *

- * Primary session is the highest priority session that this service manages. Here are some - * recommendations of the primary session. - *

    - *
  1. When there's no {@link MediaSession2}, create and return a new session. Resume the - * playback that the app has the lastly played with the new session. The behavior is what - * framework expects when the framework sends key events to the service.
  2. - *
  3. When there's multiple {@link MediaSession2}s, pick the session that has the lastly - * started the playback. This is the same way as the framework prioritize sessions to receive - * media key events.
  4. - *
+ * {@link Session2Token}. Return the session for telling the controller which session to + * connect. Return {@code null} to reject the connection from this controller. *

* Session returned here will be added to this service automatically. You don't need to call * {@link #addSession(MediaSession2)} for that. *

- * Session service will accept or reject the connection with the - * {@link MediaSession2.SessionCallback} in the session returned here. - *

* This method is always called on the main thread. * - * @return a new session + * @param controllerInfo information of the controller which is trying to connect. + * @return a {@link MediaSession2} instance for the controller to connect to, or {@code null} + * to reject connection * @see MediaSession2.Builder * @see #getSessions() */ - @NonNull - public abstract MediaSession2 onGetPrimarySession(); + @Nullable + public abstract MediaSession2 onGetSession(@NonNull ControllerInfo controllerInfo); /** * Called when notification UI needs update. Override this method to show or cancel your own @@ -250,6 +249,16 @@ public abstract class MediaSession2Service extends Service { return list; } + /** + * Returns the {@link MediaSessionManager}. + */ + @NonNull + MediaSessionManager getMediaSessionManager() { + synchronized (mLock) { + return mMediaSessionManager; + } + } + /** * Called by registered {@link MediaSession2.ForegroundServiceEventCallback} * @@ -365,8 +374,33 @@ public abstract class MediaSession2Service extends Service { Log.d(TAG, "Handling incoming connection request from the" + " controller, controller=" + caller + ", uid=" + uid); } + + String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME); + // The Binder.getCallingPid() can be 0 for an oneway call from the + // remote process. If it's the case, use PID from the connectionRequest. + RemoteUserInfo remoteUserInfo = new RemoteUserInfo( + callingPkg, + pid == 0 ? connectionRequest.getInt(KEY_PID) : pid, + uid); + final ControllerInfo controllerInfo = new ControllerInfo( + remoteUserInfo, + service.getMediaSessionManager() + .isTrustedForMediaControl(remoteUserInfo), + caller, + connectionRequest.getBundle(KEY_CONNECTION_HINTS)); + final MediaSession2 session; - session = service.onGetPrimarySession(); + session = service.onGetSession(controllerInfo); + + if (session == null) { + if (DEBUG) { + Log.d(TAG, "Rejecting incoming connection request from the" + + " controller, controller=" + caller + ", uid=" + uid); + } + // Note: Trusted controllers also can be rejected according to the + // service implementation. + return; + } service.addSession(session); shouldNotifyDisconnected = false; session.onConnect(caller, pid, uid, seq, connectionRequest); @@ -377,8 +411,7 @@ public abstract class MediaSession2Service extends Service { // Trick to call onDisconnected() in one place. if (shouldNotifyDisconnected) { if (DEBUG) { - Log.d(TAG, "Service has destroyed prematurely." - + " Rejecting connection"); + Log.d(TAG, "Notifying the controller of its disconnection"); } try { caller.notifyDisconnected(0); diff --git a/services/core/java/com/android/server/media/MediaSessionServiceImpl.java b/services/core/java/com/android/server/media/MediaSessionServiceImpl.java index de36deafa4d75..cdfd948f4bd42 100644 --- a/services/core/java/com/android/server/media/MediaSessionServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaSessionServiceImpl.java @@ -1037,8 +1037,9 @@ public class MediaSessionServiceImpl extends MediaSessionService.ServiceImpl { // it's closed. // TODO: Keep controller as well for better readability // because the GC behavior isn't straightforward. - MediaController2 controller = new MediaController2(mContext, sessionToken, - new HandlerExecutor(mHandler), callback); + MediaController2 controller = new MediaController2.Builder(mContext, sessionToken) + .setControllerCallback(new HandlerExecutor(mHandler), callback) + .build(); } finally { Binder.restoreCallingIdentity(token); }