Skip to content

Commit 9bf18db

Browse files
committed
Session: advertise legacy FLAG_HANDLES_QUEUE_COMMANDS
This change includes 3 things: - when the legacy media session is created, FLAG_HANDLES_QUEUE_COMMANDS is advertised if the player has the COMMAND_CHANGE_MEDIA_ITEMS available. - when the player changes its available commands, a new PlaybackStateCompat is sent to the remote media controller to advertise the updated PlyabackStateCompat actions. - when the player changes its available commands, the legacy media session flags are sent accoridingly: FLAG_HANDLES_QUEUE_COMMANDS is set only if the COMMAND_CHANGE_MEDIA_ITEMS is available. #minor-release PiperOrigin-RevId: 506605905 (cherry picked from commit ebe7ece)
1 parent 065418c commit 9bf18db

File tree

2 files changed

+234
-2
lines changed

2 files changed

+234
-2
lines changed

libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126

127127
private volatile long connectionTimeoutMs;
128128
@Nullable private FutureCallback<Bitmap> pendingBitmapLoadCallback;
129+
private int sessionFlags;
129130

130131
public MediaSessionLegacyStub(
131132
MediaSessionImpl session,
@@ -161,8 +162,6 @@ public MediaSessionLegacyStub(
161162
sessionCompat.setSessionActivity(sessionActivity);
162163
}
163164

164-
sessionCompat.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
165-
166165
@SuppressWarnings("nullness:assignment")
167166
@Initialized
168167
MediaSessionLegacyStub thisRef = this;
@@ -254,6 +253,17 @@ public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
254253
return false;
255254
}
256255

