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