Skip to content

Commit

Permalink
Add helper method to convert platform session token to Media3 token
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tonihei authored and icbaker committed Nov 24, 2022
1 parent 1803d1c commit 03f0b53
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 36 deletions.
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Release notes
([#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)).
* Remove deprecated symbols:
* Remove `DefaultAudioSink` constructors, use `DefaultAudioSink.Builder`
instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,11 +807,10 @@ public ListenableFuture<SessionResult> 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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<SessionToken> 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<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
public static ListenableFuture<SessionToken> createSessionToken(
Context context, MediaSessionCompat.Token compatToken) {
HandlerThread thread = new HandlerThread("SessionTokenThread");
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();
// 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,
Expand All @@ -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.
*
* <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
Expand All @@ -334,6 +381,8 @@ protected void onReceiveResult(int resultCode, Bundle resultData) {
* </intent>
* }</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) {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> services = new ArrayList<>();
Expand Down Expand Up @@ -370,6 +419,8 @@ public static ImmutableSet<SessionToken> getAllServiceTokens(Context context) {
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(
PackageManager manager, String serviceInterface, ComponentName serviceComponent) {
Intent serviceIntent = new Intent(serviceInterface);
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
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;
import android.os.Bundle;
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;
Expand Down Expand Up @@ -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);
Expand All @@ -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());
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -150,6 +175,7 @@ public void getSessionServiceTokens() {
ComponentName mockBrowserServiceCompatName =
new ComponentName(
SUPPORT_APP_PACKAGE_NAME, MockMediaBrowserServiceCompat.class.getCanonicalName());

Set<SessionToken> serviceTokens =
SessionToken.getAllServiceTokens(ApplicationProvider.getApplicationContext());
for (SessionToken token : serviceTokens) {
Expand All @@ -162,6 +188,7 @@ public void getSessionServiceTokens() {
hasMockLibraryService2 = true;
}
}

assertThat(hasMockBrowserServiceCompat).isTrue();
assertThat(hasMockSessionService2).isTrue();
assertThat(hasMockLibraryService2).isTrue();
Expand Down

0 comments on commit 03f0b53

Please sign in to comment.