Merge "MediaSession2: Introduce connection hints" into qt-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
cf44c41158
@@ -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<android.media.MediaSession2> 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";
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* {@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}.
|
||||
* <p>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String, MediaSession2> 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.
|
||||
* <p>
|
||||
* Primary session is the highest priority session that this service manages. Here are some
|
||||
* recommendations of the primary session.
|
||||
* <ol>
|
||||
* <li>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.</li>
|
||||
* <li>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.</li>
|
||||
* </ol>
|
||||
* {@link Session2Token}. Return the session for telling the controller which session to
|
||||
* connect. Return {@code null} to reject the connection from this controller.
|
||||
* <p>
|
||||
* Session returned here will be added to this service automatically. You don't need to call
|
||||
* {@link #addSession(MediaSession2)} for that.
|
||||
* <p>
|
||||
* Session service will accept or reject the connection with the
|
||||
* {@link MediaSession2.SessionCallback} in the session returned here.
|
||||
* <p>
|
||||
* 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user