method @NonNull public abstract android.media.MediaSession2 onGetPrimarySession();
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";
}
public static class MediaSession2Service.MediaNotification {
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 public void notifySession2Destroyed(@NonNull android.media.Session2Token);
method public void removeOnActiveSessionsChangedListener(@NonNull android.media.session.MediaSessionManager.OnActiveSessionsChangedListener);
method public void removeOnSession2TokensChangedListener(@NonNull android.media.session.MediaSessionManager.OnSession2TokensChangedListener);
}
method public void stop();
}
+ public final class Session2Token implements android.os.Parcelable {
+ ctor public Session2Token(@NonNull android.content.Context, @NonNull String, @Nullable android.os.Bundle);
+ method public void destroy();
+ method @NonNull public android.os.Bundle getExtras();
+ method public int getPid();
+ method public boolean isDestroyed();
+ field public static final String SESSION_SERVICE_INTERFACE = "android.media.MediaSession2Service";
+ }
+
public static class SubtitleData.Builder {
ctor public SubtitleData.Builder();
ctor public SubtitleData.Builder(@NonNull android.media.SubtitleData);
"apex/java/android/media/Session2Command.java",
"apex/java/android/media/Session2CommandGroup.java",
"apex/java/android/media/Session2Link.java",
- "apex/java/android/media/Session2Token.java",
],
}
static final String KEY_PACKAGE_NAME = "android.media.key.PACKAGE_NAME";
// Bundle key for Parcelable
- static final String KEY_SESSION2LINK = "android.media.key.SESSION2LINK";
+ static final String KEY_SESSION2_TOKEN = "android.media.key.SESSION2_TOKEN";
+ static final String KEY_SESSION2_LINK = "android.media.key.SESSION2_LINK";
static final String KEY_ALLOWED_COMMANDS = "android.media.key.ALLOWED_COMMANDS";
static final String KEY_PLAYBACK_ACTIVE = "android.media.key.PLAYBACK_ACTIVE";
import static android.media.MediaConstants.KEY_PACKAGE_NAME;
import static android.media.MediaConstants.KEY_PID;
import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE;
-import static android.media.MediaConstants.KEY_SESSION2LINK;
+import static android.media.MediaConstants.KEY_SESSION2_LINK;
+import static android.media.MediaConstants.KEY_SESSION2_TOKEN;
import static android.media.Session2Command.RESULT_ERROR_UNKNOWN_ERROR;
import static android.media.Session2Command.RESULT_INFO_SKIPPED;
+import static android.media.Session2Token.SESSION_SERVICE_INTERFACE;
import static android.media.Session2Token.TYPE_SESSION;
import android.annotation.NonNull;
// Called by Controller2Link.onConnected
void onConnected(int seq, Bundle connectionResult) {
- Session2Link sessionBinder = connectionResult.getParcelable(KEY_SESSION2LINK);
+ Session2Token token = connectionResult.getParcelable(KEY_SESSION2_TOKEN);
+ Session2Link sessionBinder = token.getExtras().getParcelable(KEY_SESSION2_LINK);
Session2CommandGroup allowedCommands =
connectionResult.getParcelable(KEY_ALLOWED_COMMANDS);
boolean playbackActive = connectionResult.getBoolean(KEY_PLAYBACK_ACTIVE);
// Implementation for the local binder is no-op,
// so can be used without worrying about deadlock.
sessionBinder.linkToDeath(mDeathRecipient, 0);
- mConnectedToken = new Session2Token(mSessionToken.getUid(), TYPE_SESSION,
- mSessionToken.getPackageName(), sessionBinder);
+ mConnectedToken = token;
}
mCallbackExecutor.execute(() -> {
mCallback.onConnected(MediaController2.this, allowedCommands);
}
private boolean requestConnectToSession() {
- Session2Link sessionBinder = mSessionToken.getSessionLink();
+ Session2Link sessionBinder = mSessionToken.getExtras().getParcelable(KEY_SESSION2_LINK);
Bundle connectionRequest = createConnectionRequest();
try {
sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
private boolean requestConnectToService() {
// Service. Needs to get fresh binder whenever connection is needed.
- final Intent intent = new Intent(MediaSession2Service.SERVICE_INTERFACE);
+ final Intent intent = new Intent(SESSION_SERVICE_INTERFACE);
intent.setClassName(mSessionToken.getPackageName(), mSessionToken.getServiceName());
// Use bindService() instead of startForegroundService() to start session service for three
import static android.media.MediaConstants.KEY_PACKAGE_NAME;
import static android.media.MediaConstants.KEY_PID;
import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE;
-import static android.media.MediaConstants.KEY_SESSION2LINK;
+import static android.media.MediaConstants.KEY_SESSION2_LINK;
+import static android.media.MediaConstants.KEY_SESSION2_TOKEN;
import static android.media.Session2Command.RESULT_ERROR_UNKNOWN_ERROR;
import static android.media.Session2Command.RESULT_INFO_SKIPPED;
-import static android.media.Session2Token.TYPE_SESSION;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.media.session.MediaSessionManager.RemoteUserInfo;
import android.os.Bundle;
import android.os.Handler;
-import android.os.Process;
import android.os.ResultReceiver;
import android.util.ArrayMap;
import android.util.ArraySet;
mCallbackExecutor = callbackExecutor;
mCallback = callback;
mSessionStub = new Session2Link(this);
- mSessionToken = new Session2Token(Process.myUid(), TYPE_SESSION, context.getPackageName(),
- mSessionStub);
+
+ Bundle extras = new Bundle();
+ extras.putParcelable(KEY_SESSION2_LINK, mSessionStub);
+ mSessionToken = new Session2Token(context, id, extras);
mSessionManager = (MediaSessionManager) mContext.getSystemService(
Context.MEDIA_SESSION_SERVICE);
// NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
for (ControllerInfo info : controllerInfos) {
info.notifyDisconnected();
}
+ mSessionToken.destroy();
+ mSessionManager.notifySession2Destroyed(mSessionToken);
} catch (Exception e) {
// Should not be here.
}
// It's needed because we cannot call synchronous calls between
// session/controller.
Bundle connectionResult = new Bundle();
- connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub);
+ connectionResult.putParcelable(KEY_SESSION2_TOKEN, mSessionToken);
connectionResult.putParcelable(KEY_ALLOWED_COMMANDS,
controllerInfo.mAllowedCommands);
connectionResult.putBoolean(KEY_PLAYBACK_ACTIVE, isPlaybackActive());
package android.media;
+import static android.media.Session2Token.SESSION_SERVICE_INTERFACE;
+
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
* for consistent behavior across all devices.
*/
public abstract class MediaSession2Service extends Service {
- /**
- * The {@link Intent} that must be declared as handled by the service.
- */
- public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service";
private static final String TAG = "MediaSession2Service";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@Override
@Nullable
public IBinder onBind(@NonNull Intent intent) {
- if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ if (SESSION_SERVICE_INTERFACE.equals(intent.getAction())) {
synchronized (mLock) {
return mStub;
}
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import java.util.Objects;
/**
- * Represents an ongoing {@link MediaSession2} or a {@link MediaSession2Service}.
+ * Represents an ongoing MediaSession2 or a MediaSession2Service.
* If it's representing a session service, it may not be ongoing.
* <p>
* This API is not generally intended for third party application developers.
* for consistent behavior across all devices.
* <p>
* This may be passed to apps by the session owner to allow them to create a
- * {@link MediaController2} to communicate with the session.
+ * MediaController2 to communicate with the session.
* <p>
* It can be also obtained by {@link android.media.session.MediaSessionManager}.
*/
};
/**
+ * The {@link Intent} that must be declared for the session service.
+ * @hide
+ */
+ @SystemApi
+ public static final String SESSION_SERVICE_INTERFACE = "android.media.MediaSession2Service";
+
+ /**
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
}
/**
- * Type for {@link MediaSession2}.
+ * Type for MediaSession2.
*/
public static final int TYPE_SESSION = 0;
/**
- * Type for {@link MediaSession2Service}.
+ * Type for MediaSession2Service.
*/
public static final int TYPE_SESSION_SERVICE = 1;
+ private final String mSessionId;
+ private final int mPid;
private final int mUid;
@TokenType
private final int mType;
private final String mPackageName;
private final String mServiceName;
- private final Session2Link mSessionLink;
private final ComponentName mComponentName;
+ private final Bundle mExtras;
+
+ private boolean mDestroyed = false;
/**
* Constructor for the token with type {@link #TYPE_SESSION_SERVICE}.
final PackageManager manager = context.getPackageManager();
final int uid = getUid(manager, serviceComponent.getPackageName());
- if (!isInterfaceDeclared(manager, MediaSession2Service.SERVICE_INTERFACE,
- serviceComponent)) {
+ if (!isInterfaceDeclared(manager, SESSION_SERVICE_INTERFACE, serviceComponent)) {
Log.w(TAG, serviceComponent + " doesn't implement MediaSession2Service.");
}
+ mSessionId = null;
mComponentName = serviceComponent;
mPackageName = serviceComponent.getPackageName();
mServiceName = serviceComponent.getClassName();
+ mPid = -1;
mUid = uid;
mType = TYPE_SESSION_SERVICE;
- mSessionLink = null;
+ mExtras = null;
}
- Session2Token(int uid, int type, String packageName, Session2Link sessionLink) {
- mUid = uid;
- mType = type;
- mPackageName = packageName;
+ /**
+ * Constructor for the token with type {@link #TYPE_SESSION}.
+ *
+ * @param context The context.
+ * @param sessionId The ID of the session. Should be unique.
+ * @param extras The extras.
+ * @hide
+ */
+ @SystemApi
+ public Session2Token(@NonNull Context context, @NonNull String sessionId,
+ @Nullable Bundle extras) {
+ if (sessionId == null) {
+ throw new IllegalArgumentException("sessionId shouldn't be null");
+ }
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ mSessionId = sessionId;
+ mPid = Process.myPid();
+ mUid = Process.myUid();
+ mType = TYPE_SESSION;
+ mPackageName = context.getPackageName();
+ mExtras = extras;
mServiceName = null;
mComponentName = null;
- mSessionLink = sessionLink;
}
Session2Token(Parcel in) {
+ mSessionId = in.readString();
+ mPid = in.readInt();
mUid = in.readInt();
mType = in.readInt();
mPackageName = in.readString();
mServiceName = in.readString();
- mSessionLink = in.readParcelable(null);
mComponentName = ComponentName.unflattenFromString(in.readString());
+ mExtras = in.readParcelable(null);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mSessionId);
+ dest.writeInt(mPid);
dest.writeInt(mUid);
dest.writeInt(mType);
dest.writeString(mPackageName);
dest.writeString(mServiceName);
- dest.writeParcelable(mSessionLink, flags);
dest.writeString(mComponentName == null ? "" : mComponentName.flattenToString());
+ dest.writeParcelable(mExtras, flags);
}
@Override
@Override
public int hashCode() {
- return Objects.hash(mType, mUid, mPackageName, mServiceName, mSessionLink);
+ return Objects.hash(mSessionId, mPid, mUid, mType, mPackageName, mServiceName);
}
@Override
return false;
}
Session2Token other = (Session2Token) obj;
- return mUid == other.mUid
- && TextUtils.equals(mPackageName, other.mPackageName)
- && TextUtils.equals(mServiceName, other.mServiceName)
+ return TextUtils.equals(mSessionId, other.mSessionId)
+ && mPid == other.mPid
+ && mUid == other.mUid
&& mType == other.mType
- && Objects.equals(mSessionLink, other.mSessionLink);
+ && TextUtils.equals(mPackageName, other.mPackageName)
+ && TextUtils.equals(mServiceName, other.mServiceName);
}
@Override
public String toString() {
return "Session2Token {pkg=" + mPackageName + " type=" + mType
- + " service=" + mServiceName + " Session2Link=" + mSessionLink + "}";
+ + " service=" + mServiceName + "}";
+ }
+
+ /**
+ * @return pid of the session
+ * @hide
+ */
+ @SystemApi
+ public int getPid() {
+ return mPid;
}
/**
return mType;
}
- Session2Link getSessionLink() {
- return mSessionLink;
+ /**
+ * @return extras
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras == null ? new Bundle() : new Bundle(mExtras);
+ }
+
+ /**
+ * Destroys this session token. After this method is called,
+ * {@link MediaSessionManager#notifySession2Created(Session2Token)} should not be called
+ * with this token.
+ *
+ * @see MediaSessionManager#notifySession2Created(Session2Token)
+ * @hide
+ */
+ @SystemApi
+ public void destroy() {
+ mDestroyed = true;
+ }
+
+ /**
+ * @return whether this token is destroyed
+ * @hide
+ */
+ @SystemApi
+ public boolean isDestroyed() {
+ return mDestroyed;
}
private static boolean isInterfaceDeclared(PackageManager manager, String serviceInterface,
SessionLink createSession(String packageName, in SessionCallbackLink sessionCb, String tag,
int userId);
void notifySession2Created(in Session2Token sessionToken);
+ void notifySession2Destroyed(in Session2Token sessionToken);
List<ControllerLink> getSessions(in ComponentName compName, int userId);
List<Session2Token> getSession2Tokens(int userId);
void dispatchMediaKeyEvent(String packageName, boolean asSystemService, in KeyEvent keyEvent,
import android.content.Context;
import android.media.AudioManager;
import android.media.IRemoteVolumeController;
-import android.media.MediaSession2;
import android.media.Session2Token;
import android.os.Handler;
import android.os.IBinder;
}
/**
- * Notifies that a new {@link MediaSession2} with type {@link Session2Token#TYPE_SESSION} is
+ * Notifies that a new MediaSession2 with type {@link Session2Token#TYPE_SESSION} is
* created.
* <p>
* Do not use this API directly, but create a new instance through the
- * {@link MediaSession2.Builder} instead.
+ * MediaSession2.Builder instead.
*
* @param token newly created session2 token
*/
if (token.getType() != Session2Token.TYPE_SESSION) {
throw new IllegalArgumentException("token's type should be TYPE_SESSION");
}
+ if (token.isDestroyed()) {
+ throw new IllegalArgumentException("token is already destroyed");
+ }
try {
mService.notifySession2Created(token);
} catch (RemoteException e) {
}
/**
+ * Notifies that a new MediaSession2 with type {@link Session2Token#TYPE_SESSION} is
+ * destroyed.
+ * <p>
+ * Do not use this API directly, but close a session with MediaSession2#close() instead.
+ *
+ * @param token destroyed session2 token
+ */
+ public void notifySession2Destroyed(@NonNull Session2Token token) {
+ if (token == null) {
+ throw new IllegalArgumentException("token shouldn't be null");
+ }
+ if (token.getType() != Session2Token.TYPE_SESSION) {
+ throw new IllegalArgumentException("token's type should be TYPE_SESSION");
+ }
+ if (!token.isDestroyed()) {
+ throw new IllegalArgumentException("token should have been destroyed");
+ }
+ try {
+ mService.notifySession2Destroyed(token);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Get a list of controllers for all ongoing sessions. The controllers will
* be provided in priority order with the most important controller at index
* 0.
* current user.
* <p>
* Although this API can be used without any restriction, each session owners can accept or
- * reject your uses of {@link MediaSession2}.
+ * reject your uses of MediaSession2.
*
* @return A list of {@link Session2Token}.
*/
import android.media.AudioSystem;
import android.media.IAudioService;
import android.media.IRemoteVolumeController;
-import android.media.MediaController2;
-import android.media.Session2CommandGroup;
import android.media.Session2Token;
import android.media.session.ControllerLink;
import android.media.session.IActiveSessionsListener;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
-import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
if (DEBUG) {
Log.d(TAG, "Session2 is created " + sessionToken);
}
+ if (pid != sessionToken.getPid()) {
+ throw new SecurityException("Unexpected Session2Token's PID, expected=" + pid
+ + " but actually=" + sessionToken.getPid());
+ }
+ if (uid != sessionToken.getUid()) {
+ throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
+ + " but actually=" + sessionToken.getUid());
+ }
+ int userId = UserHandle.getUserId(uid);
+ List<Session2Token> session2Tokens = mSession2TokensPerUser.get(userId);
+ if (session2Tokens.contains(sessionToken)) {
+ if (DEBUG) {
+ Log.d(TAG, "notifySession2Created(): Ignoring already existing token "
+ + sessionToken);
+ }
+ return;
+ }
+ session2Tokens.add(sessionToken);
+ pushSession2TokensChangedLocked(userId);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void notifySession2Destroyed(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 destroyed " + sessionToken);
+ }
+ if (pid != sessionToken.getPid()) {
+ throw new SecurityException("Unexpected Session2Token's PID, expected=" + pid
+ + " but actually=" + sessionToken.getPid());
+ }
if (uid != sessionToken.getUid()) {
throw new SecurityException("Unexpected Session2Token's UID, expected=" + uid
+ " but actually=" + sessionToken.getUid());
}
- Controller2Callback callback = new Controller2Callback(sessionToken);
- // Note: It's safe not to keep controller here because it wouldn't be GC'ed until
- // 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);
+ int userId = UserHandle.getUserId(uid);
+ mSession2TokensPerUser.get(userId).remove(sessionToken);
+ pushSession2TokensChangedLocked(userId);
} finally {
Binder.restoreCallingIdentity(token);
}
obtainMessage(MSG_SESSIONS_CHANGED, userIdInteger).sendToTarget();
}
}
-
- private class Controller2Callback extends MediaController2.ControllerCallback {
- private final Session2Token mToken;
-
- Controller2Callback(Session2Token token) {
- mToken = token;
- }
-
- @Override
- public void onConnected(MediaController2 controller, Session2CommandGroup allowedCommands) {
- synchronized (mLock) {
- int userId = UserHandle.getUserId(mToken.getUid());
- mSession2TokensPerUser.get(userId).add(mToken);
- pushSession2TokensChangedLocked(userId);
- }
- }
-
- @Override
- public void onDisconnected(MediaController2 controller) {
- synchronized (mLock) {
- int userId = UserHandle.getUserId(mToken.getUid());
- mSession2TokensPerUser.get(userId).remove(mToken);
- pushSession2TokensChangedLocked(userId);
- }
- }
- }
}