256+
private void maybeUpdateFlags(PlayerWrapper playerWrapper) {
257+
int newFlags =
258+
playerWrapper.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)
259+
? MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS
260+
: 0;
261+
if (sessionFlags != newFlags) {
262+
sessionFlags = newFlags;
263+
sessionCompat.setFlags(sessionFlags);
264+
}
265+
}
266+
257267
private void handleMediaPlayPauseOnHandler(RemoteUserInfo remoteUserInfo) {
258268
mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey();
259269
dispatchSessionTaskWithPlayerCommand(
@@ -894,6 +904,13 @@ public ControllerLegacyCbForBroadcast() {
894904
lastDurationMs = C.TIME_UNSET;
895905
}
896906

907+
@Override
908+
public void onAvailableCommandsChangedFromPlayer(int seq, Player.Commands availableCommands) {
909+
PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper();
910+
maybeUpdateFlags(playerWrapper);
911+
sessionImpl.getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat());
912+
}
913+
897914
@Override
898915
public void onDisconnected(int seq) throws RemoteException {
899916
// Calling MediaSessionCompat#release() is already done in release().
@@ -936,6 +953,7 @@ public void onPlayerChanged(
936953
onDeviceInfoChanged(seq, newPlayerWrapper.getDeviceInfo());
937954

938955
// Rest of changes are all notified via PlaybackStateCompat.
956+
maybeUpdateFlags(newPlayerWrapper);
939957
@Nullable MediaItem newMediaItem = newPlayerWrapper.getCurrentMediaItemWithCommandCheck();
940958
if (oldPlayerWrapper == null
941959
|| !Util.areEqual(oldPlayerWrapper.getCurrentMediaItemWithCommandCheck(), newMediaItem)) {

libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,28 @@
1717
package androidx.media3.session;
1818

1919
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
20+
import static androidx.media3.test.session.common.TestUtils.getEventsAsList;
2021
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
2122
import static com.google.common.truth.Truth.assertThat;
2223
import static java.util.concurrent.TimeUnit.MILLISECONDS;
24+
import static org.junit.Assert.assertThrows;
2325

2426
import android.net.Uri;
2527
import android.os.Bundle;
2628
import android.os.Handler;
29+
import android.os.Looper;
30+
import android.support.v4.media.MediaDescriptionCompat;
2731
import android.support.v4.media.session.MediaControllerCompat;
32+
import android.support.v4.media.session.MediaSessionCompat;
2833
import android.support.v4.media.session.PlaybackStateCompat;
2934
import androidx.annotation.Nullable;
35+
import androidx.core.util.Predicate;
3036
import androidx.media3.common.C;
3137
import androidx.media3.common.ForwardingPlayer;
3238
import androidx.media3.common.MediaItem;
3339
import androidx.media3.common.PlaybackParameters;
3440
import androidx.media3.common.Player;
41+
import androidx.media3.common.SimpleBasePlayer;
3542
import androidx.media3.common.Timeline;
3643
import androidx.media3.common.util.ConditionVariable;
3744
import androidx.media3.common.util.Consumer;
@@ -1261,6 +1268,173 @@ public void onRepeatModeChanged(int repeatMode) {
12611268
releasePlayer(player);
12621269
}
12631270

1271+
@Test
1272+
public void playerWithCommandChangeMediaItems_flagHandleQueueIsAdvertised() throws Exception {
1273+
Player player =
1274+
createPlayerWithAvailableCommand(createDefaultPlayer(), Player.COMMAND_CHANGE_MEDIA_ITEMS);
1275+
MediaSession mediaSession =
1276+
createMediaSession(
1277+
player,
1278+
new MediaSession.Callback() {
1279+
@Override
1280+
public ListenableFuture<List<MediaItem>> onAddMediaItems(
1281+
MediaSession mediaSession,
1282+
MediaSession.ControllerInfo controller,
1283+
List<MediaItem> mediaItems) {
1284+
return Futures.immediateFuture(
1285+
ImmutableList.of(MediaItem.fromUri("asset://media/wav/sample.wav")));
1286+
}
1287+
});
1288+
MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession);
1289+
1290+
// Wait until a playback state is sent to the controller.
1291+
getFirstPlaybackState(controllerCompat, threadTestRule.getHandler());
1292+
assertThat(controllerCompat.getFlags() & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS)
1293+
.isNotEqualTo(0);
1294+
1295+
ArrayList<Timeline> receivedTimelines = new ArrayList<>();
1296+
ArrayList<Integer> receivedTimelineReasons = new ArrayList<>();
1297+
CountDownLatch latch = new CountDownLatch(2);
1298+
Player.Listener listener =
1299+
new Player.Listener() {
1300+
@Override
1301+
public void onTimelineChanged(
1302+
Timeline timeline, @Player.TimelineChangeReason int reason) {
1303+
receivedTimelines.add(timeline);
1304+
receivedTimelineReasons.add(reason);
1305+
latch.countDown();
1306+
}
1307+
};
1308+
player.addListener(listener);
1309+
1310+
controllerCompat.addQueueItem(
1311+
new MediaDescriptionCompat.Builder().setMediaId("mediaId").build());
1312+
controllerCompat.addQueueItem(
1313+
new MediaDescriptionCompat.Builder().setMediaId("mediaId").build(), /* index= */ 0);
1314+
1315+
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
1316+
assertThat(receivedTimelines).hasSize(2);
1317+
assertThat(receivedTimelines.get(0).getWindowCount()).isEqualTo(1);
1318+
assertThat(receivedTimelines.get(1).getWindowCount()).isEqualTo(2);
1319+
assertThat(receivedTimelineReasons)
1320+
.containsExactly(
1321+
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
1322+
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
1323+
1324+
mediaSession.release();
1325+
releasePlayer(player);
1326+
}
1327+
1328+
@Test
1329+
public void playerWithoutCommandChangeMediaItems_flagHandleQueueNotAdvertised() throws Exception {
1330+
Player player =
1331+
createPlayerWithExcludedCommand(createDefaultPlayer(), Player.COMMAND_CHANGE_MEDIA_ITEMS);
1332+
MediaSession mediaSession =
1333+
createMediaSession(
1334+
player,
1335+
new MediaSession.Callback() {
1336+
@Override
1337+
public ListenableFuture<List<MediaItem>> onAddMediaItems(
1338+
MediaSession mediaSession,
1339+
MediaSession.ControllerInfo controller,
1340+
List<MediaItem> mediaItems) {
1341+
return Futures.immediateFuture(
1342+
ImmutableList.of(MediaItem.fromUri("asset://media/wav/sample.wav")));
1343+
}
1344+
});
1345+
MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession);
1346+
1347+
// Wait until a playback state is sent to the controller.
1348+
getFirstPlaybackState(controllerCompat, threadTestRule.getHandler());
1349+
assertThat(controllerCompat.getFlags() & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS)
1350+
.isEqualTo(0);
1351+
assertThrows(
1352+
UnsupportedOperationException.class,
1353+
() ->
1354+
controllerCompat.addQueueItem(
1355+
new MediaDescriptionCompat.Builder().setMediaId("mediaId").build()));
1356+
assertThrows(
1357+
UnsupportedOperationException.class,
1358+
() ->
1359+
controllerCompat.addQueueItem(
1360+
new MediaDescriptionCompat.Builder().setMediaId("mediaId").build(),
1361+
/* index= */ 0));
1362+
1363+
mediaSession.release();
1364+
releasePlayer(player);
1365+
}
1366+
1367+
@Test
1368+
public void playerChangesAvailableCommands_actionsAreUpdated() throws Exception {
1369+
// TODO(b/261158047): Add COMMAND_RELEASE to the available commands so that we can release the
1370+
// player.
1371+
ControllingCommandsPlayer player =
1372+
new ControllingCommandsPlayer(
1373+
Player.Commands.EMPTY, threadTestRule.getHandler().getLooper());
1374+
MediaSession mediaSession = createMediaSession(player);
1375+
MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession);
1376+
LinkedBlockingDeque<PlaybackStateCompat> receivedPlaybackStateCompats =
1377+
new LinkedBlockingDeque<>();
1378+
MediaControllerCompat.Callback callback =
1379+
new MediaControllerCompat.Callback() {
1380+
@Override
1381+
public void onPlaybackStateChanged(PlaybackStateCompat state) {
1382+
receivedPlaybackStateCompats.add(state);
1383+
}
1384+
};
1385+
controllerCompat.registerCallback(callback, threadTestRule.getHandler());
1386+
1387+
ArrayList<Player.Events> receivedEvents = new ArrayList<>();
1388+
ConditionVariable eventsArrived = new ConditionVariable();
1389+
player.addListener(
1390+
new Player.Listener() {
1391+
@Override
1392+
public void onEvents(Player player, Player.Events events) {
1393+
receivedEvents.add(events);
1394+
eventsArrived.open();
1395+
}
1396+
});
1397+
threadTestRule
1398+
.getHandler()
1399+
.postAndSync(
1400+
() -> {
1401+
player.setAvailableCommands(
1402+
new Player.Commands.Builder().add(Player.COMMAND_PREPARE).build());
1403+
});
1404+
1405+
assertThat(eventsArrived.block(TIMEOUT_MS)).isTrue();
1406+
assertThat(getEventsAsList(receivedEvents.get(0)))
1407+
.containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED);
1408+
assertThat(
1409+
waitUntilPlaybackStateArrived(
1410+
receivedPlaybackStateCompats,
1411+
/* predicate= */ playbackStateCompat ->
1412+
(playbackStateCompat.getActions() & PlaybackStateCompat.ACTION_PREPARE) != 0))
1413+
.isTrue();
1414+
1415+
eventsArrived.open();
1416+
threadTestRule
1417+
.getHandler()
1418+
.postAndSync(
1419+
() -> {
1420+
player.setAvailableCommands(Player.Commands.EMPTY);
1421+
});
1422+
1423+
assertThat(eventsArrived.block(TIMEOUT_MS)).isTrue();
1424+
assertThat(
1425+
waitUntilPlaybackStateArrived(
1426+
receivedPlaybackStateCompats,
1427+
/* predicate= */ playbackStateCompat ->
1428+
(playbackStateCompat.getActions() & PlaybackStateCompat.ACTION_PREPARE) == 0))
1429+
.isTrue();
1430+
assertThat(getEventsAsList(receivedEvents.get(1)))
1431+
.containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED);
1432+
1433+
mediaSession.release();
1434+
// This player is instantiated to use the threadTestRule, so it's released on that thread.
1435+
threadTestRule.getHandler().postAndSync(player::release);
1436+
}
1437+
12641438
private PlaybackStateCompat getFirstPlaybackState(
12651439
MediaControllerCompat mediaControllerCompat, Handler handler) throws InterruptedException {
12661440
LinkedBlockingDeque<PlaybackStateCompat> playbackStateCompats = new LinkedBlockingDeque<>();
@@ -1347,6 +1521,21 @@ private static Player createPlayerWithExcludedCommand(
13471521
player, Player.Commands.EMPTY, new Player.Commands.Builder().add(excludedCommand).build());
13481522
}
13491523

