Merge "MediaSession2: Introduce connection hints" into qt-dev

This commit is contained in:
TreeHugger Robot
2019-04-14 22:19:05 +00:00
committed by Android (Google) Code Review
6 changed files with 193 additions and 51 deletions

View File

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

View File

@@ -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() {
}

View File

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

View File

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

View File

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

View File

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