Commit 85a936ec by bachinger Committed by Marc Baechinger

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
parent a629d094
Showing with 260 additions and 12 deletions
......@@ -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;
......
......@@ -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 =
......
......@@ -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 {
......
......@@ -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);
}
......
......@@ -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
......
......@@ -184,6 +184,11 @@ import java.util.List;
}
@Override
public void onExtrasChanged(int seq, Bundle extras) {
dispatchControllerTaskOnHandler(controller -> controller.onExtrasChanged(extras));
}
@Override
public void onRenderedFirstFrame(int seq) {
dispatchControllerTaskOnHandler(MediaControllerImplBase::onRenderedFirstFrame);
}
......
......@@ -703,6 +703,32 @@ public class MediaSession {
}
/**
* 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.
*
* <p>The result from {@link MediaController.Listener#onCustomCommand(MediaController,
......@@ -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 {}
......
......@@ -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)) {
......
......@@ -887,6 +887,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
@Override
public void onSessionExtrasChanged(int seq, Bundle sessionExtras) {
sessionImpl.getSessionCompat().setExtras(sessionExtras);
}
@Override
public void onPlayWhenReadyChanged(
int seq, boolean playWhenReady, @Player.PlaybackSuppressionReason int reason)
throws RemoteException {
......
......@@ -1802,6 +1802,11 @@ import java.util.concurrent.ExecutionException;
}
@Override
public void onSessionExtrasChanged(int seq, Bundle sessionExtras) throws RemoteException {
iController.onExtrasChanged(seq, sessionExtras);
}
@Override
public int hashCode() {
return ObjectsCompat.hash(getCallbackBinder());
}
......
......@@ -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);
......
......@@ -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);
}
......@@ -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() {}
}
......@@ -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();
}
}
......@@ -820,6 +820,30 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
}
@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;
long testPosition = 1234;
......
......@@ -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;
......@@ -1801,6 +1802,55 @@ public class MediaControllerListenerTest {
}
@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 =
new VideoSize(
......
......@@ -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();
}
}
......@@ -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);
}
}
}
......@@ -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
////////////////////////////////////////////////////////////////////////////////
......
......@@ -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
////////////////////////////////////////////////////////////////////////////////
......
......@@ -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
////////////////////////////////////////////////////////////////////////////////
......
......@@ -59,6 +59,11 @@ public final class TestMediaBrowserListener implements MediaBrowser.Listener {
}
@Override
public void onExtrasChanged(MediaController controller, Bundle extras) {
delegate.onExtrasChanged(controller, extras);
}
@Override
public void onAvailableSessionCommandsChanged(
MediaController controller, SessionCommands commands) {
delegate.onAvailableSessionCommandsChanged(controller, commands);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment