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:
parent
1803d1cdb8
commit
03f0b53cf8
@ -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.
|
||||
|
@ -807,11 +807,10 @@ public class MediaSession {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
|
@ -501,8 +501,7 @@ public class MediaStyleNotificationHelper {
|
||||
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;
|
||||
}
|
||||
|
@ -28,12 +28,14 @@ import android.content.pm.ServiceInfo;
|
||||
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 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
|
||||
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, 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,
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -334,6 +381,8 @@ public final class SessionToken implements Bundleable {
|
||||
* </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<>();
|
||||
@ -370,6 +419,8 @@ public final class SessionToken implements Bundleable {
|
||||
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);
|
||||
@ -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 {
|
||||
|
||||
boolean isLegacySession();
|
||||
|
@ -26,7 +26,6 @@ import android.media.session.MediaSession;
|
||||
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 class MediaControllerWithFrameworkMediaSessionTest {
|
||||
@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 class MediaControllerWithFrameworkMediaSessionTest {
|
||||
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)
|
||||
|
@ -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.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.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;
|
||||
@ -68,6 +70,7 @@ public class SessionTokenTest {
|
||||
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 class SessionTokenTest {
|
||||
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 class SessionTokenTest {
|
||||
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 class SessionTokenTest {
|
||||
ComponentName mockBrowserServiceCompatName =
|
||||
new ComponentName(
|
||||
SUPPORT_APP_PACKAGE_NAME, MockMediaBrowserServiceCompat.class.getCanonicalName());
|
||||
|
||||
Set<SessionToken> serviceTokens =
|
||||
SessionToken.getAllServiceTokens(ApplicationProvider.getApplicationContext());
|
||||
for (SessionToken token : serviceTokens) {
|
||||
@ -162,6 +188,7 @@ public class SessionTokenTest {
|
||||
hasMockLibraryService2 = true;
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(hasMockBrowserServiceCompat).isTrue();
|
||||
assertThat(hasMockSessionService2).isTrue();
|
||||
assertThat(hasMockLibraryService2).isTrue();
|
||||
|
Loading…
x
Reference in New Issue
Block a user