diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java index f6dd0a9d39..6ce24166c5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java @@ -18,6 +18,7 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; import android.app.PendingIntent; +import android.media.session.MediaSession.Token; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -57,6 +58,8 @@ import java.util.List; public final ImmutableList customLayout; + @Nullable public final Token platformToken; + public ConnectionState( int libraryVersion, int sessionInterfaceVersion, @@ -68,7 +71,8 @@ import java.util.List; Player.Commands playerCommandsFromPlayer, Bundle tokenExtras, Bundle sessionExtras, - PlayerInfo playerInfo) { + PlayerInfo playerInfo, + @Nullable Token platformToken) { this.libraryVersion = libraryVersion; this.sessionInterfaceVersion = sessionInterfaceVersion; this.sessionBinder = sessionBinder; @@ -80,6 +84,7 @@ import java.util.List; this.tokenExtras = tokenExtras; this.sessionExtras = sessionExtras; this.playerInfo = playerInfo; + this.platformToken = platformToken; } private static final String FIELD_LIBRARY_VERSION = Util.intToStringMaxRadix(0); @@ -94,8 +99,9 @@ import java.util.List; private static final String FIELD_PLAYER_INFO = Util.intToStringMaxRadix(7); private static final String FIELD_SESSION_INTERFACE_VERSION = Util.intToStringMaxRadix(8); private static final String FIELD_IN_PROCESS_BINDER = Util.intToStringMaxRadix(10); + private static final String FIELD_PLATFORM_TOKEN = Util.intToStringMaxRadix(12); - // Next field key = 12 + // Next field key = 13 public Bundle toBundleForRemoteProcess(int controllerInterfaceVersion) { Bundle bundle = new Bundle(); @@ -121,6 +127,9 @@ import java.util.List; intersectedCommands, /* excludeTimeline= */ false, /* excludeTracks= */ false) .toBundleForRemoteProcess(controllerInterfaceVersion)); bundle.putInt(FIELD_SESSION_INTERFACE_VERSION, sessionInterfaceVersion); + if (platformToken != null) { + bundle.putParcelable(FIELD_PLATFORM_TOKEN, platformToken); + } return bundle; } @@ -176,6 +185,7 @@ import java.util.List; playerInfoBundle == null ? PlayerInfo.DEFAULT : PlayerInfo.fromBundle(playerInfoBundle, sessionInterfaceVersion); + @Nullable Token platformToken = bundle.getParcelable(FIELD_PLATFORM_TOKEN); return new ConnectionState( libraryVersion, sessionInterfaceVersion, @@ -187,7 +197,8 @@ import java.util.List; playerCommandsFromPlayer, tokenExtras == null ? Bundle.EMPTY : tokenExtras, sessionExtras == null ? Bundle.EMPTY : sessionExtras, - playerInfo); + playerInfo, + platformToken); } private final class InProcessBinder extends Binder { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 3a2cf83181..7c15be9bc0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -36,6 +36,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.graphics.Rect; import android.graphics.SurfaceTexture; +import android.media.session.MediaSession; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -2619,6 +2620,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; CommandButton.copyWithUnavailableButtonsDisabled( result.customLayout, sessionCommands, intersectedPlayerCommands); playerInfo = result.playerInfo; + MediaSession.Token platformToken = + result.platformToken == null ? token.getPlatformToken() : result.platformToken; try { // Implementation for the local binder is no-op, // so can be used without worrying about deadlock. @@ -2635,7 +2638,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; result.sessionInterfaceVersion, token.getPackageName(), result.sessionBinder, - result.tokenExtras); + result.tokenExtras, + platformToken); sessionExtras = result.sessionExtras; getInstance().notifyAccepted(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 5d15ba2d82..a56a304754 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -830,7 +830,7 @@ public class MediaSession { return impl.getId(); } - /** Returns the {@link SessionToken} for creating {@link MediaController}. */ + /** Returns the {@link SessionToken} for creating {@link MediaController} instances. */ public final SessionToken getToken() { return impl.getToken(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 3c16e25e04..52e0f918c2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -40,6 +40,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.media.session.MediaSession.Token; import android.net.Uri; import android.os.Bundle; import android.os.DeadObjectException; @@ -215,6 +216,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; .appendPath(id) .appendPath(String.valueOf(SystemClock.elapsedRealtime())) .build(); + + sessionLegacyStub = + new MediaSessionLegacyStub( + /* session= */ thisRef, sessionUri, applicationHandler, tokenExtras); + + Token platformToken = (Token) sessionLegacyStub.getSessionCompat().getSessionToken().getToken(); sessionToken = new SessionToken( Process.myUid(), @@ -223,10 +230,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; MediaSessionStub.VERSION_INT, context.getPackageName(), sessionStub, - tokenExtras); + tokenExtras, + platformToken); - sessionLegacyStub = - new MediaSessionLegacyStub(/* session= */ thisRef, sessionUri, applicationHandler); // For PlayerWrapper, use the same default commands as the proxy controller gets when the app // doesn't overrides the default commands in `onConnect`. When the default is overridden by the // app in `onConnect`, the default set here will be overridden with these values. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index f9e506ea7a..771e5550a5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -134,7 +134,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; private int sessionFlags; @SuppressWarnings("PendingIntentMutability") // We can't use SaferPendingIntent - public MediaSessionLegacyStub(MediaSessionImpl session, Uri sessionUri, Handler handler) { + public MediaSessionLegacyStub( + MediaSessionImpl session, Uri sessionUri, Handler handler, Bundle tokenExtras) { sessionImpl = session; Context context = sessionImpl.getContext(); sessionManager = MediaSessionManager.getSessionManager(context); @@ -204,7 +205,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; sessionCompatId, Util.SDK_INT < 31 ? receiverComponentName : null, Util.SDK_INT < 31 ? mediaButtonIntent : null, - session.getToken().getExtras()); + /* sessionInfo= */ tokenExtras); if (Util.SDK_INT >= 31 && broadcastReceiverComponentName != null) { Api31.setMediaButtonBroadcastReceiver(sessionCompat, broadcastReceiverComponentName); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 9e32a005a1..df9f731e90 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -60,6 +60,7 @@ import static androidx.media3.session.SessionError.ERROR_UNKNOWN; import static androidx.media3.session.SessionError.INFO_CANCELLED; import android.app.PendingIntent; +import android.media.session.MediaSession.Token; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; @@ -521,6 +522,8 @@ import java.util.concurrent.ExecutionException; PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); PlayerInfo playerInfo = playerWrapper.createPlayerInfoForBundling(); playerInfo = generateAndCacheUniqueTrackGroupIds(playerInfo); + Token platformToken = + (Token) sessionImpl.getSessionCompat().getSessionToken().getToken(); ConnectionState state = new ConnectionState( MediaLibraryInfo.VERSION_INT, @@ -539,7 +542,8 @@ import java.util.concurrent.ExecutionException; connectionResult.sessionExtras != null ? connectionResult.sessionExtras : sessionImpl.getSessionExtras(), - playerInfo); + playerInfo, + platformToken); // Double check if session is still there, because release() can be called in // another thread. diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java index ea86649dc6..8855a59abc 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.media.session.MediaSession.Token; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; @@ -147,10 +148,18 @@ public final class SessionToken { int interfaceVersion, String packageName, IMediaSession iSession, - Bundle tokenExtras) { + Bundle tokenExtras, + @Nullable Token platformToken) { impl = new SessionTokenImplBase( - uid, type, libraryVersion, interfaceVersion, packageName, iSession, tokenExtras); + uid, + type, + libraryVersion, + interfaceVersion, + packageName, + iSession, + tokenExtras, + platformToken); } /** Creates a session token connected to a legacy media session. */ @@ -158,12 +167,12 @@ public final class SessionToken { this.impl = new SessionTokenImplLegacy(token, packageName, uid, extras); } - private SessionToken(Bundle bundle) { + private SessionToken(Bundle bundle, @Nullable Token platformToken) { checkArgument(bundle.containsKey(FIELD_IMPL_TYPE), "Impl type needs to be set."); @SessionTokenImplType int implType = bundle.getInt(FIELD_IMPL_TYPE); Bundle implBundle = checkNotNull(bundle.getBundle(FIELD_IMPL)); if (implType == IMPL_TYPE_BASE) { - impl = SessionTokenImplBase.fromBundle(implBundle); + impl = SessionTokenImplBase.fromBundle(implBundle, platformToken); } else { impl = SessionTokenImplLegacy.fromBundle(implBundle); } @@ -265,12 +274,17 @@ public final class SessionToken { return impl.getBinder(); } + @Nullable /* package */ + Token getPlatformToken() { + return impl.getPlatformToken(); + } + /** - * Creates a token from a {@link android.media.session.MediaSession.Token} or {@code + * Creates a token from a {@link Token} or {@code * android.support.v4.media.session.MediaSessionCompat.Token}. * * @param context A {@link Context}. - * @param token The {@link android.media.session.MediaSession.Token} or {@code + * @param token The {@link Token} or {@code * android.support.v4.media.session.MediaSessionCompat.Token}. * @return A {@link ListenableFuture} for the {@link SessionToken}. */ @@ -281,11 +295,11 @@ public final class SessionToken { } /** - * Creates a token from a {@link android.media.session.MediaSession.Token} or {@code + * Creates a token from a {@link Token} or {@code * android.support.v4.media.session.MediaSessionCompat.Token}. * * @param context A {@link Context}. - * @param token The {@link android.media.session.MediaSession.Token} or {@code + * @param token The {@link Token} or {@code * android.support.v4.media.session.MediaSessionCompat.Token}.. * @param completionLooper The {@link Looper} on which the returned {@link ListenableFuture} * completes. This {@link Looper} can't be used to call {@code future.get()} on the returned @@ -300,7 +314,7 @@ public final class SessionToken { private static MediaSessionCompat.Token createCompatToken( Parcelable platformOrLegacyCompatToken) { - if (platformOrLegacyCompatToken instanceof android.media.session.MediaSession.Token) { + if (platformOrLegacyCompatToken instanceof Token) { return MediaSessionCompat.Token.fromToken(platformOrLegacyCompatToken); } // Assume this is an android.support.v4.media.session.MediaSessionCompat.Token. @@ -346,7 +360,7 @@ public final class SessionToken { // Remove timeout callback. handler.removeCallbacksAndMessages(null); try { - future.set(SessionToken.fromBundle(resultData)); + future.set(SessionToken.fromBundle(resultData, (Token) compatToken.getToken())); } catch (RuntimeException e) { // Fallback to a legacy token if we receive an unexpected result, e.g. a legacy // session acknowledging commands by a success callback. @@ -476,6 +490,9 @@ public final class SessionToken { Object getBinder(); Bundle toBundle(); + + @Nullable + Token getPlatformToken(); } private static final String FIELD_IMPL_TYPE = Util.intToStringMaxRadix(0); @@ -506,6 +523,14 @@ public final class SessionToken { /** Restores a {@code SessionToken} from a {@link Bundle}. */ @UnstableApi public static SessionToken fromBundle(Bundle bundle) { - return new SessionToken(bundle); + return new SessionToken(bundle, /* platformToken= */ null); + } + + /** + * Restores a {@code SessionToken} from a {@link Bundle}, setting the provided {@code + * platformToken} if not already set. + */ + private static SessionToken fromBundle(Bundle bundle, Token platformToken) { + return new SessionToken(bundle, platformToken); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplBase.java b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplBase.java index 109ffead9b..26c3fe342a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplBase.java @@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkNotEmpty; import static androidx.media3.common.util.Assertions.checkNotNull; import android.content.ComponentName; +import android.media.session.MediaSession; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; @@ -48,6 +49,8 @@ import com.google.common.base.Objects; private final Bundle extras; + @Nullable private final MediaSession.Token platformToken; + public SessionTokenImplBase(ComponentName serviceComponent, int uid, int type) { this( uid, @@ -58,7 +61,8 @@ import com.google.common.base.Objects; /* serviceName= */ serviceComponent.getClassName(), /* componentName= */ serviceComponent, /* iSession= */ null, - /* extras= */ Bundle.EMPTY); + /* extras= */ Bundle.EMPTY, + /* platformToken= */ null); } public SessionTokenImplBase( @@ -68,7 +72,8 @@ import com.google.common.base.Objects; int interfaceVersion, String packageName, IMediaSession iSession, - Bundle tokenExtras) { + Bundle tokenExtras, + @Nullable MediaSession.Token platformToken) { this( uid, type, @@ -78,7 +83,8 @@ import com.google.common.base.Objects; /* serviceName= */ "", /* componentName= */ null, iSession.asBinder(), - checkNotNull(tokenExtras)); + checkNotNull(tokenExtras), + platformToken); } private SessionTokenImplBase( @@ -90,7 +96,8 @@ import com.google.common.base.Objects; String serviceName, @Nullable ComponentName componentName, @Nullable IBinder iSession, - Bundle extras) { + Bundle extras, + @Nullable MediaSession.Token platformToken) { this.uid = uid; this.type = type; this.libraryVersion = libraryVersion; @@ -100,6 +107,7 @@ import com.google.common.base.Objects; this.componentName = componentName; this.iSession = iSession; this.extras = extras; + this.platformToken = platformToken; } @Override @@ -112,7 +120,8 @@ import com.google.common.base.Objects; packageName, serviceName, componentName, - iSession); + iSession, + platformToken); } @Override @@ -127,8 +136,9 @@ import com.google.common.base.Objects; && interfaceVersion == other.interfaceVersion && TextUtils.equals(packageName, other.packageName) && TextUtils.equals(serviceName, other.serviceName) - && Util.areEqual(componentName, other.componentName) - && Util.areEqual(iSession, other.iSession); + && Objects.equal(componentName, other.componentName) + && Objects.equal(iSession, other.iSession) + && Objects.equal(platformToken, other.platformToken); } @Override @@ -203,6 +213,12 @@ import com.google.common.base.Objects; return iSession; } + @Nullable + @Override + public MediaSession.Token getPlatformToken() { + return platformToken; + } + private static final String FIELD_UID = Util.intToStringMaxRadix(0); private static final String FIELD_TYPE = Util.intToStringMaxRadix(1); private static final String FIELD_LIBRARY_VERSION = Util.intToStringMaxRadix(2); @@ -212,8 +228,9 @@ import com.google.common.base.Objects; private static final String FIELD_ISESSION = Util.intToStringMaxRadix(6); private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(7); private static final String FIELD_INTERFACE_VERSION = Util.intToStringMaxRadix(8); + private static final String FIELD_PLATFORM_TOKEN = Util.intToStringMaxRadix(9); - // Next field key = 9 + // Next field key = 10 @Override public Bundle toBundle() { @@ -227,11 +244,15 @@ import com.google.common.base.Objects; bundle.putParcelable(FIELD_COMPONENT_NAME, componentName); bundle.putBundle(FIELD_EXTRAS, extras); bundle.putInt(FIELD_INTERFACE_VERSION, interfaceVersion); + if (platformToken != null) { + bundle.putParcelable(FIELD_PLATFORM_TOKEN, platformToken); + } return bundle; } /** Restores a {@code SessionTokenImplBase} from a {@link Bundle}. */ - public static SessionTokenImplBase fromBundle(Bundle bundle) { + public static SessionTokenImplBase fromBundle( + Bundle bundle, @Nullable MediaSession.Token platformToken) { checkArgument(bundle.containsKey(FIELD_UID), "uid should be set."); int uid = bundle.getInt(FIELD_UID); checkArgument(bundle.containsKey(FIELD_TYPE), "type should be set."); @@ -244,6 +265,11 @@ import com.google.common.base.Objects; @Nullable IBinder iSession = BundleCompat.getBinder(bundle, FIELD_ISESSION); @Nullable ComponentName componentName = bundle.getParcelable(FIELD_COMPONENT_NAME); @Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS); + @Nullable + MediaSession.Token platformTokenFromBundle = bundle.getParcelable(FIELD_PLATFORM_TOKEN); + if (platformTokenFromBundle != null) { + platformToken = platformTokenFromBundle; + } return new SessionTokenImplBase( uid, type, @@ -253,6 +279,7 @@ import com.google.common.base.Objects; serviceName, componentName, iSession, - extras == null ? Bundle.EMPTY : extras); + extras == null ? Bundle.EMPTY : extras, + platformToken); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java index 0921cd3f24..540e276b7a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java @@ -24,6 +24,7 @@ import static androidx.media3.session.SessionToken.TYPE_SESSION; import static androidx.media3.session.SessionToken.TYPE_SESSION_LEGACY; import android.content.ComponentName; +import android.media.session.MediaSession; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.media3.common.util.Util; @@ -168,6 +169,12 @@ import com.google.common.base.Objects; return legacyToken; } + @Nullable + @Override + public MediaSession.Token getPlatformToken() { + return legacyToken == null ? null : (MediaSession.Token) legacyToken.getToken(); + } + private static final String FIELD_LEGACY_TOKEN = Util.intToStringMaxRadix(0); private static final String FIELD_UID = Util.intToStringMaxRadix(1); private static final String FIELD_TYPE = Util.intToStringMaxRadix(2); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java index f79ac58f0e..03146f4a46 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerTest.java @@ -79,6 +79,15 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest { return (MediaBrowser) controllerTestRule.createController(token, connectionHints, listener); } + @Test + public void getConnectedToken_returnSessionToken() throws Exception { + MediaBrowser browser = createBrowser(); + + assertThat(browser.getConnectedToken().isLegacySession()).isFalse(); + assertThat(browser.getConnectedToken().getType()).isEqualTo(SessionToken.TYPE_SESSION); + assertThat(browser.getConnectedToken().getPlatformToken()).isNotNull(); + } + @Test public void getLibraryRoot() throws Exception { LibraryParams params = diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/SessionTokenTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/SessionTokenTest.java index e8889938ca..1b97d02a55 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/SessionTokenTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/SessionTokenTest.java @@ -35,6 +35,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; @@ -148,19 +149,27 @@ public class SessionTokenTest { throws Exception { // TODO(b/194458970): Make the callback of session and controller on the same thread work and // remove the threadTestRule + AtomicReference platformToken = + new AtomicReference<>(); MediaSession session = threadTestRule .getHandler() .postAndSync( - () -> - sessionTestRule.ensureReleaseAfterTest( - new MediaSession.Builder(context, new MockPlayer.Builder().build()) - .setId(TAG) - .build())); + () -> { + MediaSession mediaSession = + new MediaSession.Builder(context, new MockPlayer.Builder().build()) + .setId(TAG) + .build(); + platformToken.set(mediaSession.getPlatformToken()); + return sessionTestRule.ensureReleaseAfterTest(mediaSession); + }); + SessionToken token = SessionToken.createSessionToken(context, session.getSessionCompatToken()) .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertThat(token.isLegacySession()).isFalse(); + assertThat(token.getPlatformToken()).isEqualTo(platformToken.get()); } @Test