Implement session extras for Media3 and legacy controllers

This provides an (unstable) API for apps to broadcast session extras
Bundle to all connected controllers and set the extras in the legacy
session.

Similar to the custom layout, the extras Bundle is not part of the
Media3 session state. This means that when a Media3 controller
connects to the session after the broadcast, the extras needs to be
sent to that controller in  `MediaSession.Callback.onPostConnect(MediaSession session, ControllerInfo controller)`.

PiperOrigin-RevId: 451871731
This commit is contained in:
bachinger 2022-05-30 14:24:42 +00:00 committed by Marc Baechinger
parent a629d09458
commit 85a936ecb1
22 changed files with 260 additions and 12 deletions

View File

@ -42,7 +42,8 @@ oneway interface IMediaController {
void onAvailableCommandsChangedFromSession(
int seq, in Bundle sessionCommandsBundle, in Bundle playerCommandsBundle) = 3009;
void onRenderedFirstFrame(int seq) = 3010;
// Next Id for MediaController: 3011
void onExtrasChanged(int seq, in Bundle extras) = 3011;
// Next Id for MediaController: 3012
void onChildrenChanged(
int seq, String parentId, int itemCount, in @nullable Bundle libraryParams) = 4000;

View File

@ -151,8 +151,6 @@ public final class MediaConstants {
*/
public static final int ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT = 3;
/* package */ static final String SESSION_COMMAND_ON_EXTRAS_CHANGED =
"androidx.media3.session.SESSION_COMMAND_ON_EXTRAS_CHANGED";
/* package */ static final String SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED =
"androidx.media3.session.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED";
/* package */ static final String SESSION_COMMAND_REQUEST_SESSION3_TOKEN =

View File

@ -322,6 +322,14 @@ public class MediaController implements Player {
MediaController controller, SessionCommand command, Bundle args) {
return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED));
}
/**
* Called when the session extras have changed.
*
* @param controller The controller.
* @param extras The session extras that have changed.
*/
default void onExtrasChanged(MediaController controller, Bundle extras) {}
}
/* package */ interface ConnectionCallback {

View File

@ -2598,6 +2598,13 @@ import org.checkerframework.checker.nullness.qual.NonNull;
});
}
public void onExtrasChanged(Bundle extras) {
if (!isConnected()) {
return;
}
instance.notifyControllerListener(listener -> listener.onExtrasChanged(instance, extras));
}
public void onRenderedFirstFrame() {
listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onRenderedFirstFrame);
}

View File

@ -38,7 +38,6 @@ import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_QUERY;
import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_URI;
import static androidx.media3.session.MediaConstants.MEDIA_URI_SET_MEDIA_URI_PREFIX;
import static androidx.media3.session.MediaConstants.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED;
import static androidx.media3.session.MediaConstants.SESSION_COMMAND_ON_EXTRAS_CHANGED;
import static androidx.media3.session.MediaUtils.POSITION_DIFF_TOLERANCE_MS;
import static androidx.media3.session.MediaUtils.calculateBufferedPercentage;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
@ -1606,14 +1605,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Override
public void onExtrasChanged(Bundle extras) {
instance.notifyControllerListener(
listener ->
ignoreFuture(
listener.onCustomCommand(
instance,
new SessionCommand(
SESSION_COMMAND_ON_EXTRAS_CHANGED, /* extras= */ Bundle.EMPTY),
extras)));
instance.notifyControllerListener(listener -> listener.onExtrasChanged(instance, extras));
}
@Override

View File

