mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Use MediaSessionImpl.onMediaButtonEvent() to dispatch key events
This change moves the handling of any media button event into
`MediaSessionImpl.onMediaButtonEvent(intent)`. This includes
the double click handling from `MediaSessionLegacyStub`.
The advantage is that everything is in one place which allows
to offer `MediaSession.Callback.onMediaButtonEvent` with which
an app can override the default implementation and handle media
buttons in a custom way.
Media button events can originate from various places:
- Delivered to `MediaSessionService.onStartCommand(Intent)`
- A `PendingIntent` from the notification below API 33
- An `Intent` sent to the `MediaButtonReceiver` by the system dispatched
to the service
- Delivered to `MediaSessionCompat.Callback.onMediaButtonEvent(Intent)`
implemented by `MediaSessionLegacyStub` during the session is active
- Bluetooth (headset/remote control)
- Apps/system using `AudioManager.dispatchKeyEvent(KeyEvent)`
- Apps/system using `MediaControllerCompat.dispatchKeyEvent(keyEvent)`
Issue: androidx/media#12
Issue: androidx/media#159
Issue: androidx/media#216
Issue: androidx/media#249
#minor-release
PiperOrigin-RevId: 575231251
This commit is contained in:
parent
a8ab9e2c70
commit
a79d44edc5
8 changed files with 748 additions and 179 deletions
|
|
@ -44,6 +44,8 @@
|
||||||
(([#339](https://github.com/androidx/media/issues/339)).
|
(([#339](https://github.com/androidx/media/issues/339)).
|
||||||
* Use `DataSourceBitmapLoader` by default instead of `SimpleBitmapLoader`
|
* Use `DataSourceBitmapLoader` by default instead of `SimpleBitmapLoader`
|
||||||
([#271](https://github.com/androidx/media/issues/271),[#327](https://github.com/androidx/media/issues/327)).
|
([#271](https://github.com/androidx/media/issues/271),[#327](https://github.com/androidx/media/issues/327)).
|
||||||
|
* Add `MediaSession.Callback.onMediaButtonEvent(Intent)` that allows apps
|
||||||
|
to override the default media button event handling.
|
||||||
* UI:
|
* UI:
|
||||||
* Downloads:
|
* Downloads:
|
||||||
* OkHttp Extension:
|
* OkHttp Extension:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
@ -1444,6 +1445,32 @@ public class MediaSession {
|
||||||
MediaSession mediaSession, ControllerInfo controller) {
|
MediaSession mediaSession, ControllerInfo controller) {
|
||||||
return Futures.immediateFailedFuture(new UnsupportedOperationException());
|
return Futures.immediateFailedFuture(new UnsupportedOperationException());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a media button event has been received by the session.
|
||||||
|
*
|
||||||
|
* <p>Media3 handles media button events internally. An app can override the default behaviour
|
||||||
|
* by overriding this method.
|
||||||
|
*
|
||||||
|
* <p>Return true to stop propagating the event any further. When false is returned, Media3
|
||||||
|
* handles the event and calls {@linkplain MediaSession#getPlayer() the session player}
|
||||||
|
* accordingly.
|
||||||
|
*
|
||||||
|
* <p>Apps normally don't need to override this method. When overriding this method, an app
|
||||||
|
* can/needs to handle all API-level specifics on its own. The intent passed to this method can
|
||||||
|
* come directly from the system that routed a media key event (for instance sent by Bluetooth)
|
||||||
|
* to your session.
|
||||||
|
*
|
||||||
|
* @param session The session that received the media button event.
|
||||||
|
* @param controllerInfo The controller to which the media button event is attributed to.
|
||||||
|
* @param intent The media button intent.
|
||||||
|
* @return True if the event was handled, false otherwise.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
default boolean onMediaButtonEvent(
|
||||||
|
MediaSession session, ControllerInfo controllerInfo, Intent intent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Representation of a list of {@linkplain MediaItem media items} and where to start playing. */
|
/** Representation of a list of {@linkplain MediaItem media items} and where to start playing. */
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS;
|
||||||
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
|
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
import static androidx.media3.common.util.Util.SDK_INT;
|
|
||||||
import static androidx.media3.common.util.Util.postOrRun;
|
import static androidx.media3.common.util.Util.postOrRun;
|
||||||
import static androidx.media3.session.MediaSessionStub.UNKNOWN_SEQUENCE_NUMBER;
|
import static androidx.media3.session.MediaSessionStub.UNKNOWN_SEQUENCE_NUMBER;
|
||||||
import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
|
import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
|
||||||
|
|
@ -52,6 +51,7 @@ import android.os.RemoteException;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.ViewConfiguration;
|
||||||
import androidx.annotation.CheckResult;
|
import androidx.annotation.CheckResult;
|
||||||
import androidx.annotation.FloatRange;
|
import androidx.annotation.FloatRange;
|
||||||
import androidx.annotation.GuardedBy;
|
import androidx.annotation.GuardedBy;
|
||||||
|
|
@ -116,6 +116,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
private final Uri sessionUri;
|
private final Uri sessionUri;
|
||||||
private final PlayerInfoChangedHandler onPlayerInfoChangedHandler;
|
private final PlayerInfoChangedHandler onPlayerInfoChangedHandler;
|
||||||
|
private final MediaPlayPauseKeyHandler mediaPlayPauseKeyHandler;
|
||||||
private final MediaSession.Callback callback;
|
private final MediaSession.Callback callback;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final MediaSessionStub sessionStub;
|
private final MediaSessionStub sessionStub;
|
||||||
|
|
@ -161,28 +162,30 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
BitmapLoader bitmapLoader,
|
BitmapLoader bitmapLoader,
|
||||||
boolean playIfSuppressed,
|
boolean playIfSuppressed,
|
||||||
boolean isPeriodicPositionUpdateEnabled) {
|
boolean isPeriodicPositionUpdateEnabled) {
|
||||||
this.context = context;
|
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
|
this.context = context;
|
||||||
|
sessionId = id;
|
||||||
|
this.sessionActivity = sessionActivity;
|
||||||
|
this.customLayout = customLayout;
|
||||||
|
this.callback = callback;
|
||||||
|
this.bitmapLoader = bitmapLoader;
|
||||||
|
this.playIfSuppressed = playIfSuppressed;
|
||||||
|
this.isPeriodicPositionUpdateEnabled = isPeriodicPositionUpdateEnabled;
|
||||||
|
|
||||||
@SuppressWarnings("nullness:assignment")
|
@SuppressWarnings("nullness:assignment")
|
||||||
@Initialized
|
@Initialized
|
||||||
MediaSessionImpl thisRef = this;
|
MediaSessionImpl thisRef = this;
|
||||||
|
|
||||||
sessionStub = new MediaSessionStub(thisRef);
|
sessionStub = new MediaSessionStub(thisRef);
|
||||||
this.sessionActivity = sessionActivity;
|
|
||||||
this.customLayout = customLayout;
|
|
||||||
|
|
||||||
mainHandler = new Handler(Looper.getMainLooper());
|
mainHandler = new Handler(Looper.getMainLooper());
|
||||||
applicationHandler = new Handler(player.getApplicationLooper());
|
Looper applicationLooper = player.getApplicationLooper();
|
||||||
this.callback = callback;
|
applicationHandler = new Handler(applicationLooper);
|
||||||
this.bitmapLoader = bitmapLoader;
|
|
||||||
this.playIfSuppressed = playIfSuppressed;
|
|
||||||
this.isPeriodicPositionUpdateEnabled = isPeriodicPositionUpdateEnabled;
|
|
||||||
|
|
||||||
playerInfo = PlayerInfo.DEFAULT;
|
playerInfo = PlayerInfo.DEFAULT;
|
||||||
onPlayerInfoChangedHandler = new PlayerInfoChangedHandler(player.getApplicationLooper());
|
onPlayerInfoChangedHandler = new PlayerInfoChangedHandler(applicationLooper);
|
||||||
|
mediaPlayPauseKeyHandler = new MediaPlayPauseKeyHandler(applicationLooper);
|
||||||
|
|
||||||
sessionId = id;
|
|
||||||
// Build Uri that differentiate sessions across the creation/destruction in PendingIntent.
|
// Build Uri that differentiate sessions across the creation/destruction in PendingIntent.
|
||||||
// Here's the reason why Session ID / SessionToken aren't suitable here.
|
// Here's the reason why Session ID / SessionToken aren't suitable here.
|
||||||
// - Session ID
|
// - Session ID
|
||||||
|
|
@ -280,6 +283,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
}
|
}
|
||||||
closed = true;
|
closed = true;
|
||||||
}
|
}
|
||||||
|
mediaPlayPauseKeyHandler.clearPendingPlayPauseTask();
|
||||||
applicationHandler.removeCallbacksAndMessages(null);
|
applicationHandler.removeCallbacksAndMessages(null);
|
||||||
try {
|
try {
|
||||||
postOrRun(
|
postOrRun(
|
||||||
|
|
@ -1080,7 +1084,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
(callback, seq) -> callback.onDeviceInfoChanged(seq, playerInfo.deviceInfo));
|
(callback, seq) -> callback.onDeviceInfoChanged(seq, playerInfo.deviceInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ boolean onMediaButtonEvent(Intent intent) {
|
/**
|
||||||
|
* Returns true if the media button event was handled, false otherwise.
|
||||||
|
*
|
||||||
|
* <p>Must be called on the application thread of the session.
|
||||||
|
*
|
||||||
|
* @param callerInfo The calling {@link ControllerInfo}.
|
||||||
|
* @param intent The media button intent.
|
||||||
|
* @return True if the event was handled, false otherwise.
|
||||||
|
*/
|
||||||
|
/* package */ boolean onMediaButtonEvent(ControllerInfo callerInfo, Intent intent) {
|
||||||
KeyEvent keyEvent = DefaultActionFactory.getKeyEvent(intent);
|
KeyEvent keyEvent = DefaultActionFactory.getKeyEvent(intent);
|
||||||
ComponentName intentComponent = intent.getComponent();
|
ComponentName intentComponent = intent.getComponent();
|
||||||
if (!Objects.equals(intent.getAction(), Intent.ACTION_MEDIA_BUTTON)
|
if (!Objects.equals(intent.getAction(), Intent.ACTION_MEDIA_BUTTON)
|
||||||
|
|
@ -1090,18 +1103,66 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|| keyEvent.getAction() != KeyEvent.ACTION_DOWN) {
|
|| keyEvent.getAction() != KeyEvent.ACTION_DOWN) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ControllerInfo controllerInfo = getMediaNotificationControllerInfo();
|
|
||||||
if (controllerInfo == null) {
|
verifyApplicationThread();
|
||||||
if (intentComponent != null) {
|
if (callback.onMediaButtonEvent(instance, callerInfo, intent)) {
|
||||||
// Fallback to legacy if this is a media button event sent to one of our components.
|
// Event handled by app callback.
|
||||||
return getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent)
|
return true;
|
||||||
|| SDK_INT < 21;
|
}
|
||||||
}
|
// Double tap detection.
|
||||||
return false;
|
int keyCode = keyEvent.getKeyCode();
|
||||||
|
boolean doubleTapCompleted = false;
|
||||||
|
switch (keyCode) {
|
||||||
|
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
|
||||||
|
case KeyEvent.KEYCODE_HEADSETHOOK:
|
||||||
|
if (callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION
|
||||||
|
|| keyEvent.getRepeatCount() != 0) {
|
||||||
|
// Double tap detection is only for media button events from external sources
|
||||||
|
// (for instance Bluetooth) and excluding long press (repeatCount > 0).
|
||||||
|
mediaPlayPauseKeyHandler.flush();
|
||||||
|
} else if (mediaPlayPauseKeyHandler.hasPendingPlayPauseTask()) {
|
||||||
|
// A double tap arrived. Clear the pending playPause task.
|
||||||
|
mediaPlayPauseKeyHandler.clearPendingPlayPauseTask();
|
||||||
|
doubleTapCompleted = true;
|
||||||
|
} else {
|
||||||
|
// Handle event with a delayed callback that's run if no double tap arrives in time.
|
||||||
|
mediaPlayPauseKeyHandler.setPendingPlayPauseTask(callerInfo, keyEvent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// If another key is pressed within double tap timeout, make play/pause as a single tap to
|
||||||
|
// handle media keys in order.
|
||||||
|
mediaPlayPauseKeyHandler.flush();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isMediaNotificationControllerConnected()) {
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE && doubleTapCompleted) {
|
||||||
|
// Double tap completion for legacy when media notification controller is disabled.
|
||||||
|
sessionLegacyStub.onSkipToNext();
|
||||||
|
return true;
|
||||||
|
} else if (callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION) {
|
||||||
|
sessionLegacyStub.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// This is an unhandled framework event. Return false to let the framework resolve by calling
|
||||||
|
// `MediaSessionCompat.Callback.onXyz()`.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Send from media notification controller.
|
||||||
|
return applyMediaButtonKeyEvent(keyEvent, doubleTapCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean applyMediaButtonKeyEvent(KeyEvent keyEvent, boolean doubleTapCompleted) {
|
||||||
|
ControllerInfo controllerInfo = checkNotNull(instance.getMediaNotificationControllerInfo());
|
||||||
Runnable command;
|
Runnable command;
|
||||||
switch (keyEvent.getKeyCode()) {
|
int keyCode = keyEvent.getKeyCode();
|
||||||
|
if ((keyCode == KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KEYCODE_MEDIA_PLAY)
|
||||||
|
&& doubleTapCompleted) {
|
||||||
|
keyCode = KEYCODE_MEDIA_NEXT;
|
||||||
|
}
|
||||||
|
switch (keyCode) {
|
||||||
case KEYCODE_MEDIA_PLAY_PAUSE:
|
case KEYCODE_MEDIA_PLAY_PAUSE:
|
||||||
command =
|
command =
|
||||||
getPlayerWrapper().getPlayWhenReady()
|
getPlayerWrapper().getPlayWhenReady()
|
||||||
|
|
@ -1653,6 +1714,56 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handler for double click detection.
|
||||||
|
*
|
||||||
|
* <p>All methods must be called on the application thread.
|
||||||
|
*/
|
||||||
|
private class MediaPlayPauseKeyHandler extends Handler {
|
||||||
|
|
||||||
|
@Nullable private Runnable playPauseTask;
|
||||||
|
|
||||||
|
public MediaPlayPauseKeyHandler(Looper applicationLooper) {
|
||||||
|
super(applicationLooper);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPendingPlayPauseTask(ControllerInfo controllerInfo, KeyEvent keyEvent) {
|
||||||
|
playPauseTask =
|
||||||
|
() -> {
|
||||||
|
if (isMediaNotificationController(controllerInfo)) {
|
||||||
|
applyMediaButtonKeyEvent(keyEvent, /* doubleTapCompleted= */ false);
|
||||||
|
} else {
|
||||||
|
sessionLegacyStub.handleMediaPlayPauseOnHandler(
|
||||||
|
checkNotNull(controllerInfo.getRemoteUserInfo()));
|
||||||
|
}
|
||||||
|
playPauseTask = null;
|
||||||
|
};
|
||||||
|
postDelayed(playPauseTask, ViewConfiguration.getDoubleTapTimeout());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Runnable clearPendingPlayPauseTask() {
|
||||||
|
if (playPauseTask != null) {
|
||||||
|
removeCallbacks(playPauseTask);
|
||||||
|
Runnable task = playPauseTask;
|
||||||
|
playPauseTask = null;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPendingPlayPauseTask() {
|
||||||
|
return playPauseTask != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void flush() {
|
||||||
|
@Nullable Runnable task = clearPendingPlayPauseTask();
|
||||||
|
if (task != null) {
|
||||||
|
postOrRun(this, task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class PlayerInfoChangedHandler extends Handler {
|
private class PlayerInfoChangedHandler extends Handler {
|
||||||
|
|
||||||
private static final int MSG_PLAYER_INFO_CHANGED = 1;
|
private static final int MSG_PLAYER_INFO_CHANGED = 1;
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ import android.support.v4.media.session.MediaSessionCompat.QueueItem;
|
||||||
import android.support.v4.media.session.PlaybackStateCompat;
|
import android.support.v4.media.session.PlaybackStateCompat;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.ViewConfiguration;
|
|
||||||
import androidx.annotation.DoNotInline;
|
import androidx.annotation.DoNotInline;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
@ -126,9 +125,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
private final MediaSessionManager sessionManager;
|
private final MediaSessionManager sessionManager;
|
||||||
private final ControllerLegacyCbForBroadcast controllerLegacyCbForBroadcast;
|
private final ControllerLegacyCbForBroadcast controllerLegacyCbForBroadcast;
|
||||||
private final ConnectionTimeoutHandler connectionTimeoutHandler;
|
private final ConnectionTimeoutHandler connectionTimeoutHandler;
|
||||||
private final MediaPlayPauseKeyHandler mediaPlayPauseKeyHandler;
|
|
||||||
private final MediaSessionCompat sessionCompat;
|
private final MediaSessionCompat sessionCompat;
|
||||||
private final String appPackageName;
|
|
||||||
@Nullable private final MediaButtonReceiver runtimeBroadcastReceiver;
|
@Nullable private final MediaButtonReceiver runtimeBroadcastReceiver;
|
||||||
@Nullable private final ComponentName broadcastReceiverComponentName;
|
@Nullable private final ComponentName broadcastReceiverComponentName;
|
||||||
@Nullable private VolumeProviderCompat volumeProviderCompat;
|
@Nullable private VolumeProviderCompat volumeProviderCompat;
|
||||||
|
|
@ -141,11 +138,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
public MediaSessionLegacyStub(MediaSessionImpl session, Uri sessionUri, Handler handler) {
|
public MediaSessionLegacyStub(MediaSessionImpl session, Uri sessionUri, Handler handler) {
|
||||||
sessionImpl = session;
|
sessionImpl = session;
|
||||||
Context context = sessionImpl.getContext();
|
Context context = sessionImpl.getContext();
|
||||||
appPackageName = context.getPackageName();
|
|
||||||
sessionManager = MediaSessionManager.getSessionManager(context);
|
sessionManager = MediaSessionManager.getSessionManager(context);
|
||||||
controllerLegacyCbForBroadcast = new ControllerLegacyCbForBroadcast();
|
controllerLegacyCbForBroadcast = new ControllerLegacyCbForBroadcast();
|
||||||
mediaPlayPauseKeyHandler =
|
|
||||||
new MediaPlayPauseKeyHandler(session.getApplicationHandler().getLooper());
|
|
||||||
connectedControllersManager = new ConnectedControllersManager<>(session);
|
connectedControllersManager = new ConnectedControllersManager<>(session);
|
||||||
connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS;
|
connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS;
|
||||||
connectionTimeoutHandler =
|
connectionTimeoutHandler =
|
||||||
|
|
@ -318,41 +312,16 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
|
public boolean onMediaButtonEvent(Intent intent) {
|
||||||
@Nullable KeyEvent keyEvent = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
|
return sessionImpl.onMediaButtonEvent(
|
||||||
if (keyEvent == null || keyEvent.getAction() != KeyEvent.ACTION_DOWN) {
|
new ControllerInfo(
|
||||||
return false;
|
sessionCompat.getCurrentControllerInfo(),
|
||||||
}
|
ControllerInfo.LEGACY_CONTROLLER_VERSION,
|
||||||
RemoteUserInfo remoteUserInfo = sessionCompat.getCurrentControllerInfo();
|
ControllerInfo.LEGACY_CONTROLLER_INTERFACE_VERSION,
|
||||||
int keyCode = keyEvent.getKeyCode();
|
/* trusted= */ false,
|
||||||
switch (keyCode) {
|
/* cb= */ null,
|
||||||
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
|
/* connectionHints= */ Bundle.EMPTY),
|
||||||
case KeyEvent.KEYCODE_HEADSETHOOK:
|
intent);
|
||||||
// Double tap detection only for media button events from external sources (for instance
|
|
||||||
// Bluetooth). Media button events from the app package are coming from the notification
|
|
||||||
// below targetApiLevel 33.
|
|
||||||
if (!appPackageName.equals(remoteUserInfo.getPackageName())
|
|
||||||
&& keyEvent.getRepeatCount() == 0) {
|
|
||||||
if (mediaPlayPauseKeyHandler.hasPendingMediaPlayPauseKey()) {
|
|
||||||
mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey();
|
|
||||||
onSkipToNext();
|
|
||||||
} else {
|
|
||||||
mediaPlayPauseKeyHandler.addPendingMediaPlayPauseKey(remoteUserInfo);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Consider long-press as a single tap. Handle immediately.
|
|
||||||
handleMediaPlayPauseOnHandler(remoteUserInfo);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
// If another key is pressed within double tap timeout, consider the pending
|
|
||||||
// pending play/pause as a single tap to handle media keys in order.
|
|
||||||
if (mediaPlayPauseKeyHandler.hasPendingMediaPlayPauseKey()) {
|
|
||||||
handleMediaPlayPauseOnHandler(remoteUserInfo);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeUpdateFlags(PlayerWrapper playerWrapper) {
|
private void maybeUpdateFlags(PlayerWrapper playerWrapper) {
|
||||||
|
|
@ -366,8 +335,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleMediaPlayPauseOnHandler(RemoteUserInfo remoteUserInfo) {
|
/* package */ void handleMediaPlayPauseOnHandler(RemoteUserInfo remoteUserInfo) {
|
||||||
mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey();
|
|
||||||
dispatchSessionTaskWithPlayerCommand(
|
dispatchSessionTaskWithPlayerCommand(
|
||||||
COMMAND_PLAY_PAUSE,
|
COMMAND_PLAY_PAUSE,
|
||||||
controller ->
|
controller ->
|
||||||
|
|
@ -1435,34 +1403,6 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MediaPlayPauseKeyHandler extends Handler {
|
|
||||||
|
|
||||||
private static final int MSG_DOUBLE_TAP_TIMED_OUT = 1002;
|
|
||||||
|
|
||||||
public MediaPlayPauseKeyHandler(Looper looper) {
|
|
||||||
super(looper);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
RemoteUserInfo remoteUserInfo = (RemoteUserInfo) msg.obj;
|
|
||||||
handleMediaPlayPauseOnHandler(remoteUserInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addPendingMediaPlayPauseKey(RemoteUserInfo remoteUserInfo) {
|
|
||||||
Message msg = obtainMessage(MSG_DOUBLE_TAP_TIMED_OUT, remoteUserInfo);
|
|
||||||
sendMessageDelayed(msg, ViewConfiguration.getDoubleTapTimeout());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearPendingMediaPlayPauseKey() {
|
|
||||||
removeMessages(MSG_DOUBLE_TAP_TIMED_OUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasPendingMediaPlayPauseKey() {
|
|
||||||
return hasMessages(MSG_DOUBLE_TAP_TIMED_OUT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getBitmapLoadErrorMessage(Throwable throwable) {
|
private static String getBitmapLoadErrorMessage(Throwable throwable) {
|
||||||
return "Failed to load bitmap: " + throwable.getMessage();
|
return "Failed to load bitmap: " + throwable.getMessage();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import static androidx.media3.common.util.Util.postOrRun;
|
||||||
|
|
||||||
import android.app.ForegroundServiceStartNotAllowedException;
|
import android.app.ForegroundServiceStartNotAllowedException;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
@ -39,6 +40,7 @@ import androidx.annotation.RequiresApi;
|
||||||
import androidx.collection.ArrayMap;
|
import androidx.collection.ArrayMap;
|
||||||
import androidx.media.MediaBrowserServiceCompat;
|
import androidx.media.MediaBrowserServiceCompat;
|
||||||
import androidx.media.MediaSessionManager;
|
import androidx.media.MediaSessionManager;
|
||||||
|
import androidx.media3.common.MediaLibraryInfo;
|
||||||
import androidx.media3.common.util.Log;
|
import androidx.media3.common.util.Log;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
|
|
@ -425,9 +427,19 @@ public abstract class MediaSessionService extends Service {
|
||||||
}
|
}
|
||||||
addSession(session);
|
addSession(session);
|
||||||
}
|
}
|
||||||
if (!session.getImpl().onMediaButtonEvent(intent)) {
|
MediaSessionImpl sessionImpl = session.getImpl();
|
||||||
Log.w(TAG, "Ignoring unrecognized media button intent.");
|
sessionImpl
|
||||||
}
|
.getApplicationHandler()
|
||||||
|
.post(
|
||||||
|
() -> {
|
||||||
|
ControllerInfo callerInfo = sessionImpl.getMediaNotificationControllerInfo();
|
||||||
|
if (callerInfo == null) {
|
||||||
|
callerInfo = createFallbackMediaButtonCaller(intent);
|
||||||
|
}
|
||||||
|
if (!sessionImpl.onMediaButtonEvent(callerInfo, intent)) {
|
||||||
|
Log.d(TAG, "Ignored unrecognized media button intent.");
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (session != null && actionFactory.isCustomAction(intent)) {
|
} else if (session != null && actionFactory.isCustomAction(intent)) {
|
||||||
@Nullable String customAction = actionFactory.getCustomAction(intent);
|
@Nullable String customAction = actionFactory.getCustomAction(intent);
|
||||||
if (customAction == null) {
|
if (customAction == null) {
|
||||||
|
|
@ -439,6 +451,24 @@ public abstract class MediaSessionService extends Service {
|
||||||
return START_STICKY;
|
return START_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ControllerInfo createFallbackMediaButtonCaller(Intent mediaButtonIntent) {
|
||||||
|
@Nullable ComponentName componentName = mediaButtonIntent.getComponent();
|
||||||
|
String packageName =
|
||||||
|
componentName != null
|
||||||
|
? componentName.getPackageName()
|
||||||
|
: "androidx.media3.session.MediaSessionService";
|
||||||
|
return new ControllerInfo(
|
||||||
|
new MediaSessionManager.RemoteUserInfo(
|
||||||
|
packageName,
|
||||||
|
MediaSessionManager.RemoteUserInfo.UNKNOWN_PID,
|
||||||
|
MediaSessionManager.RemoteUserInfo.UNKNOWN_UID),
|
||||||
|
MediaLibraryInfo.VERSION_INT,
|
||||||
|
MediaControllerStub.VERSION_INT,
|
||||||
|
/* trusted= */ false,
|
||||||
|
/* cb= */ null,
|
||||||
|
/* connectionHints= */ Bundle.EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the service is no longer used and is being removed.
|
* Called when the service is no longer used and is being removed.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import static androidx.media3.common.Player.COMMAND_PREPARE;
|
||||||
import static androidx.media3.common.Player.STATE_ENDED;
|
import static androidx.media3.common.Player.STATE_ENDED;
|
||||||
import static androidx.media3.common.Player.STATE_IDLE;
|
import static androidx.media3.common.Player.STATE_IDLE;
|
||||||
import static androidx.media3.common.Player.STATE_READY;
|
import static androidx.media3.common.Player.STATE_READY;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE;
|
import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE;
|
||||||
import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
|
import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
|
||||||
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
|
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
|
||||||
|
|
@ -46,6 +47,7 @@ import androidx.media.AudioManagerCompat;
|
||||||
import androidx.media3.common.AudioAttributes;
|
import androidx.media3.common.AudioAttributes;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.DeviceInfo;
|
import androidx.media3.common.DeviceInfo;
|
||||||
|
import androidx.media3.common.ForwardingPlayer;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.Rating;
|
import androidx.media3.common.Rating;
|
||||||
|
|
@ -71,7 +73,6 @@ 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;
|
||||||
|
|
@ -93,6 +94,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
|
||||||
|
|
||||||
@Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG);
|
@Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG);
|
||||||
|
|
||||||
|
@Rule public final MediaSessionTestRule mediaSessionTestRule = new MediaSessionTestRule();
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
private TestHandler handler;
|
private TestHandler handler;
|
||||||
private MediaSession session;
|
private MediaSession session;
|
||||||
|
|
@ -615,37 +618,47 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void dispatchMediaButtonEvent_playWithEmptyTimeline_callsPreparesPlayerCorrectly()
|
public void dispatchMediaButtonEvent_playWithEmptyTimeline_callsPlaybackResumptionPrepareAndPlay()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
ArrayList<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
ArrayList<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
||||||
session =
|
AtomicReference<MediaSession> session = new AtomicReference<>();
|
||||||
new MediaSession.Builder(context, player)
|
CallerCollectorPlayer callerCollectorPlayer = new CallerCollectorPlayer(session, player);
|
||||||
.setId("sendMediaButtonEvent")
|
session.set(
|
||||||
.setCallback(
|
mediaSessionTestRule.ensureReleaseAfterTest(
|
||||||
new MediaSession.Callback() {
|
new MediaSession.Builder(context, callerCollectorPlayer)
|
||||||
@Override
|
.setId("dispatchMediaButtonEvent")
|
||||||
public ListenableFuture<MediaSession.MediaItemsWithStartPosition>
|
.setCallback(
|
||||||
onPlaybackResumption(MediaSession mediaSession, ControllerInfo controller) {
|
new MediaSession.Callback() {
|
||||||
return Futures.immediateFuture(
|
@Override
|
||||||
new MediaSession.MediaItemsWithStartPosition(
|
public ListenableFuture<MediaSession.MediaItemsWithStartPosition>
|
||||||
mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 123L));
|
onPlaybackResumption(
|
||||||
}
|
MediaSession mediaSession, ControllerInfo controller) {
|
||||||
})
|
return Futures.immediateFuture(
|
||||||
.build();
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 123L));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()));
|
||||||
controller =
|
controller =
|
||||||
new RemoteMediaControllerCompat(
|
new RemoteMediaControllerCompat(
|
||||||
context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true);
|
context,
|
||||||
|
session.get().getSessionCompat().getSessionToken(),
|
||||||
|
/* waitForConnection= */ true);
|
||||||
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
|
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
|
||||||
|
|
||||||
session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
session.get().getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
|
||||||
.isTrue();
|
.isTrue();
|
||||||
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
assertThat(player.startMediaItemIndex).isEqualTo(1);
|
assertThat(player.startMediaItemIndex).isEqualTo(1);
|
||||||
assertThat(player.startPositionMs).isEqualTo(123L);
|
assertThat(player.startPositionMs).isEqualTo(123L);
|
||||||
assertThat(player.mediaItems).isEqualTo(mediaItems);
|
assertThat(player.mediaItems).isEqualTo(mediaItems);
|
||||||
|
assertThat(callerCollectorPlayer.callers).hasSize(3);
|
||||||
|
for (ControllerInfo controllerInfo : callerCollectorPlayer.callers) {
|
||||||
|
assertThat(session.get().isMediaNotificationController(controllerInfo)).isFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -739,7 +752,59 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
dispatchMediaButtonEvent_playWithEmptyTimelineCallbackFailure_callsHandlePlayButtonAction()
|
dispatchMediaButtonEvent_playWithEmptyTimelineWithMediaNotificationController_callsPlaybackResumptionPrepareAndPlay()
|
||||||
|
throws Exception {
|
||||||
|
ArrayList<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
||||||
|
AtomicReference<MediaSession> session = new AtomicReference<>();
|
||||||
|
CallerCollectorPlayer callerCollectorPlayer = new CallerCollectorPlayer(session, player);
|
||||||
|
session.set(
|
||||||
|
mediaSessionTestRule.ensureReleaseAfterTest(
|
||||||
|
new MediaSession.Builder(context, callerCollectorPlayer)
|
||||||
|
.setId("dispatchMediaButtonEvent")
|
||||||
|
.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.get().getSessionCompat().getSessionToken(),
|
||||||
|
/* waitForConnection= */ true);
|
||||||
|
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY);
|
||||||
|
Bundle connectionHints = new Bundle();
|
||||||
|
connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true);
|
||||||
|
new MediaController.Builder(
|
||||||
|
ApplicationProvider.getApplicationContext(), session.get().getToken())
|
||||||
|
.setConnectionHints(connectionHints)
|
||||||
|
.buildAsync()
|
||||||
|
.get();
|
||||||
|
|
||||||
|
session.get().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);
|
||||||
|
assertThat(callerCollectorPlayer.callers).hasSize(3);
|
||||||
|
for (ControllerInfo controllerInfo : callerCollectorPlayer.callers) {
|
||||||
|
assertThat(session.get().isMediaNotificationController(controllerInfo)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
dispatchMediaButtonEvent_playWithEmptyTimelinePlaybackResumptionFailure_callsHandlePlayButtonAction()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
player.mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
player.mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
|
||||||
player.startMediaItemIndex = 1;
|
player.startMediaItemIndex = 1;
|
||||||
|
|
@ -781,28 +846,20 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
|
||||||
player.timeline = new PlaylistTimeline(player.mediaItems);
|
player.timeline = new PlaylistTimeline(player.mediaItems);
|
||||||
player.startMediaItemIndex = 1;
|
player.startMediaItemIndex = 1;
|
||||||
player.startPositionMs = 321L;
|
player.startPositionMs = 321L;
|
||||||
session =
|
AtomicReference<MediaSession> session = new AtomicReference<>();
|
||||||
new MediaSession.Builder(context, player)
|
CallerCollectorPlayer callerCollectorPlayer = new CallerCollectorPlayer(session, player);
|
||||||
.setId("sendMediaButtonEvent")
|
session.set(
|
||||||
.setCallback(
|
mediaSessionTestRule.ensureReleaseAfterTest(
|
||||||
new MediaSession.Callback() {
|
new MediaSession.Builder(context, callerCollectorPlayer)
|
||||||
@Override
|
.setId("dispatchMediaButtonEvent")
|
||||||
public ListenableFuture<MediaSession.MediaItemsWithStartPosition>
|
.build()));
|
||||||
onPlaybackResumption(MediaSession mediaSession, ControllerInfo controller) {
|
|
||||||
Assert.fail();
|
|
||||||
return Futures.immediateFuture(
|
|
||||||
new MediaSession.MediaItemsWithStartPosition(
|
|
||||||
MediaTestUtils.createMediaItems(/* size= */ 10),
|
|
||||||
/* startIndex= */ 9,
|
|
||||||
/* startPositionMs= */ 123L));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
controller =
|
controller =
|
||||||
new RemoteMediaControllerCompat(
|
new RemoteMediaControllerCompat(
|
||||||
context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true);
|
context,
|
||||||
|
session.get().getSessionCompat().getSessionToken(),
|
||||||
|
/* waitForConnection= */ true);
|
||||||
|
|
||||||
session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
session.get().getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
|
|
||||||
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
|
|
@ -811,6 +868,50 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
|
||||||
assertThat(player.startMediaItemIndex).isEqualTo(1);
|
assertThat(player.startMediaItemIndex).isEqualTo(1);
|
||||||
assertThat(player.startPositionMs).isEqualTo(321L);
|
assertThat(player.startPositionMs).isEqualTo(321L);
|
||||||
assertThat(player.mediaItems).hasSize(3);
|
assertThat(player.mediaItems).hasSize(3);
|
||||||
|
assertThat(callerCollectorPlayer.callers).hasSize(2);
|
||||||
|
for (ControllerInfo controllerInfo : callerCollectorPlayer.callers) {
|
||||||
|
assertThat(session.get().isMediaNotificationController(controllerInfo)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
dispatchMediaButtonEvent_playWithNonEmptyTimelineWithMediaNotificationController_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);
|
||||||
|
AtomicReference<MediaSession> session = new AtomicReference<>();
|
||||||
|
CallerCollectorPlayer callerCollectorPlayer = new CallerCollectorPlayer(session, player);
|
||||||
|
session.set(
|
||||||
|
mediaSessionTestRule.ensureReleaseAfterTest(
|
||||||
|
new MediaSession.Builder(context, callerCollectorPlayer)
|
||||||
|
.setId("dispatchMediaButtonEvent")
|
||||||
|
.build()));
|
||||||
|
Bundle connectionHints = new Bundle();
|
||||||
|
connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true);
|
||||||
|
new MediaController.Builder(
|
||||||
|
ApplicationProvider.getApplicationContext(), session.get().getToken())
|
||||||
|
.setConnectionHints(connectionHints)
|
||||||
|
.buildAsync()
|
||||||
|
.get();
|
||||||
|
controller =
|
||||||
|
new RemoteMediaControllerCompat(
|
||||||
|
context,
|
||||||
|
session.get().getSessionCompat().getSessionToken(),
|
||||||
|
/* waitForConnection= */ true);
|
||||||
|
|
||||||
|
session.get().getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
|
assertThat(player.mediaItems).hasSize(3);
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
|
||||||
|
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
|
||||||
|
.isFalse();
|
||||||
|
assertThat(callerCollectorPlayer.callers).hasSize(2);
|
||||||
|
for (ControllerInfo controllerInfo : callerCollectorPlayer.callers) {
|
||||||
|
assertThat(session.get().isMediaNotificationController(controllerInfo)).isTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -1807,4 +1908,33 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
|
||||||
return MediaSession.ConnectionResult.reject();
|
return MediaSession.ConnectionResult.reject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class CallerCollectorPlayer extends ForwardingPlayer {
|
||||||
|
private final List<ControllerInfo> callers;
|
||||||
|
private final AtomicReference<MediaSession> mediaSession;
|
||||||
|
|
||||||
|
public CallerCollectorPlayer(AtomicReference<MediaSession> mediaSession, MockPlayer player) {
|
||||||
|
super(player);
|
||||||
|
this.mediaSession = mediaSession;
|
||||||
|
callers = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
||||||
|
callers.add(checkNotNull(mediaSession.get().getControllerForCurrentRequest()));
|
||||||
|
super.setMediaItems(mediaItems, startIndex, startPositionMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare() {
|
||||||
|
callers.add(checkNotNull(mediaSession.get().getControllerForCurrentRequest()));
|
||||||
|
super.prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void play() {
|
||||||
|
callers.add(checkNotNull(mediaSession.get().getControllerForCurrentRequest()));
|
||||||
|
super.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
package androidx.media3.session;
|
package androidx.media3.session;
|
||||||
|
|
||||||
import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER;
|
import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER;
|
||||||
|
import static androidx.media3.common.Player.STATE_ENDED;
|
||||||
|
import static androidx.media3.session.MediaSession.ControllerInfo.LEGACY_CONTROLLER_VERSION;
|
||||||
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
|
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
|
||||||
import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS;
|
import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS;
|
||||||
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
|
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
|
||||||
|
|
@ -26,7 +28,9 @@ import static org.junit.Assume.assumeTrue;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.MediaPlayer;
|
import android.media.MediaPlayer;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import androidx.media3.common.ForwardingPlayer;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.session.MediaSession.ControllerInfo;
|
import androidx.media3.session.MediaSession.ControllerInfo;
|
||||||
|
|
@ -37,6 +41,8 @@ import androidx.media3.test.session.common.TestHandler;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.filters.LargeTest;
|
import androidx.test.filters.LargeTest;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Assume;
|
import org.junit.Assume;
|
||||||
|
|
@ -69,6 +75,7 @@ public class MediaSessionKeyEventTest {
|
||||||
private MediaSession session;
|
private MediaSession session;
|
||||||
private MockPlayer player;
|
private MockPlayer player;
|
||||||
private TestSessionCallback sessionCallback;
|
private TestSessionCallback sessionCallback;
|
||||||
|
private CallerCollectorPlayer callerCollectorPlayer;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
|
|
@ -78,10 +85,14 @@ public class MediaSessionKeyEventTest {
|
||||||
Context context = ApplicationProvider.getApplicationContext();
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
handler = threadTestRule.getHandler();
|
handler = threadTestRule.getHandler();
|
||||||
player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build();
|
player =
|
||||||
|
new MockPlayer.Builder().setMediaItems(1).setApplicationLooper(handler.getLooper()).build();
|
||||||
sessionCallback = new TestSessionCallback();
|
sessionCallback = new TestSessionCallback();
|
||||||
session = new MediaSession.Builder(context, player).setCallback(sessionCallback).build();
|
callerCollectorPlayer = new CallerCollectorPlayer(player);
|
||||||
|
session =
|
||||||
|
new MediaSession.Builder(context, callerCollectorPlayer)
|
||||||
|
.setCallback(sessionCallback)
|
||||||
|
.build();
|
||||||
|
|
||||||
// Here's the requirement for an app to receive media key events via MediaSession.
|
// Here's the requirement for an app to receive media key events via MediaSession.
|
||||||
// - SDK < 26: Player should be playing for receiving key events
|
// - SDK < 26: Player should be playing for receiving key events
|
||||||
|
|
@ -160,6 +171,92 @@ public class MediaSessionKeyEventTest {
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
fastForwardKeyEvent_mediaNotificationControllerConnected_callFromNotificationController()
|
||||||
|
throws Exception {
|
||||||
|
Assume.assumeTrue(Util.SDK_INT >= 21); // TODO: b/199064299 - Lower minSdk to 19.
|
||||||
|
MediaController controller = connectMediaNotificationController();
|
||||||
|
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, /* doubleTap= */ false);
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS);
|
||||||
|
assertThat(callerCollectorPlayer.callers).hasSize(1);
|
||||||
|
assertThat(callerCollectorPlayer.callers.get(0).getControllerVersion())
|
||||||
|
.isNotEqualTo(LEGACY_CONTROLLER_VERSION);
|
||||||
|
assertThat(callerCollectorPlayer.callers.get(0).getPackageName())
|
||||||
|
.isEqualTo("androidx.media3.test.session");
|
||||||
|
assertThat(callerCollectorPlayer.callers.get(0).getConnectionHints().size()).isEqualTo(1);
|
||||||
|
assertThat(
|
||||||
|
callerCollectorPlayer
|
||||||
|
.callers
|
||||||
|
.get(0)
|
||||||
|
.getConnectionHints()
|
||||||
|
.getBoolean(
|
||||||
|
MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER,
|
||||||
|
/* defaultValue= */ false))
|
||||||
|
.isTrue();
|
||||||
|
threadTestRule.getHandler().postAndSync(controller::release);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
fastForwardKeyEvent_mediaNotificationControllerNotConnected_callFromLegacyFallbackController()
|
||||||
|
throws Exception {
|
||||||
|
Assume.assumeTrue(Util.SDK_INT >= 21); // TODO: b/199064299 - Lower minSdk to 19.
|
||||||
|
|
||||||
|
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, false);
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS);
|
||||||
|
List<ControllerInfo> controllers = callerCollectorPlayer.callers;
|
||||||
|
assertThat(controllers).hasSize(1);
|
||||||
|
assertThat(controllers.get(0).getControllerVersion()).isEqualTo(LEGACY_CONTROLLER_VERSION);
|
||||||
|
assertThat(controllers.get(0).getConnectionHints().size()).isEqualTo(0);
|
||||||
|
assertThat(controllers.get(0).getPackageName())
|
||||||
|
.isEqualTo(getExpectedControllerPackageName(controllers.get(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rewindKeyEvent_mediaNotificationControllerConnected_callFromNotificationController()
|
||||||
|
throws Exception {
|
||||||
|
Assume.assumeTrue(Util.SDK_INT >= 21); // TODO: b/199064299 - Lower minSdk to 19.
|
||||||
|
MediaController controller = connectMediaNotificationController();
|
||||||
|
|
||||||
|
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND, false);
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_BACK, TIMEOUT_MS);
|
||||||
|
List<ControllerInfo> controllers = callerCollectorPlayer.callers;
|
||||||
|
assertThat(controllers).hasSize(1);
|
||||||
|
assertThat(controllers.get(0).getPackageName()).isEqualTo("androidx.media3.test.session");
|
||||||
|
assertThat(controllers.get(0).getControllerVersion()).isNotEqualTo(LEGACY_CONTROLLER_VERSION);
|
||||||
|
assertThat(controllers.get(0).getConnectionHints().size()).isEqualTo(1);
|
||||||
|
assertThat(
|
||||||
|
controllers
|
||||||
|
.get(0)
|
||||||
|
.getConnectionHints()
|
||||||
|
.getBoolean(
|
||||||
|
MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER,
|
||||||
|
/* defaultValue= */ false))
|
||||||
|
.isTrue();
|
||||||
|
threadTestRule.getHandler().postAndSync(controller::release);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
rewindKeyEvent_mediaNotificationControllerNotConnected_callFromLegacyFallbackController()
|
||||||
|
throws Exception {
|
||||||
|
Assume.assumeTrue(Util.SDK_INT >= 21); // TODO: b/199064299 - Lower minSdk to 19.
|
||||||
|
|
||||||
|
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND, false);
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_BACK, TIMEOUT_MS);
|
||||||
|
List<ControllerInfo> controllers = callerCollectorPlayer.callers;
|
||||||
|
assertThat(controllers).hasSize(1);
|
||||||
|
assertThat(controllers.get(0).getControllerVersion()).isEqualTo(LEGACY_CONTROLLER_VERSION);
|
||||||
|
assertThat(controllers.get(0).getConnectionHints().size()).isEqualTo(0);
|
||||||
|
assertThat(controllers.get(0).getPackageName())
|
||||||
|
.isEqualTo(getExpectedControllerPackageName(controllers.get(0)));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void stopKeyEvent() throws Exception {
|
public void stopKeyEvent() throws Exception {
|
||||||
Assume.assumeTrue(Util.SDK_INT >= 21); // TODO: b/199064299 - Lower minSdk to 19.
|
Assume.assumeTrue(Util.SDK_INT >= 21); // TODO: b/199064299 - Lower minSdk to 19.
|
||||||
|
|
@ -210,7 +307,7 @@ public class MediaSessionKeyEventTest {
|
||||||
handler.postAndSync(
|
handler.postAndSync(
|
||||||
() -> {
|
() -> {
|
||||||
player.playWhenReady = true;
|
player.playWhenReady = true;
|
||||||
player.playbackState = Player.STATE_ENDED;
|
player.playbackState = STATE_ENDED;
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false);
|
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false);
|
||||||
|
|
@ -233,6 +330,36 @@ public class MediaSessionKeyEventTest {
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playPauseKeyEvent_doubleTapOnPlayPause_seekNext() throws Exception {
|
||||||
|
Assume.assumeTrue(Util.SDK_INT >= 21); // TODO: b/199064299 - Lower minSdk to 19.
|
||||||
|
handler.postAndSync(
|
||||||
|
() -> {
|
||||||
|
player.playWhenReady = true;
|
||||||
|
player.playbackState = Player.STATE_READY;
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, /* doubleTap= */ true);
|
||||||
|
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaController connectMediaNotificationController() throws Exception {
|
||||||
|
return threadTestRule
|
||||||
|
.getHandler()
|
||||||
|
.postAndSync(
|
||||||
|
() -> {
|
||||||
|
Bundle connectionHints = new Bundle();
|
||||||
|
connectionHints.putBoolean(
|
||||||
|
MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, /* value= */ true);
|
||||||
|
return new MediaController.Builder(
|
||||||
|
ApplicationProvider.getApplicationContext(), session.getToken())
|
||||||
|
.setConnectionHints(connectionHints)
|
||||||
|
.buildAsync()
|
||||||
|
.get();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void dispatchMediaKeyEvent(int keyCode, boolean doubleTap) {
|
private void dispatchMediaKeyEvent(int keyCode, boolean doubleTap) {
|
||||||
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||||
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
||||||
|
|
@ -242,30 +369,56 @@ public class MediaSessionKeyEventTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestSessionCallback implements MediaSession.Callback {
|
private static String getExpectedControllerPackageName(ControllerInfo controllerInfo) {
|
||||||
|
if (controllerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION) {
|
||||||
|
return SUPPORT_APP_PACKAGE_NAME;
|
||||||
|
}
|
||||||
|
// Legacy controllers
|
||||||
|
if (Util.SDK_INT < 21 || Util.SDK_INT >= 28) {
|
||||||
|
// Above API 28: package of the app using AudioManager.
|
||||||
|
// Below 21: package of the owner of the session. Note: This is specific to this test setup
|
||||||
|
// where `ApplicationProvider.getContext().packageName == SUPPORT_APP_PACKAGE_NAME`.
|
||||||
|
return SUPPORT_APP_PACKAGE_NAME;
|
||||||
|
} else if (Util.SDK_INT >= 24) {
|
||||||
|
// API 24 - 27: KeyEvent from system service has the package name "android".
|
||||||
|
return "android";
|
||||||
|
} else {
|
||||||
|
// API 21 - 23: Fallback set by MediaSessionCompat#getCurrentControllerInfo
|
||||||
|
return LEGACY_CONTROLLER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final String EXPECTED_CONTROLLER_PACKAGE_NAME =
|
private static class TestSessionCallback implements MediaSession.Callback {
|
||||||
getExpectedControllerPackageName();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaSession.ConnectionResult onConnect(
|
public MediaSession.ConnectionResult onConnect(
|
||||||
MediaSession session, ControllerInfo controller) {
|
MediaSession session, ControllerInfo controller) {
|
||||||
if (EXPECTED_CONTROLLER_PACKAGE_NAME.equals(controller.getPackageName())) {
|
if (session.isMediaNotificationController(controller)
|
||||||
|
|| getExpectedControllerPackageName(controller).equals(controller.getPackageName())) {
|
||||||
return MediaSession.Callback.super.onConnect(session, controller);
|
return MediaSession.Callback.super.onConnect(session, controller);
|
||||||
}
|
}
|
||||||
return MediaSession.ConnectionResult.reject();
|
return MediaSession.ConnectionResult.reject();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String getExpectedControllerPackageName() {
|
private class CallerCollectorPlayer extends ForwardingPlayer {
|
||||||
if (Util.SDK_INT >= 28 || Util.SDK_INT < 21) {
|
private final List<ControllerInfo> callers;
|
||||||
return SUPPORT_APP_PACKAGE_NAME;
|
|
||||||
} else if (Util.SDK_INT >= 24) {
|
public CallerCollectorPlayer(Player player) {
|
||||||
// KeyEvent from system service has the package name "android".
|
super(player);
|
||||||
return "android";
|
callers = new ArrayList<>();
|
||||||
} else {
|
}
|
||||||
// In API 21+, MediaSessionCompat#getCurrentControllerInfo always returns fake info.
|
|
||||||
return LEGACY_CONTROLLER;
|
@Override
|
||||||
}
|
public void seekForward() {
|
||||||
|
callers.add(session.getControllerForCurrentRequest());
|
||||||
|
super.seekForward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void seekBack() {
|
||||||
|
callers.add(session.getControllerForCurrentRequest());
|
||||||
|
super.seekBack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import static android.view.KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
|
||||||
import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT;
|
import static android.view.KeyEvent.KEYCODE_MEDIA_NEXT;
|
||||||
import static android.view.KeyEvent.KEYCODE_MEDIA_PAUSE;
|
import static android.view.KeyEvent.KEYCODE_MEDIA_PAUSE;
|
||||||
import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY;
|
import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY;
|
||||||
|
import static android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
|
||||||
import static android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS;
|
import static android.view.KeyEvent.KEYCODE_MEDIA_PREVIOUS;
|
||||||
import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND;
|
import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND;
|
||||||
import static android.view.KeyEvent.KEYCODE_MEDIA_STOP;
|
import static android.view.KeyEvent.KEYCODE_MEDIA_STOP;
|
||||||
|
|
@ -513,7 +514,7 @@ public class MediaSessionTest {
|
||||||
session.set(
|
session.set(
|
||||||
sessionTestRule.ensureReleaseAfterTest(
|
sessionTestRule.ensureReleaseAfterTest(
|
||||||
new MediaSession.Builder(context, callerCollectorPlayer)
|
new MediaSession.Builder(context, callerCollectorPlayer)
|
||||||
.setId("getSessionCompatToken_returnsCompatibleWithMediaControllerCompat")
|
.setId("onMediaButtonEvent")
|
||||||
.setCallback(
|
.setCallback(
|
||||||
new MediaSession.Callback() {
|
new MediaSession.Callback() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -535,14 +536,41 @@ public class MediaSessionTest {
|
||||||
.buildAsync()
|
.buildAsync()
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
MediaSessionImpl impl = session.get().getImpl();
|
threadTestRule
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PLAY))).isTrue();
|
.getHandler()
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PAUSE))).isTrue();
|
.postAndSync(
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_FAST_FORWARD))).isTrue();
|
() -> {
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_REWIND))).isTrue();
|
MediaSessionImpl impl = session.get().getImpl();
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_NEXT))).isTrue();
|
ControllerInfo controllerInfo = createMediaButtonCaller();
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PREVIOUS))).isTrue();
|
assertThat(
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_STOP))).isTrue();
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PLAY)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PAUSE)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_FAST_FORWARD)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_REWIND)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_NEXT)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PREVIOUS)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_STOP)))
|
||||||
|
.isTrue();
|
||||||
|
});
|
||||||
|
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
|
||||||
|
|
@ -566,7 +594,7 @@ public class MediaSessionTest {
|
||||||
session.set(
|
session.set(
|
||||||
sessionTestRule.ensureReleaseAfterTest(
|
sessionTestRule.ensureReleaseAfterTest(
|
||||||
new MediaSession.Builder(context, callerCollectorPlayer)
|
new MediaSession.Builder(context, callerCollectorPlayer)
|
||||||
.setId("getSessionCompatToken_returnsCompatibleWithMediaControllerCompat")
|
.setId("onMediaButtonEvent")
|
||||||
.setCallback(
|
.setCallback(
|
||||||
new MediaSession.Callback() {
|
new MediaSession.Callback() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -583,19 +611,46 @@ public class MediaSessionTest {
|
||||||
.build()));
|
.build()));
|
||||||
MediaSessionImpl impl = session.get().getImpl();
|
MediaSessionImpl impl = session.get().getImpl();
|
||||||
|
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PLAY))).isTrue();
|
threadTestRule
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PAUSE))).isTrue();
|
.getHandler()
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_FAST_FORWARD))).isTrue();
|
.postAndSync(
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_REWIND))).isTrue();
|
() -> {
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_NEXT))).isTrue();
|
ControllerInfo controllerInfo = createMediaButtonCaller();
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PREVIOUS))).isTrue();
|
assertThat(
|
||||||
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_STOP))).isTrue();
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PLAY)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PAUSE)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_FAST_FORWARD)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_REWIND)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_NEXT)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PREVIOUS)))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
impl.onMediaButtonEvent(
|
||||||
|
controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_STOP)))
|
||||||
|
.isTrue();
|
||||||
|
});
|
||||||
|
|
||||||
// Fallback code path through platform session when MediaSessionImpl doesn't handle the event.
|
// Fallback through the framework session when media notification controller in disabled.
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS);
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS);
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS);
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_BACK, TIMEOUT_MS);
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS);
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS);
|
||||||
player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS);
|
player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS);
|
||||||
|
|
@ -609,12 +664,113 @@ public class MediaSessionTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
onMediaButtonEvent_appOverridesCallback_notificationControllerNotConnected_callsWhatAppCalls()
|
||||||
|
throws Exception {
|
||||||
|
List<ControllerInfo> controllers = new ArrayList<>();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
MediaSession session =
|
||||||
|
sessionTestRule.ensureReleaseAfterTest(
|
||||||
|
new MediaSession.Builder(context, player)
|
||||||
|
.setId("onMediaButtonEvent")
|
||||||
|
.setCallback(
|
||||||
|
new MediaSession.Callback() {
|
||||||
|
@Override
|
||||||
|
public MediaSession.ConnectionResult onConnect(
|
||||||
|
MediaSession session, ControllerInfo controller) {
|
||||||
|
if (TextUtils.equals(
|
||||||
|
getControllerCallerPackageName(controller),
|
||||||
|
controller.getPackageName())) {
|
||||||
|
return MediaSession.Callback.super.onConnect(session, controller);
|
||||||
|
}
|
||||||
|
return MediaSession.ConnectionResult.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMediaButtonEvent(
|
||||||
|
MediaSession session, ControllerInfo controllerInfo, Intent intent) {
|
||||||
|
session.getPlayer().seekToNext();
|
||||||
|
controllers.add(controllerInfo);
|
||||||
|
latch.countDown();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build());
|
||||||
|
MediaSessionImpl impl = session.getImpl();
|
||||||
|
|
||||||
|
ControllerInfo controllerInfo = createMediaButtonCaller();
|
||||||
|
threadTestRule
|
||||||
|
.getHandler()
|
||||||
|
.postAndSync(
|
||||||
|
() -> {
|
||||||
|
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY_PAUSE);
|
||||||
|
assertThat(impl.onMediaButtonEvent(controllerInfo, intent)).isTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS);
|
||||||
|
assertThat(controllers).hasSize(1);
|
||||||
|
assertThat(session.isMediaNotificationController(controllers.get(0))).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
onMediaButtonEvent_appOverridesCallback_notificationControllerConnected_callsWhatAppCalls()
|
||||||
|
throws Exception {
|
||||||
|
List<ControllerInfo> controllers = new ArrayList<>();
|
||||||
|
MediaSession session =
|
||||||
|
sessionTestRule.ensureReleaseAfterTest(
|
||||||
|
new MediaSession.Builder(context, player)
|
||||||
|
.setId("onMediaButtonEvent")
|
||||||
|
.setCallback(
|
||||||
|
new MediaSession.Callback() {
|
||||||
|
@Override
|
||||||
|
public boolean onMediaButtonEvent(
|
||||||
|
MediaSession session, ControllerInfo controllerInfo, Intent intent) {
|
||||||
|
if (DefaultActionFactory.getKeyEvent(intent).getKeyCode()
|
||||||
|
== KEYCODE_MEDIA_PLAY) {
|
||||||
|
player.seekForward();
|
||||||
|
controllers.add(controllerInfo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return MediaSession.Callback.super.onMediaButtonEvent(
|
||||||
|
session, controllerInfo, intent);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build());
|
||||||
|
Bundle connectionHints = new Bundle();
|
||||||
|
connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true);
|
||||||
|
new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken())
|
||||||
|
.setConnectionHints(connectionHints)
|
||||||
|
.buildAsync()
|
||||||
|
.get();
|
||||||
|
|
||||||
|
boolean isEventHandled =
|
||||||
|
threadTestRule
|
||||||
|
.getHandler()
|
||||||
|
.postAndSync(
|
||||||
|
() ->
|
||||||
|
session
|
||||||
|
.getImpl()
|
||||||
|
.onMediaButtonEvent(
|
||||||
|
session.getMediaNotificationControllerInfo(),
|
||||||
|
getMediaButtonIntent(KEYCODE_MEDIA_PLAY)));
|
||||||
|
|
||||||
|
assertThat(isEventHandled).isTrue();
|
||||||
|
// App changed default behaviour
|
||||||
|
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_FORWARD, TIMEOUT_MS);
|
||||||
|
assertThat(controllers).hasSize(1);
|
||||||
|
assertThat(session.isMediaNotificationController(controllers.get(0))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void onMediaButtonEvent_noKeyEvent_returnsFalse() {
|
public void onMediaButtonEvent_noKeyEvent_returnsFalse() {
|
||||||
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
||||||
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
|
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
|
||||||
|
|
||||||
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
|
boolean isEventHandled =
|
||||||
|
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
|
||||||
|
|
||||||
assertThat(isEventHandled).isFalse();
|
assertThat(isEventHandled).isFalse();
|
||||||
}
|
}
|
||||||
|
|
@ -631,7 +787,8 @@ public class MediaSessionTest {
|
||||||
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
||||||
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
|
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
|
||||||
|
|
||||||
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
|
boolean isEventHandled =
|
||||||
|
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
|
||||||
|
|
||||||
assertThat(isEventHandled).isFalse();
|
assertThat(isEventHandled).isFalse();
|
||||||
}
|
}
|
||||||
|
|
@ -642,7 +799,8 @@ public class MediaSessionTest {
|
||||||
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
|
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
|
||||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KEYCODE_MEDIA_PAUSE));
|
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KEYCODE_MEDIA_PAUSE));
|
||||||
|
|
||||||
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
|
boolean isEventHandled =
|
||||||
|
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
|
||||||
|
|
||||||
assertThat(isEventHandled).isFalse();
|
assertThat(isEventHandled).isFalse();
|
||||||
}
|
}
|
||||||
|
|
@ -660,7 +818,8 @@ public class MediaSessionTest {
|
||||||
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
|
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
|
||||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KEYCODE_MEDIA_PAUSE));
|
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KEYCODE_MEDIA_PAUSE));
|
||||||
|
|
||||||
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
|
boolean isEventHandled =
|
||||||
|
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
|
||||||
|
|
||||||
assertThat(isEventHandled).isFalse();
|
assertThat(isEventHandled).isFalse();
|
||||||
}
|
}
|
||||||
|
|
@ -670,7 +829,8 @@ public class MediaSessionTest {
|
||||||
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
||||||
intent.setAction("notAMediaButtonAction");
|
intent.setAction("notAMediaButtonAction");
|
||||||
|
|
||||||
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
|
boolean isEventHandled =
|
||||||
|
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
|
||||||
|
|
||||||
assertThat(isEventHandled).isFalse();
|
assertThat(isEventHandled).isFalse();
|
||||||
}
|
}
|
||||||
|
|
@ -687,7 +847,8 @@ public class MediaSessionTest {
|
||||||
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
||||||
intent.setAction("notAMediaButtonAction");
|
intent.setAction("notAMediaButtonAction");
|
||||||
|
|
||||||
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
|
boolean isEventHandled =
|
||||||
|
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
|
||||||
|
|
||||||
assertThat(isEventHandled).isFalse();
|
assertThat(isEventHandled).isFalse();
|
||||||
}
|
}
|
||||||
|
|
@ -697,7 +858,8 @@ public class MediaSessionTest {
|
||||||
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
||||||
intent.setComponent(new ComponentName("a.package", "a.class"));
|
intent.setComponent(new ComponentName("a.package", "a.class"));
|
||||||
|
|
||||||
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
|
boolean isEventHandled =
|
||||||
|
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
|
||||||
|
|
||||||
assertThat(isEventHandled).isFalse();
|
assertThat(isEventHandled).isFalse();
|
||||||
}
|
}
|
||||||
|
|
@ -715,7 +877,8 @@ public class MediaSessionTest {
|
||||||
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
|
||||||
intent.setComponent(new ComponentName("a.package", "a.class"));
|
intent.setComponent(new ComponentName("a.package", "a.class"));
|
||||||
|
|
||||||
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
|
boolean isEventHandled =
|
||||||
|
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
|
||||||
|
|
||||||
assertThat(isEventHandled).isFalse();
|
assertThat(isEventHandled).isFalse();
|
||||||
}
|
}
|
||||||
|
|
@ -750,6 +913,19 @@ public class MediaSessionTest {
|
||||||
: MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER;
|
: MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ControllerInfo createMediaButtonCaller() {
|
||||||
|
return new ControllerInfo(
|
||||||
|
new MediaSessionManager.RemoteUserInfo(
|
||||||
|
"RANDOM_MEDIA_BUTTON_CALLER_PACKAGE",
|
||||||
|
MediaSessionManager.RemoteUserInfo.UNKNOWN_PID,
|
||||||
|
MediaSessionManager.RemoteUserInfo.UNKNOWN_UID),
|
||||||
|
MediaLibraryInfo.VERSION_INT,
|
||||||
|
MediaControllerStub.VERSION_INT,
|
||||||
|
/* trusted= */ false,
|
||||||
|
/* cb= */ null,
|
||||||
|
/* connectionHints= */ Bundle.EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
private static class CallerCollectorPlayer extends ForwardingPlayer {
|
private static class CallerCollectorPlayer extends ForwardingPlayer {
|
||||||
private final List<ControllerInfo> callingControllers;
|
private final List<ControllerInfo> callingControllers;
|
||||||
private final AtomicReference<MediaSession> session;
|
private final AtomicReference<MediaSession> session;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue