mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add MediaButtonReceiver for Media3
The media button has API support with `Callback.getPlaybackResumption()` that apps need to override to provide a playlist to resume playback with. Issue: androidx/media#167 PiperOrigin-RevId: 529495845
This commit is contained in:
parent
804b57ea7b
commit
e48dec5f2c
@ -62,6 +62,10 @@
|
|||||||
([#355](https://github.com/androidx/media/issues/355)).
|
([#355](https://github.com/androidx/media/issues/355)).
|
||||||
* Fix memory leak of `MediaSessionService` or `MediaLibraryService`
|
* Fix memory leak of `MediaSessionService` or `MediaLibraryService`
|
||||||
([#346](https://github.com/androidx/media/issues/346)).
|
([#346](https://github.com/androidx/media/issues/346)).
|
||||||
|
* Add `androidx.media3.session.MediaButtonReceiver` to enable apps to
|
||||||
|
implement playback resumption with media button events sent by, for
|
||||||
|
example, a Bluetooth headset
|
||||||
|
([#167](https://github.com/androidx/media/issues/167)).
|
||||||
* UI:
|
* UI:
|
||||||
* Add Util methods `shouldShowPlayButton` and
|
* Add Util methods `shouldShowPlayButton` and
|
||||||
`handlePlayPauseButtonAction` to write custom UI elements with a
|
`handlePlayPauseButtonAction` to write custom UI elements with a
|
||||||
|
@ -0,0 +1,240 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package androidx.media3.session;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
|
||||||
|
import android.app.ForegroundServiceStartNotAllowedException;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ResolveInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import androidx.annotation.DoNotInline;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.media3.common.util.Log;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A media button receiver receives hardware media playback button intent, such as those sent by
|
||||||
|
* wired and wireless headsets.
|
||||||
|
*
|
||||||
|
* <p>You can add this MediaButtonReceiver to your app by adding it directly to your
|
||||||
|
* AndroidManifest.xml:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* <receiver
|
||||||
|
* android:name="androidx.media3.session.MediaButtonReceiver"
|
||||||
|
* android:exported="true">
|
||||||
|
* <intent-filter>
|
||||||
|
* <action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
* </intent-filter>
|
||||||
|
* </receiver>
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>Apps that add this receiver to the manifest, must implement {@link
|
||||||
|
* MediaSession.Callback#onPlaybackResumption} or active automatic playback resumption (Note: If you
|
||||||
|
* choose to make this receiver start your own service that is not a {@link MediaSessionService} or
|
||||||
|
* {@link MediaLibraryService}, then you need to fulfill all requirements around starting a service
|
||||||
|
* in the foreground on all API levels your app should properly work on).
|
||||||
|
*
|
||||||
|
* <h2>Service discovery</h2>
|
||||||
|
*
|
||||||
|
* <p>This class assumes you have a {@link Service} in your app's manifest that controls media
|
||||||
|
* playback via a {@link MediaSession}. Once a key event is received by this receiver, it tries to
|
||||||
|
* find a {@link Service} that can handle the action {@link Intent#ACTION_MEDIA_BUTTON}, {@link
|
||||||
|
* MediaSessionService#SERVICE_INTERFACE} or {@link MediaSessionService#SERVICE_INTERFACE}. If an
|
||||||
|
* appropriate service is found, this class starts the service as a foreground service and sends the
|
||||||
|
* key event to the service by an {@link Intent} with action {@link Intent#ACTION_MEDIA_BUTTON}. If
|
||||||
|
* neither is available or more than one valid service is found for one of the actions, an {@link
|
||||||
|
* IllegalStateException} is thrown.
|
||||||
|
*
|
||||||
|
* <h3>Service handling ACTION_MEDIA_BUTTON</h3>
|
||||||
|
*
|
||||||
|
* <p>A service can receive a key event by including an intent filter that handles {@code
|
||||||
|
* android.intent.action.MEDIA_BUTTON}.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* <service android:name="com.example.android.MediaPlaybackService" >
|
||||||
|
* <intent-filter>
|
||||||
|
* <action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
* </intent-filter>
|
||||||
|
* </service>
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <h3>Service handling action {@link MediaSessionService} or {@link MediaLibraryService}</h3>
|
||||||
|
*
|
||||||
|
* <p>If you are using a {@link MediaSessionService} or {@link MediaLibraryService}, the service
|
||||||
|
* interface name is already used as the intent action. In this case, no further configuration is
|
||||||
|
* required.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* <service android:name="com.example.android.MediaPlaybackService" >
|
||||||
|
* <intent-filter>
|
||||||
|
* <action android:name="androidx.media3.session.MediaLibraryService" />
|
||||||
|
* </intent-filter>
|
||||||
|
* </service>
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public class MediaButtonReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
private static final String TAG = "MediaButtonReceiver";
|
||||||
|
private static final String[] ACTIONS = {
|
||||||
|
Intent.ACTION_MEDIA_BUTTON,
|
||||||
|
MediaLibraryService.SERVICE_INTERFACE,
|
||||||
|
MediaSessionService.SERVICE_INTERFACE
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onReceive(Context context, @Nullable Intent intent) {
|
||||||
|
if (intent == null
|
||||||
|
|| !Objects.equals(intent.getAction(), Intent.ACTION_MEDIA_BUTTON)
|
||||||
|
|| !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) {
|
||||||
|
android.util.Log.d(TAG, "Ignore unsupported intent: " + intent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Util.SDK_INT >= 26) {
|
||||||
|
@Nullable
|
||||||
|
KeyEvent keyEvent = checkNotNull(intent.getExtras()).getParcelable(Intent.EXTRA_KEY_EVENT);
|
||||||
|
if (keyEvent != null
|
||||||
|
&& keyEvent.getKeyCode() != KeyEvent.KEYCODE_MEDIA_PLAY
|
||||||
|
&& keyEvent.getKeyCode() != KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
|
||||||
|
// Starting with Android 8 (API 26), the service must be started immediately in the
|
||||||
|
// foreground when being started. Also starting with Android 8, the system sends media
|
||||||
|
// button intents to this receiver only when the session is released or not active, meaning
|
||||||
|
// the service is not running. Hence we only accept a PLAY command here that ensures that
|
||||||
|
// playback is started and the MediaSessionService/MediaLibraryService is put into the
|
||||||
|
// foreground (see https://developer.android.com/guide/topics/media-apps/mediabuttons and
|
||||||
|
// https://developer.android.com/about/versions/oreo/android-8.0-changes#back-all).
|
||||||
|
android.util.Log.w(
|
||||||
|
TAG,
|
||||||
|
"Ignore key event that is not a `play` command on API 26 or above to avoid an"
|
||||||
|
+ " 'ForegroundServiceDidNotStartInTimeException'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String action : ACTIONS) {
|
||||||
|
ComponentName mediaButtonServiceComponentName = getServiceComponentByAction(context, action);
|
||||||
|
if (mediaButtonServiceComponentName != null) {
|
||||||
|
intent.setComponent(mediaButtonServiceComponentName);
|
||||||
|
try {
|
||||||
|
ContextCompat.startForegroundService(context, intent);
|
||||||
|
} catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 31
|
||||||
|
&& Api31.instanceOfForegroundServiceStartNotAllowedException(e)) {
|
||||||
|
onForegroundServiceStartNotAllowedException(
|
||||||
|
intent, Api31.castToForegroundServiceStartNotAllowedException(e));
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Could not find any Service that handles any of the actions " + Arrays.toString(ACTIONS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when an exception is thrown when calling {@link
|
||||||
|
* Context#startForegroundService(Intent)} as a result of receiving a media button event.
|
||||||
|
*
|
||||||
|
* <p>By default, this method only logs the exception and it can be safely overridden. Apps that
|
||||||
|
* find that such a media button event has been legitimately sent, may choose to override this
|
||||||
|
* method and take the opportunity to post a notification from where the user journey can
|
||||||
|
* continue.
|
||||||
|
*
|
||||||
|
* <p>This exception can be thrown if a broadcast media button event is received and a media
|
||||||
|
* service is found in the manifest that is registered to handle {@link
|
||||||
|
* Intent#ACTION_MEDIA_BUTTON}. If this happens on API 31+ and the app is in the background then
|
||||||
|
* an exception is thrown.
|
||||||
|
*
|
||||||
|
* <p>With the exception of devices that are running API 20 and below, a media button intent is
|
||||||
|
* only required to be sent to this receiver for a Bluetooth media button event that wants to
|
||||||
|
* restart the service. In such a case the app gets an exemption and is allowed to start the
|
||||||
|
* foreground service. In this case this method will never be called.
|
||||||
|
*
|
||||||
|
* <p>In all other cases of attempting to start a Media3 service or to send a media button event,
|
||||||
|
* apps must use a {@link MediaBrowser} or {@link MediaController} to bind to the service instead
|
||||||
|
* of broadcasting an intent.
|
||||||
|
*
|
||||||
|
* @param intent The intent that was used {@linkplain Context#startForegroundService(Intent) for
|
||||||
|
* starting the foreground service}.
|
||||||
|
* @param e The exception thrown by the system and caught by this broadcast receiver.
|
||||||
|
*/
|
||||||
|
@RequiresApi(31)
|
||||||
|
protected void onForegroundServiceStartNotAllowedException(
|
||||||
|
Intent intent, ForegroundServiceStartNotAllowedException e) {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"caught exception when trying to start a foreground service from the "
|
||||||
|
+ "background: "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("QueryPermissionsNeeded") // Needs to be provided in the app manifest.
|
||||||
|
@Nullable
|
||||||
|
private static ComponentName getServiceComponentByAction(Context context, String action) {
|
||||||
|
PackageManager pm = context.getPackageManager();
|
||||||
|
Intent queryIntent = new Intent(action);
|
||||||
|
queryIntent.setPackage(context.getPackageName());
|
||||||
|
List<ResolveInfo> resolveInfos = pm.queryIntentServices(queryIntent, /* flags= */ 0);
|
||||||
|
if (resolveInfos.size() == 1) {
|
||||||
|
ResolveInfo resolveInfo = resolveInfos.get(0);
|
||||||
|
return new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
|
||||||
|
} else if (resolveInfos.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Expected 1 service that handles " + action + ", found " + resolveInfos.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(31)
|
||||||
|
private static final class Api31 {
|
||||||
|
/**
|
||||||
|
* Returns true if the passed exception is a {@link ForegroundServiceStartNotAllowedException}.
|
||||||
|
*/
|
||||||
|
@DoNotInline
|
||||||
|
public static boolean instanceOfForegroundServiceStartNotAllowedException(
|
||||||
|
IllegalStateException e) {
|
||||||
|
return e instanceof ForegroundServiceStartNotAllowedException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Casts the {@link IllegalStateException} to a {@link
|
||||||
|
* ForegroundServiceStartNotAllowedException} and throws an exception if the cast fails.
|
||||||
|
*/
|
||||||
|
@DoNotInline
|
||||||
|
public static ForegroundServiceStartNotAllowedException
|
||||||
|
castToForegroundServiceStartNotAllowedException(IllegalStateException e) {
|
||||||
|
return (ForegroundServiceStartNotAllowedException) e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1234,6 +1234,21 @@ public class MediaSession {
|
|||||||
Futures.immediateFuture(
|
Futures.immediateFuture(
|
||||||
new MediaItemsWithStartPosition(mediaItemList, startIndex, startPositionMs)));
|
new MediaItemsWithStartPosition(mediaItemList, startIndex, startPositionMs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last recent playlist of the player with which the player should be prepared when
|
||||||
|
* playback resumption from a media button receiver or the System UI notification is requested.
|
||||||
|
*
|
||||||
|
* @param mediaSession The media session for which playback resumption is requested.
|
||||||
|
* @param controller The controller that requests the playback resumption. This is a short
|
||||||
|
* living controller created only for issuing a play command for resuming playback.
|
||||||
|
* @return The {@linkplain MediaItemsWithStartPosition playlist} to resume playback with.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
default ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
|
||||||
|
MediaSession mediaSession, ControllerInfo controller) {
|
||||||
|
return Futures.immediateFailedFuture(new UnsupportedOperationException());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Representation of list of media items and where to start playing */
|
/** Representation of list of media items and where to start playing */
|
||||||
|
@ -21,6 +21,7 @@ import static androidx.media3.common.util.Util.postOrRun;
|
|||||||
import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
|
import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
|
||||||
import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN;
|
import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN;
|
||||||
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
|
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
@ -43,8 +44,10 @@ import androidx.annotation.CheckResult;
|
|||||||
import androidx.annotation.FloatRange;
|
import androidx.annotation.FloatRange;
|
||||||
import androidx.annotation.GuardedBy;
|
import androidx.annotation.GuardedBy;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.ExecutorCompat;
|
||||||
import androidx.media.MediaBrowserServiceCompat;
|
import androidx.media.MediaBrowserServiceCompat;
|
||||||
import androidx.media3.common.AudioAttributes;
|
import androidx.media3.common.AudioAttributes;
|
||||||
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.DeviceInfo;
|
import androidx.media3.common.DeviceInfo;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaLibraryInfo;
|
import androidx.media3.common.MediaLibraryInfo;
|
||||||
@ -63,18 +66,22 @@ import androidx.media3.common.VideoSize;
|
|||||||
import androidx.media3.common.text.CueGroup;
|
import androidx.media3.common.text.CueGroup;
|
||||||
import androidx.media3.common.util.BitmapLoader;
|
import androidx.media3.common.util.BitmapLoader;
|
||||||
import androidx.media3.common.util.Log;
|
import androidx.media3.common.util.Log;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.session.MediaSession.ControllerCb;
|
import androidx.media3.session.MediaSession.ControllerCb;
|
||||||
import androidx.media3.session.MediaSession.ControllerInfo;
|
import androidx.media3.session.MediaSession.ControllerInfo;
|
||||||
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition;
|
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition;
|
||||||
import androidx.media3.session.SequencedFutureManager.SequencedFuture;
|
import androidx.media3.session.SequencedFutureManager.SequencedFuture;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
import org.checkerframework.checker.initialization.qual.Initialized;
|
import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
|
|
||||||
/* package */ class MediaSessionImpl {
|
/* package */ class MediaSessionImpl {
|
||||||
@ -136,6 +143,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
|||||||
// Should be only accessed on the application looper
|
// Should be only accessed on the application looper
|
||||||
private long sessionPositionUpdateDelayMs;
|
private long sessionPositionUpdateDelayMs;
|
||||||
|
|
||||||
|
@SuppressWarnings("StaticAssignmentInConstructor") // TODO(b/277754694): Remove mutable constants
|
||||||
public MediaSessionImpl(
|
public MediaSessionImpl(
|
||||||
MediaSession instance,
|
MediaSession instance,
|
||||||
Context context,
|
Context context,
|
||||||
@ -426,7 +434,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
|||||||
|
|
||||||
public MediaSession.ConnectionResult onConnectOnHandler(ControllerInfo controller) {
|
public MediaSession.ConnectionResult onConnectOnHandler(ControllerInfo controller) {
|
||||||
return checkNotNull(
|
return checkNotNull(
|
||||||
callback.onConnect(instance, controller), "onConnect must return non-null future");
|
callback.onConnect(instance, controller), "Callback.onConnect must return non-null future");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onPostConnectOnHandler(ControllerInfo controller) {
|
public void onPostConnectOnHandler(ControllerInfo controller) {
|
||||||
@ -447,21 +455,21 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
|||||||
ControllerInfo controller, String mediaId, Rating rating) {
|
ControllerInfo controller, String mediaId, Rating rating) {
|
||||||
return checkNotNull(
|
return checkNotNull(
|
||||||
callback.onSetRating(instance, controller, mediaId, rating),
|
callback.onSetRating(instance, controller, mediaId, rating),
|
||||||
"onSetRating must return non-null future");
|
"Callback.onSetRating must return non-null future");
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListenableFuture<SessionResult> onSetRatingOnHandler(
|
public ListenableFuture<SessionResult> onSetRatingOnHandler(
|
||||||
ControllerInfo controller, Rating rating) {
|
ControllerInfo controller, Rating rating) {
|
||||||
return checkNotNull(
|
return checkNotNull(
|
||||||
callback.onSetRating(instance, controller, rating),
|
callback.onSetRating(instance, controller, rating),
|
||||||
"onSetRating must return non-null future");
|
"Callback.onSetRating must return non-null future");
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListenableFuture<SessionResult> onCustomCommandOnHandler(
|
public ListenableFuture<SessionResult> onCustomCommandOnHandler(
|
||||||
ControllerInfo browser, SessionCommand command, Bundle extras) {
|
ControllerInfo browser, SessionCommand command, Bundle extras) {
|
||||||
return checkNotNull(
|
return checkNotNull(
|
||||||
callback.onCustomCommand(instance, browser, command, extras),
|
callback.onCustomCommand(instance, browser, command, extras),
|
||||||
"onCustomCommandOnHandler must return non-null future");
|
"Callback.onCustomCommandOnHandler must return non-null future");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void connectFromService(
|
public void connectFromService(
|
||||||
@ -502,14 +510,14 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
|||||||
ControllerInfo controller, List<MediaItem> mediaItems) {
|
ControllerInfo controller, List<MediaItem> mediaItems) {
|
||||||
return checkNotNull(
|
return checkNotNull(
|
||||||
callback.onAddMediaItems(instance, controller, mediaItems),
|
callback.onAddMediaItems(instance, controller, mediaItems),
|
||||||
"onAddMediaItems must return a non-null future");
|
"Callback.onAddMediaItems must return a non-null future");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ListenableFuture<MediaItemsWithStartPosition> onSetMediaItemsOnHandler(
|
protected ListenableFuture<MediaItemsWithStartPosition> onSetMediaItemsOnHandler(
|
||||||
ControllerInfo controller, List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
ControllerInfo controller, List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
||||||
return checkNotNull(
|
return checkNotNull(
|
||||||
callback.onSetMediaItems(instance, controller, mediaItems, startIndex, startPositionMs),
|
callback.onSetMediaItems(instance, controller, mediaItems, startIndex, startPositionMs),
|
||||||
"onSetMediaItems must return a non-null future");
|
"Callback.onSetMediaItems must return a non-null future");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isReleased() {
|
protected boolean isReleased() {
|
||||||
@ -594,6 +602,70 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to prepare and play for playback resumption.
|
||||||
|
*
|
||||||
|
* <p>If playlist data for playback resumption can be successfully obtained, the media items are
|
||||||
|
* set and the player is prepared. {@link Player#play()} is called regardless of success or
|
||||||
|
* failure of playback resumption.
|
||||||
|
*
|
||||||
|
* @param controller The controller requesting playback resumption.
|
||||||
|
* @param player The player to setup for playback resumption.
|
||||||
|
*/
|
||||||
|
/* package */ void prepareAndPlayForPlaybackResumption(ControllerInfo controller, Player player) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
@Nullable
|
||||||
|
ListenableFuture<MediaItemsWithStartPosition> future =
|
||||||
|
checkNotNull(
|
||||||
|
callback.onPlaybackResumption(instance, controller),
|
||||||
|
"Callback.onPlaybackResumption must return a non-null future");
|
||||||
|
// Use a direct executor when an immediate future is returned to execute the player setup in the
|
||||||
|
// caller's looper event on the application thread.
|
||||||
|
Executor executor =
|
||||||
|
future.isDone()
|
||||||
|
? MoreExecutors.directExecutor()
|
||||||
|
: ExecutorCompat.create(getApplicationHandler());
|
||||||
|
Futures.addCallback(
|
||||||
|
future,
|
||||||
|
new FutureCallback<MediaItemsWithStartPosition>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) {
|
||||||
|
ImmutableList<MediaItem> mediaItems = mediaItemsWithStartPosition.mediaItems;
|
||||||
|
player.setMediaItems(
|
||||||
|
mediaItems,
|
||||||
|
mediaItemsWithStartPosition.startIndex != C.INDEX_UNSET
|
||||||
|
? min(mediaItems.size() - 1, mediaItemsWithStartPosition.startIndex)
|
||||||
|
: 0,
|
||||||
|
mediaItemsWithStartPosition.startPositionMs);
|
||||||
|
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||||
|
player.prepare();
|
||||||
|
}
|
||||||
|
player.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable t) {
|
||||||
|
if (t instanceof UnsupportedOperationException) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"UnsupportedOperationException: Make sure to implement"
|
||||||
|
+ " MediaSession.Callback.onPlaybackResumption() if you add a"
|
||||||
|
+ " media button receiver to your manifest or if you implement the recent"
|
||||||
|
+ " media item contract with your MediaLibraryService.",
|
||||||
|
t);
|
||||||
|
} else {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"Failure calling MediaSession.Callback.onPlaybackResumption(): " + t.getMessage(),
|
||||||
|
t);
|
||||||
|
}
|
||||||
|
// Play as requested either way.
|
||||||
|
Util.handlePlayButtonAction(player);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
executor);
|
||||||
|
}
|
||||||
|
|
||||||
private void dispatchRemoteControllerTaskToLegacyStub(RemoteControllerTask task) {
|
private void dispatchRemoteControllerTaskToLegacyStub(RemoteControllerTask task) {
|
||||||
try {
|
try {
|
||||||
task.run(sessionLegacyStub.getControllerLegacyCbForBroadcast(), /* seq= */ 0);
|
task.run(sessionLegacyStub.getControllerLegacyCbForBroadcast(), /* seq= */ 0);
|
||||||
@ -727,6 +799,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@SuppressWarnings("QueryPermissionsNeeded") // Needs to be provided in the app manifest.
|
||||||
private static ComponentName getServiceComponentByAction(Context context, String action) {
|
private static ComponentName getServiceComponentByAction(Context context, String action) {
|
||||||
PackageManager pm = context.getPackageManager();
|
PackageManager pm = context.getPackageManager();
|
||||||
Intent queryIntent = new Intent(action);
|
Intent queryIntent = new Intent(action);
|
||||||
|
@ -135,6 +135,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
@Nullable private FutureCallback<Bitmap> pendingBitmapLoadCallback;
|
@Nullable private FutureCallback<Bitmap> pendingBitmapLoadCallback;
|
||||||
private int sessionFlags;
|
private int sessionFlags;
|
||||||
|
|
||||||
|
@SuppressWarnings("PendingIntentMutability") // We can't use SaferPendingIntent
|
||||||
public MediaSessionLegacyStub(
|
public MediaSessionLegacyStub(
|
||||||
MediaSessionImpl session,
|
MediaSessionImpl session,
|
||||||
Uri sessionUri,
|
Uri sessionUri,
|
||||||
@ -215,6 +216,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@SuppressWarnings("QueryPermissionsNeeded") // Needs to be provided in the app manifest.
|
||||||
private static ComponentName queryPackageManagerForMediaButtonReceiver(Context context) {
|
private static ComponentName queryPackageManagerForMediaButtonReceiver(Context context) {
|
||||||
PackageManager pm = context.getPackageManager();
|
PackageManager pm = context.getPackageManager();
|
||||||
Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
||||||
@ -382,7 +384,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
COMMAND_PLAY_PAUSE,
|
COMMAND_PLAY_PAUSE,
|
||||||
controller -> {
|
controller -> {
|
||||||
if (sessionImpl.onPlayRequested()) {
|
if (sessionImpl.onPlayRequested()) {
|
||||||
Util.handlePlayButtonAction(sessionImpl.getPlayerWrapper());
|
PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper();
|
||||||
|
if (playerWrapper.getMediaItemCount() == 0) {
|
||||||
|
// The player is in IDLE or ENDED state and has no media items in the playlist yet.
|
||||||
|
// Handle the play command as a playback resumption command to try resume playback.
|
||||||
|
sessionImpl.prepareAndPlayForPlaybackResumption(controller, playerWrapper);
|
||||||
|
} else {
|
||||||
|
Util.handlePlayButtonAction(playerWrapper);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sessionCompat.getCurrentControllerInfo());
|
sessionCompat.getCurrentControllerInfo());
|
||||||
|
@ -674,6 +674,10 @@ import java.util.concurrent.ExecutionException;
|
|||||||
if (caller == null) {
|
if (caller == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ControllerInfo controller = connectedControllersManager.getController(caller.asBinder());
|
||||||
|
if (controller == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
queueSessionTaskWithPlayerCommand(
|
queueSessionTaskWithPlayerCommand(
|
||||||
caller,
|
caller,
|
||||||
sequenceNumber,
|
sequenceNumber,
|
||||||
@ -684,9 +688,16 @@ import java.util.concurrent.ExecutionException;
|
|||||||
if (sessionImpl == null || sessionImpl.isReleased()) {
|
if (sessionImpl == null || sessionImpl.isReleased()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionImpl.onPlayRequested()) {
|
if (sessionImpl.onPlayRequested()) {
|
||||||
player.play();
|
if (player.getMediaItemCount() == 0) {
|
||||||
|
// The player is in IDLE or ENDED state and has no media items in the playlist
|
||||||
|
// yet.
|
||||||
|
// Handle the play command as a playback resumption command to try resume
|
||||||
|
// playback.
|
||||||
|
sessionImpl.prepareAndPlayForPlaybackResumption(controller, player);
|
||||||
|
} else {
|
||||||
|
Util.handlePlayButtonAction(player);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,7 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
@ -214,7 +215,8 @@ public class MediaSessionCallbackTest {
|
|||||||
|
|
||||||
controller.play();
|
controller.play();
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse();
|
// If IDLE, Util.handlePlayButtonAction(player) calls prepare also.
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
assertThat(commands).hasSize(2);
|
assertThat(commands).hasSize(2);
|
||||||
assertThat(commands.get(1)).isEqualTo(Player.COMMAND_PLAY_PAUSE);
|
assertThat(commands.get(1)).isEqualTo(Player.COMMAND_PLAY_PAUSE);
|
||||||
}
|
}
|
||||||
@ -810,6 +812,94 @@ public class MediaSessionCallbackTest {
|
|||||||
assertThat(player.startPositionMs).isEqualTo(200);
|
assertThat(player.startPositionMs).isEqualTo(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onPlay_withEmptyTimelinePlaybackResumptionOn_callsOnGetPlaybackResumptionPlaylist()
|
||||||
|
throws Exception {
|
||||||
|
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
||||||
|
MediaSession.Callback callback =
|
||||||
|
new MediaSession.Callback() {
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(
|
||||||
|
MediaSession mediaSession, ControllerInfo controller) {
|
||||||
|
return Futures.immediateFuture(
|
||||||
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 123L));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MediaSession session =
|
||||||
|
sessionTestRule.ensureReleaseAfterTest(
|
||||||
|
new MediaSession.Builder(context, player).setCallback(callback).build());
|
||||||
|
RemoteMediaController controller =
|
||||||
|
controllerTestRule.createRemoteController(session.getToken());
|
||||||
|
|
||||||
|
controller.play();
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(player.startMediaItemIndex).isEqualTo(1);
|
||||||
|
assertThat(player.startPositionMs).isEqualTo(123L);
|
||||||
|
assertThat(player.mediaItems).isEqualTo(mediaItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onPlay_withEmptyTimelineCallbackFailure_callsHandlePlayButtonAction()
|
||||||
|
throws Exception {
|
||||||
|
player.startMediaItemIndex = 7;
|
||||||
|
player.startPositionMs = 321L;
|
||||||
|
MediaSession session =
|
||||||
|
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
|
||||||
|
RemoteMediaController controller =
|
||||||
|
controllerTestRule.createRemoteController(session.getToken());
|
||||||
|
|
||||||
|
controller.play();
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
|
||||||
|
.isFalse();
|
||||||
|
assertThat(player.startMediaItemIndex).isEqualTo(7);
|
||||||
|
assertThat(player.startPositionMs).isEqualTo(321L);
|
||||||
|
assertThat(player.mediaItems).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onPlay_withNonEmptyTimeline_callsHandlePlayButtonAction() throws Exception {
|
||||||
|
player.timeline = new PlaylistTimeline(MediaTestUtils.createMediaItems(/* size= */ 3));
|
||||||
|
player.mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
||||||
|
player.startMediaItemIndex = 1;
|
||||||
|
player.startPositionMs = 321L;
|
||||||
|
MediaSession.Callback callback =
|
||||||
|
new MediaSession.Callback() {
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(
|
||||||
|
MediaSession mediaSession, ControllerInfo controller) {
|
||||||
|
Assert.fail();
|
||||||
|
return Futures.immediateFuture(
|
||||||
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
MediaTestUtils.createMediaItems(/* size= */ 10),
|
||||||
|
/* startIndex= */ 9,
|
||||||
|
/* startPositionMs= */ C.TIME_UNSET));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MediaSession session =
|
||||||
|
sessionTestRule.ensureReleaseAfterTest(
|
||||||
|
new MediaSession.Builder(context, player).setCallback(callback).build());
|
||||||
|
RemoteMediaController controller =
|
||||||
|
controllerTestRule.createRemoteController(session.getToken());
|
||||||
|
|
||||||
|
controller.play();
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
|
||||||
|
.isFalse();
|
||||||
|
assertThat(player.startMediaItemIndex).isEqualTo(1);
|
||||||
|
assertThat(player.startPositionMs).isEqualTo(321L);
|
||||||
|
assertThat(player.mediaItems).hasSize(3);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void onConnect() throws Exception {
|
public void onConnect() throws Exception {
|
||||||
AtomicReference<Bundle> connectionHints = new AtomicReference<>();
|
AtomicReference<Bundle> connectionHints = new AtomicReference<>();
|
||||||
|
@ -40,6 +40,7 @@ import android.support.v4.media.RatingCompat;
|
|||||||
import android.support.v4.media.session.MediaControllerCompat;
|
import android.support.v4.media.session.MediaControllerCompat;
|
||||||
import android.support.v4.media.session.MediaSessionCompat.QueueItem;
|
import android.support.v4.media.session.MediaSessionCompat.QueueItem;
|
||||||
import android.support.v4.media.session.PlaybackStateCompat;
|
import android.support.v4.media.session.PlaybackStateCompat;
|
||||||
|
import android.view.KeyEvent;
|
||||||
import androidx.media.AudioAttributesCompat;
|
import androidx.media.AudioAttributesCompat;
|
||||||
import androidx.media.AudioManagerCompat;
|
import androidx.media.AudioManagerCompat;
|
||||||
import androidx.media3.common.AudioAttributes;
|
import androidx.media3.common.AudioAttributes;
|
||||||
@ -70,6 +71,7 @@ import java.util.concurrent.CountDownLatch;
|
|||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
@ -617,6 +619,104 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
|
|||||||
assertThat(player.seekMediaItemIndex).isEqualTo(targetIndex);
|
assertThat(player.seekMediaItemIndex).isEqualTo(targetIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dispatchMediaButtonEvent_playWithEmptyTimeline_callsPreparesPlayerCorrectly()
|
||||||
|
throws Exception {
|
||||||
|
ArrayList<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
||||||
|
session =
|
||||||
|
new MediaSession.Builder(context, player)
|
||||||
|
.setId("sendMediaButtonEvent")
|
||||||
|
.setCallback(
|
||||||
|
new MediaSession.Callback() {
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<MediaSession.MediaItemsWithStartPosition>
|
||||||
|
onPlaybackResumption(MediaSession mediaSession, ControllerInfo controller) {
|
||||||
|
return Futures.immediateFuture(
|
||||||
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 123L));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
controller =
|
||||||
|
new RemoteMediaControllerCompat(
|
||||||
|
context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true);
|
||||||
|
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
|
||||||
|
|
||||||
|
session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
|
assertThat(player.startMediaItemIndex).isEqualTo(1);
|
||||||
|
assertThat(player.startPositionMs).isEqualTo(123L);
|
||||||
|
assertThat(player.mediaItems).isEqualTo(mediaItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
dispatchMediaButtonEvent_playWithEmptyTimelineCallbackFailure_callsHandlePlayButtonAction()
|
||||||
|
throws Exception {
|
||||||
|
player.mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
||||||
|
player.startMediaItemIndex = 1;
|
||||||
|
player.startPositionMs = 321L;
|
||||||
|
session = new MediaSession.Builder(context, player).setId("sendMediaButtonEvent").build();
|
||||||
|
controller =
|
||||||
|
new RemoteMediaControllerCompat(
|
||||||
|
context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true);
|
||||||
|
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
|
||||||
|
|
||||||
|
session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
|
||||||
|
.isFalse();
|
||||||
|
assertThat(player.startMediaItemIndex).isEqualTo(1);
|
||||||
|
assertThat(player.startPositionMs).isEqualTo(321L);
|
||||||
|
assertThat(player.mediaItems).hasSize(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dispatchMediaButtonEvent_playWithNonEmptyTimeline_callsHandlePlayButtonAction()
|
||||||
|
throws Exception {
|
||||||
|
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
|
||||||
|
player.mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
||||||
|
player.timeline = new PlaylistTimeline(player.mediaItems);
|
||||||
|
player.startMediaItemIndex = 1;
|
||||||
|
player.startPositionMs = 321L;
|
||||||
|
session =
|
||||||
|
new MediaSession.Builder(context, player)
|
||||||
|
.setId("sendMediaButtonEvent")
|
||||||
|
.setCallback(
|
||||||
|
new MediaSession.Callback() {
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<MediaSession.MediaItemsWithStartPosition>
|
||||||
|
onPlaybackResumption(MediaSession mediaSession, ControllerInfo controller) {
|
||||||
|
Assert.fail();
|
||||||
|
return Futures.immediateFuture(
|
||||||
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
MediaTestUtils.createMediaItems(/* size= */ 10),
|
||||||
|
/* startIndex= */ 9,
|
||||||
|
/* startPositionMs= */ 123L));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
controller =
|
||||||
|
new RemoteMediaControllerCompat(
|
||||||
|
context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true);
|
||||||
|
|
||||||
|
session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
|
||||||
|
.isFalse();
|
||||||
|
assertThat(player.startMediaItemIndex).isEqualTo(1);
|
||||||
|
assertThat(player.startPositionMs).isEqualTo(321L);
|
||||||
|
assertThat(player.mediaItems).hasSize(3);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void setShuffleMode() throws Exception {
|
public void setShuffleMode() throws Exception {
|
||||||
session =
|
session =
|
||||||
|
@ -871,7 +871,7 @@ public class MockPlayer implements Player {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMediaItemCount() {
|
public int getMediaItemCount() {
|
||||||
throw new UnsupportedOperationException();
|
return timeline.getWindowCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user