@ -183,6 +183,11 @@ import java.util.List;
isTimelineExcluded));
}
@Override
public void onExtrasChanged(int seq, Bundle extras) {
dispatchControllerTaskOnHandler(controller -> controller.onExtrasChanged(extras));
}
@Override
public void onRenderedFirstFrame(int seq) {
dispatchControllerTaskOnHandler(MediaControllerImplBase::onRenderedFirstFrame);

View File

@ -702,6 +702,32 @@ public class MediaSession {
impl.broadcastCustomCommand(command, args);
}
/**
* Sends the session extras to connected controllers.
*
* <p>This is a synchronous call and doesn't wait for results from the controllers.
*
* @param sessionExtras The session extras.
*/
public void setSessionExtras(Bundle sessionExtras) {
checkNotNull(sessionExtras);
impl.setSessionExtras(sessionExtras);
}
/**
* Sends the session extras to the connected controller.
*
* <p>This is a synchronous call and doesn't wait for results from the controller.
*
* @param controller The controller to send the extras to.
* @param sessionExtras The session extras.
*/
public void setSessionExtras(ControllerInfo controller, Bundle sessionExtras) {
checkNotNull(controller, "controller must not be null");
checkNotNull(sessionExtras);
impl.setSessionExtras(controller, sessionExtras);
}
/**
* Sends a custom command to a specific controller.
*
@ -1119,6 +1145,8 @@ public class MediaSession {
default void setCustomLayout(int seq, List<CommandButton> layout) throws RemoteException {}
default void onSessionExtrasChanged(int seq, Bundle sessionExtras) throws RemoteException {}
default void sendCustomCommand(int seq, SessionCommand command, Bundle args)
throws RemoteException {}

View File

@ -342,6 +342,18 @@ import org.checkerframework.checker.initialization.qual.Initialized;
(controller, seq) -> controller.setCustomLayout(seq, layout));
}
public void setSessionExtras(Bundle sessionExtras) {
dispatchRemoteControllerTaskWithoutReturn(
(controller, seq) -> controller.onSessionExtrasChanged(seq, sessionExtras));
}
public void setSessionExtras(ControllerInfo controller, Bundle sessionExtras) {
if (sessionStub.getConnectedControllersManager().isConnected(controller)) {
dispatchRemoteControllerTaskWithoutReturn(
controller, (callback, seq) -> callback.onSessionExtrasChanged(seq, sessionExtras));
}
}
public void setAvailableCommands(
ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) {
if (sessionStub.getConnectedControllersManager().isConnected(controller)) {

View File

@ -886,6 +886,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
.setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat());
}
@Override
public void onSessionExtrasChanged(int seq, Bundle sessionExtras) {
sessionImpl.getSessionCompat().setExtras(sessionExtras);
}
@Override
public void onPlayWhenReadyChanged(
int seq, boolean playWhenReady, @Player.PlaybackSuppressionReason int reason)

View File

@ -1801,6 +1801,11 @@ import java.util.concurrent.ExecutionException;
iController.onRenderedFirstFrame(seq);
}
@Override
public void onSessionExtrasChanged(int seq, Bundle sessionExtras) throws RemoteException {
iController.onExtrasChanged(seq, sessionExtras);
}
@Override
public int hashCode() {
return ObjectsCompat.hash(getCallbackBinder());

View File

@ -32,6 +32,8 @@ interface IRemoteMediaSession {
void release(String sessionId);
void setAvailableCommands(String sessionId, in Bundle sessionCommands, in Bundle playerCommands);
void setCustomLayout(String sessionId, in List<Bundle> layout);
void setSessionExtras(String sessionId, in Bundle extras);
void setSessionExtrasForController(String sessionId, in String controllerKey, in Bundle extras);
// Player Methods
void setPlayWhenReady(String sessionId, boolean playWhenReady, int reason);

View File

@ -41,4 +41,5 @@ interface IRemoteMediaSessionCompat {
void setRatingType(String sessionTag, int type);
void sendSessionEvent(String sessionTag, String event, in Bundle extras);
void setCaptioningEnabled(String sessionTag, boolean enabled);
void setSessionExtras(String sessionTag, in Bundle extras);
}

View File

@ -29,6 +29,7 @@ public class MediaSessionConstants {
// Bundle keys
public static final String KEY_AVAILABLE_SESSION_COMMANDS = "availableSessionCommands";
public static final String KEY_CONTROLLER = "controllerKey";
private MediaSessionConstants() {}
}

View File

@ -27,6 +27,7 @@ import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.MainLooperTestRule;
import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
@ -137,4 +138,42 @@ public class MediaControllerCompatCallbackWithMediaSessionCompatTest {
assertThat(receivedIconResIds).containsExactly(1, 2).inOrder();
assertThat(receivedBundleValues).containsExactly("value-1", "value-2").inOrder();
}
/**
* Setting the session extras is used for instance by <a
* href="http://android-doc.github.io/reference/android/support/wearable/media/MediaControlConstants.html">
* Wear OS</a> and System UI (starting with T) to receive extras for UI customization. An app
* needs a way to set the session extras that are stored in the legacy session and broadcast to
* the connected controllers.
*/
@Test
public void setExtras_onExtrasChangedCalled() throws Exception {
Bundle sessionExtras = new Bundle();
sessionExtras.putString("key-1", "value-1");
CountDownLatch countDownLatch = new CountDownLatch(1);
MediaSessionCompat.Token sessionToken = session.getSessionToken();
List<Bundle> receivedSessionExtras = new ArrayList<>();
threadTestRule
.getHandler()
.postAndSync(
() -> {
MediaControllerCompat mediaControllerCompat =
new MediaControllerCompat(context, sessionToken);
mediaControllerCompat.registerCallback(
new MediaControllerCompat.Callback() {
@Override
public void onExtrasChanged(Bundle extras) {
receivedSessionExtras.add(extras);
receivedSessionExtras.add(mediaControllerCompat.getExtras());
countDownLatch.countDown();
}
});
});
session.setExtras(sessionExtras);
assertThat(countDownLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(TestUtils.equals(receivedSessionExtras.get(0), sessionExtras)).isTrue();
assertThat(TestUtils.equals(receivedSessionExtras.get(1), sessionExtras)).isTrue();
}
}

View File

@ -819,6 +819,30 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
assertThat(receivedBundleValues).containsExactly("value-1", "value-2").inOrder();
}
@Test
public void setSessionExtras_cnExtrasChangedCalled() throws Exception {
Bundle sessionExtras = new Bundle();
sessionExtras.putString("key-0", "value-0");
CountDownLatch latch = new CountDownLatch(1);
List<Bundle> receivedSessionExtras = new ArrayList<>();
MediaControllerCompat.Callback callback =
new MediaControllerCompat.Callback() {
@Override
public void onExtrasChanged(Bundle extras) {
receivedSessionExtras.add(extras);
receivedSessionExtras.add(controllerCompat.getExtras());
latch.countDown();
}
};
controllerCompat.registerCallback(callback, handler);
session.setSessionExtras(sessionExtras);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(TestUtils.equals(receivedSessionExtras.get(0), sessionExtras)).isTrue();
assertThat(TestUtils.equals(receivedSessionExtras.get(1), sessionExtras)).isTrue();
}
@Test
public void currentMediaItemChange() throws Exception {
int testItemIndex = 3;

View File

@ -28,6 +28,7 @@ import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
import static androidx.media3.test.session.common.CommonConstants.DEFAULT_TEST_NAME;
import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_LIBRARY_SERVICE;
import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_SESSION_SERVICE;
import static androidx.media3.test.session.common.MediaSessionConstants.KEY_CONTROLLER;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_CONTROLLER_LISTENER_SESSION_REJECTS;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_WITH_CUSTOM_COMMANDS;
import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS;
@ -1800,6 +1801,55 @@ public class MediaControllerListenerTest {
assertThat(receivedBundleValues).containsExactly("value-1", "value-2").inOrder();
}
@Test
public void setSessionExtras_onExtrasChangedCalled() throws Exception {
Bundle sessionExtras = TestUtils.createTestBundle();
sessionExtras.putString("key-0", "value-0");
CountDownLatch latch = new CountDownLatch(1);
List<Bundle> receivedSessionExtras = new ArrayList<>();
MediaController.Listener listener =
new MediaController.Listener() {
@Override
public void onExtrasChanged(MediaController controller, Bundle extras) {
receivedSessionExtras.add(extras);
latch.countDown();
}
};
controllerTestRule.createController(
remoteSession.getToken(), /* connectionHints= */ null, listener);
remoteSession.setSessionExtras(sessionExtras);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(receivedSessionExtras).hasSize(1);
assertThat(TestUtils.equals(receivedSessionExtras.get(0), sessionExtras)).isTrue();
}
@Test
public void setSessionExtras_specificMedia3Controller_onExtrasChangedCalled() throws Exception {
Bundle sessionExtras = TestUtils.createTestBundle();
sessionExtras.putString("key-0", "value-0");
CountDownLatch latch = new CountDownLatch(1);
List<Bundle> receivedSessionExtras = new ArrayList<>();
MediaController.Listener listener =
new MediaController.Listener() {
@Override
public void onExtrasChanged(MediaController controller, Bundle extras) {
receivedSessionExtras.add(extras);
latch.countDown();
}
};
Bundle connectionHints = new Bundle();
connectionHints.putString(KEY_CONTROLLER, "controller_key_1");
controllerTestRule.createController(remoteSession.getToken(), connectionHints, listener);
remoteSession.setSessionExtras("controller_key_1", sessionExtras);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(receivedSessionExtras).hasSize(1);
assertThat(TestUtils.equals(receivedSessionExtras.get(0), sessionExtras)).isTrue();
}
@Test
public void onVideoSizeChanged() throws Exception {
VideoSize testVideoSize =

View File

@ -32,6 +32,7 @@ import androidx.media3.common.FlagSet;
import androidx.media3.common.Player;
import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.MainLooperTestRule;
import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
@ -168,4 +169,26 @@ public class MediaControllerListenerWithMediaSessionCompatTest {
assertThat(receivedIconResIds).containsExactly(1, 2).inOrder();
assertThat(receivedBundleValues).containsExactly("value-1", "value-2").inOrder();
}
@Test
public void setSessionExtras_onExtrasChangedCalled() throws Exception {
Bundle sessionExtras = new Bundle();
sessionExtras.putString("key-1", "value-1");
CountDownLatch countDownLatch = new CountDownLatch(1);
List<Bundle> receivedSessionExtras = new ArrayList<>();
controllerTestRule.createController(
session.getSessionToken(),
new MediaController.Listener() {
@Override
public void onExtrasChanged(MediaController controller, Bundle extras) {
receivedSessionExtras.add(extras);
countDownLatch.countDown();
}
});
session.setExtras(sessionExtras);
assertThat(countDownLatch.await(1_000, MILLISECONDS)).isTrue();
assertThat(TestUtils.equals(receivedSessionExtras.get(0), sessionExtras)).isTrue();
}
}

View File

@ -216,5 +216,11 @@ public class MediaSessionCompatProviderService extends Service {
MediaSessionCompat session = sessionMap.get(sessionTag);
session.setCaptioningEnabled(enabled);
}
@Override
public void setSessionExtras(String sessionTag, Bundle extras) throws RemoteException {
MediaSessionCompat session = sessionMap.get(sessionTag);
session.setExtras(extras);
}
}
}

View File

@ -54,6 +54,7 @@ import static androidx.media3.test.session.common.CommonConstants.KEY_TRACK_SELE
import static androidx.media3.test.session.common.CommonConstants.KEY_VIDEO_SIZE;
import static androidx.media3.test.session.common.CommonConstants.KEY_VOLUME;
import static androidx.media3.test.session.common.MediaSessionConstants.KEY_AVAILABLE_SESSION_COMMANDS;
import static androidx.media3.test.session.common.MediaSessionConstants.KEY_CONTROLLER;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_CONTROLLER_LISTENER_SESSION_REJECTS;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_SESSION_ACTIVITY;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_IS_SESSION_COMMAND_AVAILABLE;
@ -427,6 +428,29 @@ public class MediaSessionProviderService extends Service {
});
}
@Override
public void setSessionExtras(String sessionId, Bundle extras) throws RemoteException {
runOnHandler(() -> sessionMap.get(sessionId).setSessionExtras(extras));
}
@Override
public void setSessionExtrasForController(String sessionId, String controllerKey, Bundle extras)
throws RemoteException {
runOnHandler(
() -> {
MediaSession mediaSession = sessionMap.get(sessionId);
for (ControllerInfo controllerInfo : mediaSession.getConnectedControllers()) {
if (controllerInfo
.getConnectionHints()
.getString(KEY_CONTROLLER, /* defaultValue= */ "")
.equals(controllerKey)) {
mediaSession.setSessionExtras(controllerInfo, extras);
break;
}
}
});
}
////////////////////////////////////////////////////////////////////////////////
// MockPlayer methods
////////////////////////////////////////////////////////////////////////////////

