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
(cherry picked from commit a79d44edc5c7fdc81120dbc9b2c89b9799b14031)
This commit is contained in:
bachinger 2023-10-20 08:52:55 -07:00 committed by Rohit Singh
parent 47a451abf7
commit f2cf43ccd5
8 changed files with 748 additions and 179 deletions

View File

@ -37,6 +37,8 @@
(([#339](https://github.com/androidx/media/issues/339)).
* Use `DataSourceBitmapLoader` by default instead of `SimpleBitmapLoader`
([#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:
* Downloads:
* OkHttp Extension:

View File

@ -24,6 +24,7 @@ import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
@ -1444,6 +1445,32 @@ public class MediaSession {
MediaSession mediaSession, ControllerInfo controller) {
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. */

View File

@ -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.util.Assertions.checkNotNull;
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.session.MediaSessionStub.UNKNOWN_SEQUENCE_NUMBER;
import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED;
@ -52,6 +51,7 @@ import android.os.RemoteException;
import android.os.SystemClock;
import android.support.v4.media.session.MediaSessionCompat;
import android.view.KeyEvent;
import android.view.ViewConfiguration;
import androidx.annotation.CheckResult;
import androidx.annotation.FloatRange;
import androidx.annotation.GuardedBy;
@ -116,6 +116,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Uri sessionUri;
private final PlayerInfoChangedHandler onPlayerInfoChangedHandler;
private final MediaPlayPauseKeyHandler mediaPlayPauseKeyHandler;
private final MediaSession.Callback callback;
private final Context context;
private final MediaSessionStub sessionStub;
@ -161,28 +162,30 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
BitmapLoader bitmapLoader,
boolean playIfSuppressed,
boolean isPeriodicPositionUpdateEnabled) {
this.context = context;
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")
@Initialized
MediaSessionImpl thisRef = this;
sessionStub = new MediaSessionStub(thisRef);
this.sessionActivity = sessionActivity;
this.customLayout = customLayout;
mainHandler = new Handler(Looper.getMainLooper());
applicationHandler = new Handler(player.getApplicationLooper());
this.callback = callback;
this.bitmapLoader = bitmapLoader;
this.playIfSuppressed = playIfSuppressed;
this.isPeriodicPositionUpdateEnabled = isPeriodicPositionUpdateEnabled;
Looper applicationLooper = player.getApplicationLooper();
applicationHandler = new Handler(applicationLooper);
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.
// Here's the reason why Session ID / SessionToken aren't suitable here.
// - Session ID
@ -280,6 +283,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
closed = true;
}
mediaPlayPauseKeyHandler.clearPendingPlayPauseTask();
applicationHandler.removeCallbacksAndMessages(null);
try {
postOrRun(
@ -1080,7 +1084,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
(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);
ComponentName intentComponent = intent.getComponent();
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) {
return false;
}
ControllerInfo controllerInfo = getMediaNotificationControllerInfo();
if (controllerInfo == null) {
if (intentComponent != null) {
// Fallback to legacy if this is a media button event sent to one of our components.
return getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent)
|| SDK_INT < 21;
}
return false;
verifyApplicationThread();
if (callback.onMediaButtonEvent(instance, callerInfo, intent)) {
// Event handled by app callback.
return true;
}
// Double tap detection.
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;
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:
command =
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 static final int MSG_PLAYER_INFO_CHANGED = 1;

View File

@ -65,7 +65,6 @@ import android.support.v4.media.session.MediaSessionCompat.QueueItem;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.ViewConfiguration;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@ -126,9 +125,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
private final MediaSessionManager sessionManager;
private final ControllerLegacyCbForBroadcast controllerLegacyCbForBroadcast;
private final ConnectionTimeoutHandler connectionTimeoutHandler;
private final MediaPlayPauseKeyHandler mediaPlayPauseKeyHandler;
private final MediaSessionCompat sessionCompat;
private final String appPackageName;
@Nullable private final MediaButtonReceiver runtimeBroadcastReceiver;
@Nullable private final ComponentName broadcastReceiverComponentName;
@Nullable private VolumeProviderCompat volumeProviderCompat;
@ -141,11 +138,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
public MediaSessionLegacyStub(MediaSessionImpl session, Uri sessionUri, Handler handler) {
sessionImpl = session;
Context context = sessionImpl.getContext();
appPackageName = context.getPackageName();
sessionManager = MediaSessionManager.getSessionManager(context);
controllerLegacyCbForBroadcast = new ControllerLegacyCbForBroadcast();
mediaPlayPauseKeyHandler =
new MediaPlayPauseKeyHandler(session.getApplicationHandler().getLooper());
connectedControllersManager = new ConnectedControllersManager<>(session);
connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS;
connectionTimeoutHandler =
@ -318,41 +312,16 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
@Override
public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
@Nullable KeyEvent keyEvent = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
if (keyEvent == null || keyEvent.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
RemoteUserInfo remoteUserInfo = sessionCompat.getCurrentControllerInfo();
int keyCode = keyEvent.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
// 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;
public boolean onMediaButtonEvent(Intent intent) {
return sessionImpl.onMediaButtonEvent(
new ControllerInfo(
sessionCompat.getCurrentControllerInfo(),
ControllerInfo.LEGACY_CONTROLLER_VERSION,
ControllerInfo.LEGACY_CONTROLLER_INTERFACE_VERSION,
/* trusted= */ false,
/* cb= */ null,
/* connectionHints= */ Bundle.EMPTY),
intent);
}
private void maybeUpdateFlags(PlayerWrapper playerWrapper) {
@ -366,8 +335,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
}
private void handleMediaPlayPauseOnHandler(RemoteUserInfo remoteUserInfo) {
mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey();
/* package */ void handleMediaPlayPauseOnHandler(RemoteUserInfo remoteUserInfo) {
dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE,
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) {
return "Failed to load bitmap: " + throwable.getMessage();
}

View File

@ -22,6 +22,7 @@ import static androidx.media3.common.util.Util.postOrRun;
import android.app.ForegroundServiceStartNotAllowedException;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@ -39,6 +40,7 @@ import androidx.annotation.RequiresApi;
import androidx.collection.ArrayMap;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media.MediaSessionManager;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
@ -425,9 +427,19 @@ public abstract class MediaSessionService extends Service {
}
addSession(session);
}
if (!session.getImpl().onMediaButtonEvent(intent)) {
Log.w(TAG, "Ignoring unrecognized media button intent.");
}
MediaSessionImpl sessionImpl = session.getImpl();
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)) {
@Nullable String customAction = actionFactory.getCustomAction(intent);
if (customAction == null) {
@ -439,6 +451,24 @@ public abstract class MediaSessionService extends Service {
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.
*

View File

@ -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_IDLE;
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_SUCCESS;
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.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.ForwardingPlayer;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.Rating;
@ -71,7 +73,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
@ -93,6 +94,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
@Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG);
@Rule public final MediaSessionTestRule mediaSessionTestRule = new MediaSessionTestRule();
private Context context;
private TestHandler handler;
private MediaSession session;
@ -615,37 +618,47 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
}
@Test
public void dispatchMediaButtonEvent_playWithEmptyTimeline_callsPreparesPlayerCorrectly()
public void dispatchMediaButtonEvent_playWithEmptyTimeline_callsPlaybackResumptionPrepareAndPlay()
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();
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.getSessionCompat().getSessionToken(), /* waitForConnection= */ true);
context,
session.get().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);
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)).isFalse();
}
}
@Test
@ -739,7 +752,59 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
@Test
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 {
player.mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
player.startMediaItemIndex = 1;
@ -781,28 +846,20 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
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();
AtomicReference<MediaSession> session = new AtomicReference<>();
CallerCollectorPlayer callerCollectorPlayer = new CallerCollectorPlayer(session, player);
session.set(
mediaSessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, callerCollectorPlayer)
.setId("dispatchMediaButtonEvent")
.build()));
controller =
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);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
@ -811,6 +868,50 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
assertThat(player.startMediaItemIndex).isEqualTo(1);
assertThat(player.startPositionMs).isEqualTo(321L);
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
@ -1807,4 +1908,33 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
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();
}
}
}