1524+
private static boolean waitUntilPlaybackStateArrived(
1525+
LinkedBlockingDeque<PlaybackStateCompat> playbackStateCompats,
1526+
Predicate<PlaybackStateCompat> predicate)
1527+
throws InterruptedException {
1528+
while (true) {
1529+
@Nullable
1530+
PlaybackStateCompat playbackStateCompat = playbackStateCompats.poll(TIMEOUT_MS, MILLISECONDS);
1531+
if (playbackStateCompat == null) {
1532+
return false;
1533+
} else if (predicate.test(playbackStateCompat)) {
1534+
return true;
1535+
}
1536+
}
1537+
}
1538+
13501539
/**
13511540
* Returns an {@link Player} where {@code availableCommands} are always included and {@code
13521541
* excludedCommands} are always excluded from the {@linkplain Player#getAvailableCommands()
@@ -1371,4 +1560,29 @@ public boolean isCommandAvailable(int command) {
13711560
}
13721561
};
13731562
}
1563+
1564+
private static class ControllingCommandsPlayer extends SimpleBasePlayer {
1565+
1566+
private Commands availableCommands;
1567+
1568+
public ControllingCommandsPlayer(Commands availableCommands, Looper applicationLooper) {
1569+
super(applicationLooper);
1570+
this.availableCommands = availableCommands;
1571+
}
1572+
1573+
public void setAvailableCommands(Commands availableCommands) {
1574+
this.availableCommands = availableCommands;
1575+
invalidateState();
1576+
}
1577+
1578+
@Override
1579+
protected State getState() {
1580+
return new State.Builder().setAvailableCommands(availableCommands).build();
1581+
}
1582+
1583+
@Override
1584+
protected ListenableFuture handleRelease() {
1585+
return Futures.immediateVoidFuture();
1586+
}
1587+
}
13741588
}

0 commit comments

Comments
 (0)