Commit b475f1f2 by tonihei Committed by Marc Baechinger

Support setMediaItem(s) in MediaControllerImplLegacy

These calls were not implemented so far as they require a mix of
initial prepareFrom/playFrom calls and addQueueItem. We can also
support clients without queue handling to set single MediaItems.

To make the calls consistent and predictable in the session,
we need to ensure that none of the play/pause/addQueueItem/
removeQueueItem/prepare/playFromXYZ/prepareFromXYZ are called
before the controller is prepared and has media.

#minor-release

PiperOrigin-RevId: 455110246
parent 84c43f85
......@@ -179,6 +179,8 @@
of requests.
* Forward legacy `MediaController` calls to play media to
`MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`.
* Support `setMediaItems(s)` methods when `MediaController` connects to a
legacy media session.
* Data sources:
* Rename `DummyDataSource` to `PlaceholderDataSource`.
* Workaround OkHttp interrupt handling.
......
......@@ -81,13 +81,14 @@ import org.checkerframework.checker.initialization.qual.Initialized;
* <li><a href="#ControllerLifeCycle">Controller Lifecycle</a>
* <li><a href="#ThreadingModel">Threading Model</a>
* <li><a href="#PackageVisibilityFilter">Package Visibility Filter</a>
* <li><a href="#BackwardCompatibility">Backward Compatibility with legacy media sessions</a>
* </ol>
*
* <h2 id="ControllerLifeCycle">Controller Lifecycle</h2>
*
* <p>When a controller is created with the {@link SessionToken} for a {@link MediaSession} (i.e.
* session token type is {@link SessionToken#TYPE_SESSION}), the controller will connect to the
* specific session.
* specific session.F
*
* <p>When a controller is created with the {@link SessionToken} for a {@link MediaSessionService}
* (i.e. session token type is {@link SessionToken#TYPE_SESSION_SERVICE} or {@link
......@@ -127,6 +128,34 @@ import org.checkerframework.checker.initialization.qual.Initialized;
* <!-- Or, as a package name -->
* <package android:name="package_name_of_the_other_app" />
* }</pre>
*
* <h2 id="BackwardCompatibility">Backward Compatibility with legacy media sessions</h2>
*
* <p>In addition to {@link MediaSession}, the controller also supports connecting to a legacy media
* session - {@linkplain android.media.session.MediaSession framework session} and {@linkplain
* MediaSessionCompat AndroidX session compat}.
*
* <p>To request legacy sessions to play media, use one of the {@link #setMediaItem} methods and set
* either {@link MediaItem#mediaId}, {@link MediaItem.RequestMetadata#mediaUri} or {@link
* MediaItem.RequestMetadata#searchQuery}. Once the controller is {@linkplain #prepare() prepared},
* the controller triggers one of the following callbacks depending on the provided information and
* the value of {@link #getPlayWhenReady()}:
*
* <ul>
* <li>{@link MediaSessionCompat.Callback#onPrepareFromUri onPrepareFromUri}
* <li>{@link MediaSessionCompat.Callback#onPlayFromUri onPlayFromUri}
* <li>{@link MediaSessionCompat.Callback#onPrepareFromMediaId onPrepareFromMediaId}
* <li>{@link MediaSessionCompat.Callback#onPlayFromMediaId onPlayFromMediaId}
* <li>{@link MediaSessionCompat.Callback#onPrepareFromSearch onPrepareFromSearch}
* <li>{@link MediaSessionCompat.Callback#onPlayFromSearch onPlayFromSearch}
* </ul>
*
* Other playlist change methods, like {@link #addMediaItem} or {@link #removeMediaItem}, trigger
* the {@link MediaSessionCompat.Callback#onAddQueueItem onAddQueueItem} and {@link
* MediaSessionCompat.Callback#onRemoveQueueItem} onRemoveQueueItem} callbacks. Check {@link
* #getAvailableCommands()} to see if playlist modifications are {@linkplain
* androidx.media3.common.Player.Command#COMMAND_CHANGE_MEDIA_ITEMS supported} by the legacy
* session.
*/
public class MediaController implements Player {
......@@ -478,13 +507,6 @@ public class MediaController implements Player {
return impl.isConnected();
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with
* previously called {@link #setMediaUri}. See {@link #setMediaUri} for details.
*/
@Override
public void play() {
verifyApplicationThread();
......@@ -505,13 +527,6 @@ public class MediaController implements Player {
impl.pause();
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with
* previously called {@link #setMediaUri}. See {@link #setMediaUri} for details.
*/
@Override
public void prepare() {
verifyApplicationThread();
......@@ -980,44 +995,6 @@ public class MediaController implements Player {
* <p>The {@link Player.Listener#onTimelineChanged} and/or {@link
* Player.Listener#onMediaItemTransition} would be called when it's completed.
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this call will be grouped together with
* later {@link #prepare} or {@link #play}, depending on the uri pattern as follows:
*
* <table>
* <caption>Uri patterns and following API calls for MediaControllerCompat methods</caption>
* <tr>
* <th>Uri patterns</th><th>Following API calls</th><th>Method</th>
* </tr><tr>
* <td rowspan="2">{@code androidx://media3-session/setMediaUri?uri=[uri]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* </tr><tr>
* <td rowspan="2">{@code androidx://media3-session/setMediaUri?id=[mediaId]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}
* </tr><tr>
* <td rowspan="2">{@code androidx://media3-session/setMediaUri?query=[query]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}
* </tr><tr>
* <td rowspan="2">Does not match with any pattern above</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* </tr></table>
*
* <p>Returned {@link ListenableFuture} will return {@link SessionResult#RESULT_SUCCESS} when it's
* handled together with {@link #prepare} or {@link #play}. If this API is called multiple times
* without prepare or play, then {@link SessionResult#RESULT_INFO_SKIPPED} will be returned for
......@@ -1027,15 +1004,6 @@ public class MediaController implements Player {
* @param extras A {@link Bundle} to send extra information. May be empty.
* @return A {@link ListenableFuture} of {@link SessionResult} representing the pending
* completion.
* @see MediaConstants#MEDIA_URI_AUTHORITY
* @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID
* @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID
* @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_SEARCH
* @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_SEARCH
* @see MediaConstants#MEDIA_URI_PATH_SET_MEDIA_URI
* @see MediaConstants#MEDIA_URI_QUERY_ID
* @see MediaConstants#MEDIA_URI_QUERY_QUERY
* @see MediaConstants#MEDIA_URI_QUERY_URI
*/
public ListenableFuture<SessionResult> setMediaUri(Uri uri, Bundle extras) {
verifyApplicationThread();
......
......@@ -88,7 +88,7 @@ import java.util.List;
* <li><a href="#ThreadingModel">Threading Model</a>
* <li><a href="#KeyEvents">Media Key Events Mapping</a>
* <li><a href="#MultipleSessions">Supporting Multiple Sessions</a>
* <li><a href="#CompatibilitySession">Backward Compatibility with Legacy Session APIs</a>
* <li><a href="#BackwardCompatibility">Backward Compatibility with Legacy Session APIs</a>
* <li><a href="#CompatibilityController">Backward Compatibility with Legacy Controller APIs</a>
* </ol>
*
......@@ -201,10 +201,10 @@ import java.util.List;
*
* <h2 id="CompatibilityController">Backward Compatibility with Legacy Controller APIs</h2>
*
* <p>In addition to {@link MediaController}, session also supports connection from the legacy
* controller APIs - {@link android.media.session.MediaController framework controller} and {@link
* MediaControllerCompat AndroidX controller compat}. However, {@link ControllerInfo} may not be
* precise for legacy controllers. See {@link ControllerInfo} for the details.
* <p>In addition to {@link MediaController}, the session also supports connections from the legacy
* controller APIs - {@linkplain android.media.session.MediaController framework controller} and
* {@linkplain MediaControllerCompat AndroidX controller compat}. However, {@link ControllerInfo}
* may not be precise for legacy controllers. See {@link ControllerInfo} for the details.
*
* <p>Unknown package name nor UID doesn't mean that you should disallow connection nor commands.
* For SDK levels where such issues happen, session tokens could only be obtained by trusted
......
......@@ -30,6 +30,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
......@@ -1058,7 +1059,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
COMMAND_SEEK_TO_NEXT,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_GET_CURRENT_MEDIA_ITEM);
COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_SET_MEDIA_ITEM);
boolean includePlaylistCommands = (sessionFlags & FLAG_HANDLES_QUEUE_COMMANDS) != 0;
if (includePlaylistCommands) {
playerCommandsBuilder.add(COMMAND_CHANGE_MEDIA_ITEMS);
......
......@@ -79,11 +79,11 @@ import java.util.Map;
newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem);
}
public QueueTimeline copyWithNewMediaItems(int addToIndex, List<MediaItem> newMediaItems) {
public QueueTimeline copyWithNewMediaItems(int index, List<MediaItem> newMediaItems) {
ImmutableList.Builder<MediaItem> newMediaItemsBuilder = new ImmutableList.Builder<>();
newMediaItemsBuilder.addAll(mediaItems.subList(0, addToIndex));
newMediaItemsBuilder.addAll(mediaItems.subList(0, index));
newMediaItemsBuilder.addAll(newMediaItems);
newMediaItemsBuilder.addAll(mediaItems.subList(addToIndex, mediaItems.size()));
newMediaItemsBuilder.addAll(mediaItems.subList(index, mediaItems.size()));
return new QueueTimeline(
newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem);
}
......
......@@ -29,7 +29,6 @@ import static androidx.media3.common.Player.STATE_BUFFERING;
import static androidx.media3.common.Player.STATE_READY;
import static androidx.media3.session.MediaConstants.ARGUMENT_CAPTIONING_ENABLED;
import static androidx.media3.session.MediaConstants.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
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.METADATA_ALBUM_TITLE;
......@@ -37,10 +36,8 @@ import static androidx.media3.test.session.common.CommonConstants.METADATA_ARTIS
import static androidx.media3.test.session.common.CommonConstants.METADATA_DESCRIPTION;
import static androidx.media3.test.session.common.CommonConstants.METADATA_TITLE;
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_MS;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.app.PendingIntent;
......@@ -85,8 +82,6 @@ import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
......@@ -568,77 +563,6 @@ public class MediaControllerWithMediaSessionCompatTest {
}
@Test
public void setMediaUri_resultSetAfterPrepare() throws Exception {
MediaController controller = controllerTestRule.createController(session.getSessionToken());
Uri testUri = Uri.parse("androidx://test");
ListenableFuture<SessionResult> future =
threadTestRule
.getHandler()
.postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ Bundle.EMPTY));
SessionResult result;
try {
result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertWithMessage("TimeoutException is expected").fail();
} catch (TimeoutException e) {
// expected.
}
threadTestRule.getHandler().postAndSync(controller::prepare);
result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
}
@Test
public void setMediaUri_resultSetAfterPlay() throws Exception {
MediaController controller = controllerTestRule.createController(session.getSessionToken());
Uri testUri = Uri.parse("androidx://test");
ListenableFuture<SessionResult> future =
threadTestRule
.getHandler()
.postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ Bundle.EMPTY));
SessionResult result;
try {
result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertWithMessage("TimeoutException is expected").fail();
} catch (TimeoutException e) {
// expected.
}
threadTestRule.getHandler().postAndSync(controller::play);
result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
}
@Test
public void setMediaUris_multipleCalls_previousCallReturnsResultInfoSkipped() throws Exception {
MediaController controller = controllerTestRule.createController(session.getSessionToken());
Uri testUri1 = Uri.parse("androidx://test1");
Uri testUri2 = Uri.parse("androidx://test2");
ListenableFuture<SessionResult> future1 =
threadTestRule
.getHandler()
.postAndSync(() -> controller.setMediaUri(testUri1, /* extras= */ Bundle.EMPTY));
ListenableFuture<SessionResult> future2 =
threadTestRule
.getHandler()
.postAndSync(() -> controller.setMediaUri(testUri2, /* extras= */ Bundle.EMPTY));
threadTestRule.getHandler().postAndSync(controller::prepare);
SessionResult result1 = future1.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
SessionResult result2 = future2.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(result1.resultCode).isEqualTo(RESULT_INFO_SKIPPED);
assertThat(result2.resultCode).isEqualTo(RESULT_SUCCESS);
}
@Test
public void seekToDefaultPosition_withMediaItemIndex_updatesExpectedMediaItemIndex()
throws Exception {
List<MediaItem> testList = MediaTestUtils.createMediaItems(3);
......
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