View File

@ -16,6 +16,8 @@
package androidx.media3.session;
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.TestUtils.LONG_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.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.KeyEvent;
import androidx.media3.common.ForwardingPlayer;
import androidx.media3.common.Player;
import androidx.media3.common.util.Util;
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.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Assume;
@ -69,6 +75,7 @@ public class MediaSessionKeyEventTest {
private MediaSession session;
private MockPlayer player;
private TestSessionCallback sessionCallback;
private CallerCollectorPlayer callerCollectorPlayer;
@Before
public void setUp() throws Exception {
@ -78,10 +85,14 @@ public class MediaSessionKeyEventTest {
Context context = ApplicationProvider.getApplicationContext();
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
handler = threadTestRule.getHandler();
player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build();
player =
new MockPlayer.Builder().setMediaItems(1).setApplicationLooper(handler.getLooper()).build();
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.
// - 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);
}
@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
public void stopKeyEvent() throws Exception {
Assume.assumeTrue(Util.SDK_INT >= 21); // TODO: b/199064299 - Lower minSdk to 19.
@ -210,7 +307,7 @@ public class MediaSessionKeyEventTest {
handler.postAndSync(
() -> {
player.playWhenReady = true;
player.playbackState = Player.STATE_ENDED;
player.playbackState = STATE_ENDED;
});
dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false);
@ -233,6 +330,36 @@ public class MediaSessionKeyEventTest {
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) {
audioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, 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 =
getExpectedControllerPackageName();
private static class TestSessionCallback implements MediaSession.Callback {
@Override
public MediaSession.ConnectionResult onConnect(
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.ConnectionResult.reject();
}
}
private static String getExpectedControllerPackageName() {
if (Util.SDK_INT >= 28 || Util.SDK_INT < 21) {
return SUPPORT_APP_PACKAGE_NAME;
} else if (Util.SDK_INT >= 24) {
// KeyEvent from system service has the package name "android".
return "android";
} else {
// In API 21+, MediaSessionCompat#getCurrentControllerInfo always returns fake info.
return LEGACY_CONTROLLER;
}
private class CallerCollectorPlayer extends ForwardingPlayer {
private final List<ControllerInfo> callers;
public CallerCollectorPlayer(Player player) {
super(player);
callers = new ArrayList<>();
}
@Override
public void seekForward() {
callers.add(session.getControllerForCurrentRequest());
super.seekForward();
}
@Override
public void seekBack() {
callers.add(session.getControllerForCurrentRequest());
super.seekBack();
}
}
}

View File

@ -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_PAUSE;
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_REWIND;
import static android.view.KeyEvent.KEYCODE_MEDIA_STOP;
@ -513,7 +514,7 @@ public class MediaSessionTest {
session.set(
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, callerCollectorPlayer)
.setId("getSessionCompatToken_returnsCompatibleWithMediaControllerCompat")
.setId("onMediaButtonEvent")
.setCallback(
new MediaSession.Callback() {
@Override
@ -535,14 +536,41 @@ public class MediaSessionTest {
.buildAsync()
.get();
MediaSessionImpl impl = session.get().getImpl();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PLAY))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PAUSE))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_FAST_FORWARD))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_REWIND))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_NEXT))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PREVIOUS))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_STOP))).isTrue();
threadTestRule
.getHandler()
.postAndSync(
() -> {
MediaSessionImpl impl = session.get().getImpl();
ControllerInfo controllerInfo = createMediaButtonCaller();
assertThat(
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_PAUSE, TIMEOUT_MS);
@ -566,7 +594,7 @@ public class MediaSessionTest {
session.set(
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, callerCollectorPlayer)
.setId("getSessionCompatToken_returnsCompatibleWithMediaControllerCompat")
.setId("onMediaButtonEvent")
.setCallback(
new MediaSession.Callback() {
@Override
@ -583,19 +611,46 @@ public class MediaSessionTest {
.build()));
MediaSessionImpl impl = session.get().getImpl();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PLAY))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PAUSE))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_FAST_FORWARD))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_REWIND))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_NEXT))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_PREVIOUS))).isTrue();
assertThat(impl.onMediaButtonEvent(getMediaButtonIntent(KEYCODE_MEDIA_STOP))).isTrue();
threadTestRule
.getHandler()
.postAndSync(
() -> {
ControllerInfo controllerInfo = createMediaButtonCaller();
assertThat(
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_PAUSE, 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_PREVIOUS, 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
public void onMediaButtonEvent_noKeyEvent_returnsFalse() {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
boolean isEventHandled =
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
assertThat(isEventHandled).isFalse();
}
@ -631,7 +787,8 @@ public class MediaSessionTest {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
boolean isEventHandled =
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
assertThat(isEventHandled).isFalse();
}
@ -642,7 +799,8 @@ public class MediaSessionTest {
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
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();
}
@ -660,7 +818,8 @@ public class MediaSessionTest {
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
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();
}
@ -670,7 +829,8 @@ public class MediaSessionTest {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.setAction("notAMediaButtonAction");
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
boolean isEventHandled =
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
assertThat(isEventHandled).isFalse();
}
@ -687,7 +847,8 @@ public class MediaSessionTest {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.setAction("notAMediaButtonAction");
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
boolean isEventHandled =
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
assertThat(isEventHandled).isFalse();
}
@ -697,7 +858,8 @@ public class MediaSessionTest {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.setComponent(new ComponentName("a.package", "a.class"));
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
boolean isEventHandled =
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
assertThat(isEventHandled).isFalse();
}
@ -715,7 +877,8 @@ public class MediaSessionTest {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.setComponent(new ComponentName("a.package", "a.class"));
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
boolean isEventHandled =
session.getImpl().onMediaButtonEvent(createMediaButtonCaller(), intent);
assertThat(isEventHandled).isFalse();
}
@ -750,6 +913,19 @@ public class MediaSessionTest {
: 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 final List<ControllerInfo> callingControllers;
private final AtomicReference<MediaSession> session;