Add helper method to convert platform session token to Media3 token

This avoids that apps have to depend on the legacy compat support
library when they want to make this conversion.

Also add a version to both helper methods that takes a Looper to
give apps the option to use an existing Looper, which should be
much faster than spinning up a new thread for every method call.

Issue: androidx/media#171
PiperOrigin-RevId: 490441913
This commit is contained in:
tonihei 2022-11-23 09:45:23 +00:00 committed by Ian Baker
parent 1803d1cdb8
commit 03f0b53cf8
6 changed files with 108 additions and 37 deletions

View File

@ -14,6 +14,9 @@ Release notes
([#10604](https://github.com/google/ExoPlayer/issues/10604)). ([#10604](https://github.com/google/ExoPlayer/issues/10604)).
* Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing * Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing
playback thread for a new ExoPlayer instance. playback thread for a new ExoPlayer instance.
* Session:
* Add helper method to convert platform session token to Media3
`SessionToken` ([#171](https://github.com/androidx/media/issues/171)).
* Remove deprecated symbols: * Remove deprecated symbols:
* Remove `DefaultAudioSink` constructors, use `DefaultAudioSink.Builder` * Remove `DefaultAudioSink` constructors, use `DefaultAudioSink.Builder`
instead. instead.

View File

@ -807,11 +807,10 @@ public class MediaSession {
/** /**
* Returns the {@link MediaSessionCompat.Token} of the {@link MediaSessionCompat} created * Returns the {@link MediaSessionCompat.Token} of the {@link MediaSessionCompat} created
* internally by this session. You may cast the {@link Object} to {@link * internally by this session.
* MediaSessionCompat.Token}.
*/ */
@UnstableApi @UnstableApi
public Object getSessionCompatToken() { public MediaSessionCompat.Token getSessionCompatToken() {
return impl.getSessionCompat().getSessionToken(); return impl.getSessionCompat().getSessionToken();
} }

View File

@ -501,8 +501,7 @@ public class MediaStyleNotificationHelper {
if (actionsToShowInCompact != null) { if (actionsToShowInCompact != null) {
setShowActionsInCompactView(style, actionsToShowInCompact); setShowActionsInCompactView(style, actionsToShowInCompact);
} }
MediaSessionCompat.Token legacyToken = MediaSessionCompat.Token legacyToken = session.getSessionCompatToken();
(MediaSessionCompat.Token) session.getSessionCompatToken();
style.setMediaSession((android.media.session.MediaSession.Token) legacyToken.getToken()); style.setMediaSession((android.media.session.MediaSession.Token) legacyToken.getToken());
return style; return style;
} }

View File

@ -28,12 +28,14 @@ import android.content.pm.ServiceInfo;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper;
import android.os.ResultReceiver; import android.os.ResultReceiver;
import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media.MediaBrowserServiceCompat; import androidx.media.MediaBrowserServiceCompat;
import androidx.media3.common.Bundleable; import androidx.media3.common.Bundleable;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -258,37 +260,86 @@ public final class SessionToken implements Bundleable {
} }
/** /**
* Creates a token from {@link MediaSessionCompat.Token}. * Creates a token from a {@link android.media.session.MediaSession.Token}.
* *
* @return a {@link ListenableFuture} of {@link SessionToken} * @param context A {@link Context}.
* @param token The {@link android.media.session.MediaSession.Token}.
* @return A {@link ListenableFuture} for the {@link SessionToken}.
*/
@SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession.
@UnstableApi
@RequiresApi(21)
public static ListenableFuture<SessionToken> createSessionToken(
Context context, android.media.session.MediaSession.Token token) {
return createSessionToken(context, MediaSessionCompat.Token.fromToken(token));
}
/**
* Creates a token from a {@link android.media.session.MediaSession.Token}.
*
* @param context A {@link Context}.
* @param token The {@link android.media.session.MediaSession.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
* {@link ListenableFuture}.
* @return A {@link ListenableFuture} for the {@link SessionToken}.
*/
@SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession.
@UnstableApi
@RequiresApi(21)
public static ListenableFuture<SessionToken> createSessionToken(
Context context, android.media.session.MediaSession.Token token, Looper completionLooper) {
return createSessionToken(context, MediaSessionCompat.Token.fromToken(token), completionLooper);
}
/**
* Creates a token from a {@link MediaSessionCompat.Token}.
*
* @param context A {@link Context}.
* @param compatToken The {@link MediaSessionCompat.Token}.
* @return A {@link ListenableFuture} for the {@link SessionToken}.
*/ */
@UnstableApi @UnstableApi
public static ListenableFuture<SessionToken> createSessionToken( public static ListenableFuture<SessionToken> createSessionToken(
Context context, Object compatToken) { Context context, MediaSessionCompat.Token compatToken) {
checkNotNull(context, "context must not be null");
checkNotNull(compatToken, "compatToken must not be null");
checkArgument(compatToken instanceof MediaSessionCompat.Token);
HandlerThread thread = new HandlerThread("SessionTokenThread"); HandlerThread thread = new HandlerThread("SessionTokenThread");
thread.start(); thread.start();
ListenableFuture<SessionToken> tokenFuture =
createSessionToken(context, compatToken, thread.getLooper());
tokenFuture.addListener(thread::quit, MoreExecutors.directExecutor());
return tokenFuture;
}
/**
* Creates a token from a {@link MediaSessionCompat.Token}.
*
* @param context A {@link Context}.
* @param compatToken The {@link 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
* {@link ListenableFuture}.
* @return A {@link ListenableFuture} for the {@link SessionToken}.
*/
@UnstableApi
public static ListenableFuture<SessionToken> createSessionToken(
Context context, MediaSessionCompat.Token compatToken, Looper completionLooper) {
checkNotNull(context, "context must not be null");
checkNotNull(compatToken, "compatToken must not be null");
SettableFuture<SessionToken> future = SettableFuture.create(); SettableFuture<SessionToken> future = SettableFuture.create();
// Try retrieving media3 token by connecting to the session. // Try retrieving media3 token by connecting to the session.
MediaControllerCompat controller = MediaControllerCompat controller = new MediaControllerCompat(context, compatToken);
createMediaControllerCompat(context, (MediaSessionCompat.Token) compatToken);
String packageName = controller.getPackageName(); String packageName = controller.getPackageName();
Handler handler = new Handler(thread.getLooper()); Handler handler = new Handler(completionLooper);
Runnable createFallbackLegacyToken = Runnable createFallbackLegacyToken =
() -> { () -> {
int uid = getUid(context.getPackageManager(), packageName); int uid = getUid(context.getPackageManager(), packageName);
SessionToken resultToken = SessionToken resultToken =
new SessionToken( new SessionToken(compatToken, packageName, uid, controller.getSessionInfo());
(MediaSessionCompat.Token) compatToken,
packageName,
uid,
controller.getSessionInfo());
future.set(resultToken); future.set(resultToken);
}; };
// Post creating a fallback token if the command receives no result after a timeout.
handler.postDelayed(createFallbackLegacyToken, WAIT_TIME_MS_FOR_SESSION3_TOKEN);
controller.sendCommand( controller.sendCommand(
MediaConstants.SESSION_COMMAND_REQUEST_SESSION3_TOKEN, MediaConstants.SESSION_COMMAND_REQUEST_SESSION3_TOKEN,
/* params= */ null, /* params= */ null,
@ -306,17 +357,13 @@ public final class SessionToken implements Bundleable {
} }
} }
}); });
// Post creating a fallback token if the command receives no result after a timeout.
handler.postDelayed(createFallbackLegacyToken, WAIT_TIME_MS_FOR_SESSION3_TOKEN);
future.addListener(() -> thread.quit(), MoreExecutors.directExecutor());
return future; return future;
} }
/** /**
* Returns a {@link ImmutableSet} of {@link SessionToken} for media session services; {@link * Returns an {@link ImmutableSet} of {@linkplain SessionToken session tokens} for media session
* MediaSessionService}, {@link MediaLibraryService}, and {@link MediaBrowserServiceCompat} * services; {@link MediaSessionService}, {@link MediaLibraryService}, and {@link
* regardless of their activeness. This set represents media apps that publish {@link * MediaBrowserServiceCompat} regardless of their activeness.
* MediaSession}.
* *
* <p>The app targeting API level 30 or higher must include a {@code <queries>} element in their * <p>The app targeting API level 30 or higher must include a {@code <queries>} element in their
* manifest to get service tokens of other apps. See the following example and <a * manifest to get service tokens of other apps. See the following example and <a
@ -334,6 +381,8 @@ public final class SessionToken implements Bundleable {
* </intent> * </intent>
* }</pre> * }</pre>
*/ */
// We ask the app to declare the <queries> tags, so it's expected that they are missing.
@SuppressWarnings("QueryPermissionsNeeded")
public static ImmutableSet<SessionToken> getAllServiceTokens(Context context) { public static ImmutableSet<SessionToken> getAllServiceTokens(Context context) {
PackageManager pm = context.getPackageManager(); PackageManager pm = context.getPackageManager();
List<ResolveInfo> services = new ArrayList<>(); List<ResolveInfo> services = new ArrayList<>();
@ -370,6 +419,8 @@ public final class SessionToken implements Bundleable {
return sessionServiceTokens.build(); return sessionServiceTokens.build();
} }
// We ask the app to declare the <queries> tags, so it's expected that they are missing.
@SuppressWarnings("QueryPermissionsNeeded")
private static boolean isInterfaceDeclared( private static boolean isInterfaceDeclared(
PackageManager manager, String serviceInterface, ComponentName serviceComponent) { PackageManager manager, String serviceInterface, ComponentName serviceComponent) {
Intent serviceIntent = new Intent(serviceInterface); Intent serviceIntent = new Intent(serviceInterface);
@ -402,11 +453,6 @@ public final class SessionToken implements Bundleable {
} }
} }
private static MediaControllerCompat createMediaControllerCompat(
Context context, MediaSessionCompat.Token sessionToken) {
return new MediaControllerCompat(context, sessionToken);
}
/* package */ interface SessionTokenImpl extends Bundleable { /* package */ interface SessionTokenImpl extends Bundleable {
boolean isLegacySession(); boolean isLegacySession();

View File

@ -26,7 +26,6 @@ import android.media.session.MediaSession;
import android.media.session.PlaybackState; import android.media.session.PlaybackState;
import android.os.Build; import android.os.Build;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Player.State; import androidx.media3.common.Player.State;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
@ -94,8 +93,7 @@ public class MediaControllerWithFrameworkMediaSessionTest {
@Test @Test
public void createController() throws Exception { public void createController() throws Exception {
SessionToken token = SessionToken token =
SessionToken.createSessionToken( SessionToken.createSessionToken(context, fwkSession.getSessionToken())
context, MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken()))
.get(TIMEOUT_MS, MILLISECONDS); .get(TIMEOUT_MS, MILLISECONDS);
MediaController controller = MediaController controller =
new MediaController.Builder(context, token) new MediaController.Builder(context, token)
@ -111,8 +109,7 @@ public class MediaControllerWithFrameworkMediaSessionTest {
AtomicInteger playbackStateRef = new AtomicInteger(); AtomicInteger playbackStateRef = new AtomicInteger();
AtomicBoolean playWhenReadyRef = new AtomicBoolean(); AtomicBoolean playWhenReadyRef = new AtomicBoolean();
SessionToken token = SessionToken token =
SessionToken.createSessionToken( SessionToken.createSessionToken(context, fwkSession.getSessionToken())
context, MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken()))
.get(TIMEOUT_MS, MILLISECONDS); .get(TIMEOUT_MS, MILLISECONDS);
MediaController controller = MediaController controller =
new MediaController.Builder(context, token) new MediaController.Builder(context, token)

View File

@ -20,6 +20,7 @@ import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_SE
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeTrue;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@ -27,6 +28,7 @@ import android.os.Bundle;
import android.os.Process; import android.os.Process;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.util.Util;
import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.MainLooperTestRule; import androidx.media3.test.session.common.MainLooperTestRule;
import androidx.media3.test.session.common.TestUtils; import androidx.media3.test.session.common.TestUtils;
@ -68,6 +70,7 @@ public class SessionTokenTest {
context, context,
new ComponentName( new ComponentName(
context.getPackageName(), MockMediaSessionService.class.getCanonicalName())); context.getPackageName(), MockMediaSessionService.class.getCanonicalName()));
assertThat(token.getPackageName()).isEqualTo(context.getPackageName()); assertThat(token.getPackageName()).isEqualTo(context.getPackageName());
assertThat(token.getUid()).isEqualTo(Process.myUid()); assertThat(token.getUid()).isEqualTo(Process.myUid());
assertThat(token.getType()).isEqualTo(SessionToken.TYPE_SESSION_SERVICE); assertThat(token.getType()).isEqualTo(SessionToken.TYPE_SESSION_SERVICE);
@ -80,6 +83,7 @@ public class SessionTokenTest {
ComponentName testComponentName = ComponentName testComponentName =
new ComponentName( new ComponentName(
context.getPackageName(), MockMediaLibraryService.class.getCanonicalName()); context.getPackageName(), MockMediaLibraryService.class.getCanonicalName());
SessionToken token = new SessionToken(context, testComponentName); SessionToken token = new SessionToken(context, testComponentName);
assertThat(token.getPackageName()).isEqualTo(context.getPackageName()); assertThat(token.getPackageName()).isEqualTo(context.getPackageName());
@ -110,15 +114,36 @@ public class SessionTokenTest {
assertThat(token.getServiceName()).isEmpty(); assertThat(token.getServiceName()).isEmpty();
} }
@Test
public void createSessionToken_withPlatformTokenFromMedia1Session_returnsTokenForLegacySession()
throws Exception {
assumeTrue(Util.SDK_INT >= 21);
MediaSessionCompat sessionCompat =
sessionTestRule.ensureReleaseAfterTest(
new MediaSessionCompat(context, "createSessionToken_withLegacyToken"));
SessionToken token =
SessionToken.createSessionToken(
context,
(android.media.session.MediaSession.Token)
sessionCompat.getSessionToken().getToken())
.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(token.isLegacySession()).isTrue();
}
@Test @Test
public void createSessionToken_withCompatTokenFromMedia1Session_returnsTokenForLegacySession() public void createSessionToken_withCompatTokenFromMedia1Session_returnsTokenForLegacySession()
throws Exception { throws Exception {
MediaSessionCompat sessionCompat = MediaSessionCompat sessionCompat =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSessionCompat(context, "createSessionToken_withLegacyToken")); new MediaSessionCompat(context, "createSessionToken_withLegacyToken"));
SessionToken token = SessionToken token =
SessionToken.createSessionToken(context, sessionCompat.getSessionToken()) SessionToken.createSessionToken(context, sessionCompat.getSessionToken())
.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(token.isLegacySession()).isTrue(); assertThat(token.isLegacySession()).isTrue();
} }
@ -150,6 +175,7 @@ public class SessionTokenTest {
ComponentName mockBrowserServiceCompatName = ComponentName mockBrowserServiceCompatName =
new ComponentName( new ComponentName(
SUPPORT_APP_PACKAGE_NAME, MockMediaBrowserServiceCompat.class.getCanonicalName()); SUPPORT_APP_PACKAGE_NAME, MockMediaBrowserServiceCompat.class.getCanonicalName());
Set<SessionToken> serviceTokens = Set<SessionToken> serviceTokens =
SessionToken.getAllServiceTokens(ApplicationProvider.getApplicationContext()); SessionToken.getAllServiceTokens(ApplicationProvider.getApplicationContext());
for (SessionToken token : serviceTokens) { for (SessionToken token : serviceTokens) {
@ -162,6 +188,7 @@ public class SessionTokenTest {
hasMockLibraryService2 = true; hasMockLibraryService2 = true;
} }
} }
assertThat(hasMockBrowserServiceCompat).isTrue(); assertThat(hasMockBrowserServiceCompat).isTrue();
assertThat(hasMockSessionService2).isTrue(); assertThat(hasMockSessionService2).isTrue();
assertThat(hasMockLibraryService2).isTrue(); assertThat(hasMockLibraryService2).isTrue();