View File

@ -197,6 +197,14 @@ public class RemoteMediaSession {
binder.setCustomLayout(sessionId, bundleList);
}
public void setSessionExtras(Bundle extras) throws RemoteException {
binder.setSessionExtras(sessionId, extras);
}
public void setSessionExtras(String controllerKey, Bundle extras) throws RemoteException {
binder.setSessionExtrasForController(sessionId, controllerKey, extras);
}
////////////////////////////////////////////////////////////////////////////////
// RemoteMockPlayer methods
////////////////////////////////////////////////////////////////////////////////

View File

@ -173,6 +173,10 @@ public class RemoteMediaSessionCompat {
binder.setCaptioningEnabled(sessionTag, enabled);
}
public void setExtras(Bundle extras) throws RemoteException {
binder.setSessionExtras(sessionTag, extras);
}
////////////////////////////////////////////////////////////////////////////////
// Non-public methods
////////////////////////////////////////////////////////////////////////////////

View File

@ -58,6 +58,11 @@ public final class TestMediaBrowserListener implements MediaBrowser.Listener {
return delegate.onSetCustomLayout(controller, layout);
}
@Override
public void onExtrasChanged(MediaController controller, Bundle extras) {
delegate.onExtrasChanged(controller, extras);
}
@Override
public void onAvailableSessionCommandsChanged(
MediaController controller, SessionCommands commands) {