From 9829ff3d4cb58584bbe87a2f04381cc71318ac47 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Nov 2022 09:45:23 +0000 Subject: [PATCH] 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 (cherry picked from commit 03f0b53cf823bb4878f884c055b550b52b9b57ab) --- RELEASENOTES.md | 5 + .../androidx/media3/session/MediaSession.java | 5 +- .../session/MediaStyleNotificationHelper.java | 3 +- .../androidx/media3/session/SessionToken.java | 98 ++++++++++++++----- ...ntrollerWithFrameworkMediaSessionTest.java | 7 +- .../media3/session/SessionTokenTest.java | 27 +++++ 6 files changed, 109 insertions(+), 36 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 50b763ca1ae..854f943d895 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,11 @@ selector, hardware decoder with only functional support will be preferred over software decoder that fully supports the format ([#10604](https://github.com/google/ExoPlayer/issues/10604)). + * Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing + 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)). ### 1.0.0-beta03 (2022-11-22) 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 2697fd11dd0..9da5c30fe5e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -807,11 +807,10 @@ public ListenableFuture sendCustomCommand( /** * Returns the {@link MediaSessionCompat.Token} of the {@link MediaSessionCompat} created - * internally by this session. You may cast the {@link Object} to {@link - * MediaSessionCompat.Token}. + * internally by this session. */ @UnstableApi - public Object getSessionCompatToken() { + public MediaSessionCompat.Token getSessionCompatToken() { return impl.getSessionCompat().getSessionToken(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java b/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java index b15df339195..739c0baefa1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java @@ -501,8 +501,7 @@ public static Notification.MediaStyle fillInMediaStyle( if (actionsToShowInCompact != null) { setShowActionsInCompactView(style, actionsToShowInCompact); } - MediaSessionCompat.Token legacyToken = - (MediaSessionCompat.Token) session.getSessionCompatToken(); + MediaSessionCompat.Token legacyToken = session.getSessionCompatToken(); style.setMediaSession((android.media.session.MediaSession.Token) legacyToken.getToken()); return style; } 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 09c5e61de73..7a25ccc1c9d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java @@ -28,12 +28,14 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.os.ResultReceiver; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.media.MediaBrowserServiceCompat; import androidx.media3.common.Bundleable; import androidx.media3.common.C; @@ -258,37 +260,86 @@ public Bundle getExtras() { } /** - * 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 createSessionToken( - Context context, Object compatToken) { - checkNotNull(context, "context must not be null"); - checkNotNull(compatToken, "compatToken must not be null"); - checkArgument(compatToken instanceof MediaSessionCompat.Token); + 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 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 + public static ListenableFuture createSessionToken( + Context context, MediaSessionCompat.Token compatToken) { HandlerThread thread = new HandlerThread("SessionTokenThread"); thread.start(); + ListenableFuture 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 createSessionToken( + Context context, MediaSessionCompat.Token compatToken, Looper completionLooper) { + checkNotNull(context, "context must not be null"); + checkNotNull(compatToken, "compatToken must not be null"); SettableFuture future = SettableFuture.create(); // Try retrieving media3 token by connecting to the session. - MediaControllerCompat controller = - createMediaControllerCompat(context, (MediaSessionCompat.Token) compatToken); + MediaControllerCompat controller = new MediaControllerCompat(context, compatToken); String packageName = controller.getPackageName(); - Handler handler = new Handler(thread.getLooper()); + Handler handler = new Handler(completionLooper); Runnable createFallbackLegacyToken = () -> { int uid = getUid(context.getPackageManager(), packageName); SessionToken resultToken = - new SessionToken( - (MediaSessionCompat.Token) compatToken, - packageName, - uid, - controller.getSessionInfo()); + new SessionToken(compatToken, packageName, uid, controller.getSessionInfo()); 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( MediaConstants.SESSION_COMMAND_REQUEST_SESSION3_TOKEN, /* params= */ null, @@ -306,17 +357,13 @@ protected void onReceiveResult(int resultCode, Bundle resultData) { } } }); - // 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; } /** - * Returns a {@link ImmutableSet} of {@link SessionToken} for media session services; {@link - * MediaSessionService}, {@link MediaLibraryService}, and {@link MediaBrowserServiceCompat} - * regardless of their activeness. This set represents media apps that publish {@link - * MediaSession}. + * Returns an {@link ImmutableSet} of {@linkplain SessionToken session tokens} for media session + * services; {@link MediaSessionService}, {@link MediaLibraryService}, and {@link + * MediaBrowserServiceCompat} regardless of their activeness. * *

The app targeting API level 30 or higher must include a {@code } element in their * manifest to get service tokens of other apps. See the following example and * } */ + // We ask the app to declare the tags, so it's expected that they are missing. + @SuppressWarnings("QueryPermissionsNeeded") public static ImmutableSet getAllServiceTokens(Context context) { PackageManager pm = context.getPackageManager(); List services = new ArrayList<>(); @@ -370,6 +419,8 @@ public static ImmutableSet getAllServiceTokens(Context context) { return sessionServiceTokens.build(); } + // We ask the app to declare the tags, so it's expected that they are missing. + @SuppressWarnings("QueryPermissionsNeeded") private static boolean isInterfaceDeclared( PackageManager manager, String serviceInterface, ComponentName serviceComponent) { Intent serviceIntent = new Intent(serviceInterface); @@ -402,11 +453,6 @@ private static int getUid(PackageManager manager, String packageName) { } } - private static MediaControllerCompat createMediaControllerCompat( - Context context, MediaSessionCompat.Token sessionToken) { - return new MediaControllerCompat(context, sessionToken); - } - /* package */ interface SessionTokenImpl extends Bundleable { boolean isLegacySession(); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithFrameworkMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithFrameworkMediaSessionTest.java index 3c9102296c3..6fe9bd89577 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithFrameworkMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithFrameworkMediaSessionTest.java @@ -26,7 +26,6 @@ import android.media.session.PlaybackState; import android.os.Build; import android.os.HandlerThread; -import android.support.v4.media.session.MediaSessionCompat; import androidx.media3.common.Player; import androidx.media3.common.Player.State; import androidx.media3.common.util.Util; @@ -94,8 +93,7 @@ public void cleanUp() { @Test public void createController() throws Exception { SessionToken token = - SessionToken.createSessionToken( - context, MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken())) + SessionToken.createSessionToken(context, fwkSession.getSessionToken()) .get(TIMEOUT_MS, MILLISECONDS); MediaController controller = new MediaController.Builder(context, token) @@ -111,8 +109,7 @@ public void onPlaybackStateChanged_isNotifiedByFwkSessionChanges() throws Except AtomicInteger playbackStateRef = new AtomicInteger(); AtomicBoolean playWhenReadyRef = new AtomicBoolean(); SessionToken token = - SessionToken.createSessionToken( - context, MediaSessionCompat.Token.fromToken(fwkSession.getSessionToken())) + SessionToken.createSessionToken(context, fwkSession.getSessionToken()) .get(TIMEOUT_MS, MILLISECONDS); MediaController controller = new MediaController.Builder(context, token) 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 b2d54e2abf7..f0efaea3de2 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 @@ -20,6 +20,7 @@ import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeTrue; import android.content.ComponentName; import android.content.Context; @@ -27,6 +28,7 @@ import android.os.Process; import android.support.v4.media.session.MediaSessionCompat; import androidx.media3.common.MediaLibraryInfo; +import androidx.media3.common.util.Util; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; import androidx.media3.test.session.common.TestUtils; @@ -68,6 +70,7 @@ public void constructor_sessionService() { context, new ComponentName( context.getPackageName(), MockMediaSessionService.class.getCanonicalName())); + assertThat(token.getPackageName()).isEqualTo(context.getPackageName()); assertThat(token.getUid()).isEqualTo(Process.myUid()); assertThat(token.getType()).isEqualTo(SessionToken.TYPE_SESSION_SERVICE); @@ -80,6 +83,7 @@ public void constructor_libraryService() { ComponentName testComponentName = new ComponentName( context.getPackageName(), MockMediaLibraryService.class.getCanonicalName()); + SessionToken token = new SessionToken(context, testComponentName); assertThat(token.getPackageName()).isEqualTo(context.getPackageName()); @@ -110,15 +114,36 @@ public void getters_whenCreatedBySession() { 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 public void createSessionToken_withCompatTokenFromMedia1Session_returnsTokenForLegacySession() throws Exception { MediaSessionCompat sessionCompat = sessionTestRule.ensureReleaseAfterTest( new MediaSessionCompat(context, "createSessionToken_withLegacyToken")); + SessionToken token = SessionToken.createSessionToken(context, sessionCompat.getSessionToken()) .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertThat(token.isLegacySession()).isTrue(); } @@ -150,6 +175,7 @@ public void getSessionServiceTokens() { ComponentName mockBrowserServiceCompatName = new ComponentName( SUPPORT_APP_PACKAGE_NAME, MockMediaBrowserServiceCompat.class.getCanonicalName()); + Set serviceTokens = SessionToken.getAllServiceTokens(ApplicationProvider.getApplicationContext()); for (SessionToken token : serviceTokens) { @@ -162,6 +188,7 @@ public void getSessionServiceTokens() { hasMockLibraryService2 = true; } } + assertThat(hasMockBrowserServiceCompat).isTrue(); assertThat(hasMockSessionService2).isTrue(); assertThat(hasMockLibraryService2).isTrue();