Add platform token to Media3 SessionToken

Access is package-private and it will allow the media controller logic
to interact with the underlying platform session directly if needed.

Interop: When a MediaController connects to an older session (before this
change), it won't get the platform token from the session directly.
Many controllers will be set up with a platform or compat token though
and we can simply keep the already known token and use it. The only
cases where we still don't have a platform token in the MediaController
are the cases where the controller is created with a SessionToken based
on a ComponentName.
PiperOrigin-RevId: 678230977
This commit is contained in:
tonihei 2024-09-24 06:58:55 -07:00 committed by Copybara-Service
parent d8dc513431
commit 43765b7567
11 changed files with 140 additions and 37 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<android.media.session.MediaSession.Token> 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