PiperOrigin-RevId: 574290408
This commit is contained in:
Googler 2023-10-17 15:58:28 -07:00 committed by Copybara-Service
parent 61770f8a61
commit 1a43aa3602
8 changed files with 249 additions and 690 deletions

View File

@ -56,19 +56,6 @@ import androidx.media3.common.util.Util;
public static final String EXTRAS_KEY_ACTION_CUSTOM_EXTRAS =
"androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION_EXTRAS";
/**
* Returns the {@link KeyEvent} that was included in the media action, or {@code null} if no
* {@link KeyEvent} is found in the {@code intent}.
*/
@Nullable
public static KeyEvent getKeyEvent(Intent intent) {
@Nullable Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(Intent.EXTRA_KEY_EVENT)) {
return extras.getParcelable(Intent.EXTRA_KEY_EVENT);
}
return null;
}
private final Service service;
private int customActionPendingIntentRequestCode = 0;
@ -110,7 +97,6 @@ import androidx.media3.common.util.Util;
mediaSession, customCommand.customAction, customCommand.customExtras));
}
@SuppressWarnings("PendingIntentMutability") // We can't use SaferPendingIntent
@Override
public PendingIntent createMediaActionPendingIntent(
MediaSession mediaSession, @Player.Command long command) {
@ -150,7 +136,6 @@ import androidx.media3.common.util.Util;
return KEYCODE_UNKNOWN;
}
@SuppressWarnings("PendingIntentMutability") // We can't use SaferPendingIntent
private PendingIntent createCustomActionPendingIntent(
MediaSession mediaSession, String action, Bundle extras) {
Intent intent = new Intent(ACTION_CUSTOM);
@ -177,6 +162,19 @@ import androidx.media3.common.util.Util;
return ACTION_CUSTOM.equals(intent.getAction());
}
/**
* Returns the {@link KeyEvent} that was included in the media action, or {@code null} if no
* {@link KeyEvent} is found in the {@code intent}.
*/
@Nullable
public KeyEvent getKeyEvent(Intent intent) {
@Nullable Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(Intent.EXTRA_KEY_EVENT)) {
return extras.getParcelable(Intent.EXTRA_KEY_EVENT);
}
return null;
}
/**
* Returns the custom action that was included in the {@link #createCustomAction custom action},
* or {@code null} if no custom action is found in the {@code intent}.
@ -203,7 +201,6 @@ import androidx.media3.common.util.Util;
private static final class Api26 {
private Api26() {}
@SuppressWarnings("PendingIntentMutability") // We can't use SaferPendingIntent
public static PendingIntent createForegroundServicePendingIntent(
Service service, int keyCode, Intent intent) {
return PendingIntent.getForegroundService(

View File

@ -17,6 +17,14 @@ package androidx.media3.session;
import static android.app.Service.STOP_FOREGROUND_DETACH;
import static android.app.Service.STOP_FOREGROUND_REMOVE;
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;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.annotation.SuppressLint;
@ -26,6 +34,7 @@ import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.KeyEvent;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@ -65,7 +74,7 @@ import java.util.concurrent.TimeoutException;
private final NotificationManagerCompat notificationManagerCompat;
private final Executor mainExecutor;
private final Intent startSelfIntent;
private final Map<MediaSession, ListenableFuture<MediaController>> controllerMap;
private final Map<MediaSession, ControllerAndListener> controllerAndListenerMap;
private int totalNotificationCount;
@Nullable private MediaNotification mediaNotification;
@ -82,30 +91,34 @@ import java.util.concurrent.TimeoutException;
Handler mainHandler = new Handler(Looper.getMainLooper());
mainExecutor = (runnable) -> Util.postOrRun(mainHandler, runnable);
startSelfIntent = new Intent(mediaSessionService, mediaSessionService.getClass());
controllerMap = new HashMap<>();
controllerAndListenerMap = new HashMap<>();
startedInForeground = false;
}
public void addSession(MediaSession session) {
if (controllerMap.containsKey(session)) {
if (controllerAndListenerMap.containsKey(session)) {
return;
}
MediaControllerListener listener = new MediaControllerListener(mediaSessionService, session);
MediaControllerListener controllerListener =
new MediaControllerListener(mediaSessionService, session);
PlayerListener playerListener = new PlayerListener(mediaSessionService, session);
Bundle connectionHints = new Bundle();
connectionHints.putBoolean(KEY_MEDIA_NOTIFICATION_MANAGER, true);
ListenableFuture<MediaController> controllerFuture =
new MediaController.Builder(mediaSessionService, session.getToken())
.setConnectionHints(connectionHints)
.setListener(listener)
.setListener(controllerListener)
.setApplicationLooper(Looper.getMainLooper())
.buildAsync();
controllerMap.put(session, controllerFuture);
controllerAndListenerMap.put(
session, new ControllerAndListener(controllerFuture, playerListener));
controllerFuture.addListener(
() -> {
try {
MediaController controller = controllerFuture.get(/* time= */ 0, MILLISECONDS);
listener.onConnected(shouldShowNotification(session));
controller.addListener(listener);
// Assert connection success.
controllerFuture.get(/* time= */ 0, MILLISECONDS);
controllerListener.onConnected(shouldShowNotification(session));
session.getImpl().addPlayerListener(playerListener);
} catch (CancellationException
| ExecutionException
| InterruptedException
@ -118,9 +131,52 @@ import java.util.concurrent.TimeoutException;
}
public void removeSession(MediaSession session) {
@Nullable ListenableFuture<MediaController> future = controllerMap.remove(session);
if (future != null) {
MediaController.releaseFuture(future);
ControllerAndListener controllerAndListener = controllerAndListenerMap.remove(session);
if (controllerAndListener != null) {
session.getImpl().removePlayerListener(controllerAndListener.listener);
MediaController.releaseFuture(controllerAndListener.controller);
}
}
public void onMediaButtonEvent(MediaSession session, KeyEvent keyEvent) {
int keyCode = keyEvent.getKeyCode();
@Nullable MediaController mediaController = getConnectedControllerForSession(session);
if (mediaController == null) {
session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
return;
}
switch (keyCode) {
case KEYCODE_MEDIA_PLAY_PAUSE:
if (mediaController.getPlayWhenReady()) {
mediaController.pause();
} else {
mediaController.play();
}
break;
case KEYCODE_MEDIA_PLAY:
mediaController.play();
break;
case KEYCODE_MEDIA_PAUSE:
mediaController.pause();
break;
case KEYCODE_MEDIA_NEXT:
mediaController.seekToNext();
break;
case KEYCODE_MEDIA_PREVIOUS:
mediaController.seekToPrevious();
break;
case KEYCODE_MEDIA_FAST_FORWARD:
mediaController.seekForward();
break;
case KEYCODE_MEDIA_REWIND:
mediaController.seekBack();
break;
case KEYCODE_MEDIA_STOP:
mediaController.stop();
break;
default:
Log.w(TAG, "Received media button event with unsupported key code: " + keyCode);
break;
}
}
@ -154,11 +210,11 @@ import java.util.concurrent.TimeoutException;
int notificationSequence = ++totalNotificationCount;
MediaController mediaNotificationController = null;
ListenableFuture<MediaController> controller = controllerMap.get(session);
if (controller != null && controller.isDone()) {
ControllerAndListener controllerAndListener = controllerAndListenerMap.get(session);
if (controllerAndListener != null && controllerAndListener.controller.isDone()) {
try {
mediaNotificationController = Futures.getDone(controller);
} catch (ExecutionException e) {
mediaNotificationController = Futures.getDone(controllerAndListener.controller);
} catch (CancellationException | ExecutionException e) {
// Ignore.
}
}
@ -261,13 +317,13 @@ import java.util.concurrent.TimeoutException;
@Nullable
private MediaController getConnectedControllerForSession(MediaSession session) {
ListenableFuture<MediaController> controller = controllerMap.get(session);
if (controller == null) {
ControllerAndListener controllerAndListener = controllerAndListenerMap.get(session);
if (controllerAndListener == null) {
return null;
}
try {
return Futures.getDone(controller);
} catch (ExecutionException exception) {
return Futures.getDone(controllerAndListener.controller);
} catch (CancellationException | ExecutionException exception) {
// We should never reach this.
throw new IllegalStateException(exception);
}
@ -305,8 +361,7 @@ import java.util.concurrent.TimeoutException;
}
}
private static final class MediaControllerListener
implements MediaController.Listener, Player.Listener {
private static final class MediaControllerListener implements MediaController.Listener {
private final MediaSessionService mediaSessionService;
private final MediaSession session;
@ -344,6 +399,18 @@ import java.util.concurrent.TimeoutException;
mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
}
}
private static class PlayerListener implements Player.Listener {
private final MediaSessionService mediaSessionService;
private final MediaSession session;
private final Handler mainHandler;
public PlayerListener(MediaSessionService mediaSessionService, MediaSession session) {
this.mediaSessionService = mediaSessionService;
this.session = session;
mainHandler = new Handler(Looper.getMainLooper());
}
@Override
public void onEvents(Player player, Player.Events events) {
@ -354,8 +421,13 @@ import java.util.concurrent.TimeoutException;
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_MEDIA_METADATA_CHANGED,
Player.EVENT_TIMELINE_CHANGED)) {
mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
// onUpdateNotificationInternal is required to be called on the main thread and the
// application thread of the player may be a different thread.
Util.postOrRun(
mainHandler,
() ->
mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false));
}
}
}
@ -385,6 +457,17 @@ import java.util.concurrent.TimeoutException;
startedInForeground = false;
}
private static class ControllerAndListener {
public final ListenableFuture<MediaController> controller;
public final Player.Listener listener;
private ControllerAndListener(
ListenableFuture<MediaController> controller, Player.Listener listener) {
this.controller = controller;
this.listener = listener;
}
}
@RequiresApi(24)
private static class Api24 {

View File

@ -15,28 +15,15 @@
*/
package androidx.media3.session;
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_SKIP_BACKWARD;
import static android.view.KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD;
import static android.view.KeyEvent.KEYCODE_MEDIA_STOP;
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;
import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import static java.lang.Math.min;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@ -50,7 +37,6 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.v4.media.session.MediaSessionCompat;
import android.view.KeyEvent;
import androidx.annotation.CheckResult;
import androidx.annotation.FloatRange;
import androidx.annotation.GuardedBy;
@ -148,6 +134,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private boolean closed;
// Should be only accessed on the application looper
private final List<Player.Listener> wrapperListeners;
private long sessionPositionUpdateDelayMs;
private boolean isMediaNotificationControllerConnected;
private ImmutableList<CommandButton> customLayout;
@ -174,6 +161,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sessionStub = new MediaSessionStub(thisRef);
this.sessionActivity = sessionActivity;
this.customLayout = customLayout;
wrapperListeners = new ArrayList<>();
mainHandler = new Handler(Looper.getMainLooper());
applicationHandler = new Handler(player.getApplicationLooper());
@ -252,14 +240,38 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
playerWrapper.getAvailablePlayerCommands()));
}
public void addPlayerListener(Player.Listener listener) {
postOrRun(
applicationHandler,
() -> {
wrapperListeners.add(listener);
playerWrapper.addListener(listener);
});
}
public void removePlayerListener(Player.Listener listener) {
postOrRun(
applicationHandler,
() -> {
playerWrapper.removeListener(listener);
wrapperListeners.remove(listener);
});
}
private void setPlayerInternal(
@Nullable PlayerWrapper oldPlayerWrapper, PlayerWrapper newPlayerWrapper) {
playerWrapper = newPlayerWrapper;
if (oldPlayerWrapper != null) {
oldPlayerWrapper.removeListener(checkStateNotNull(this.playerListener));
for (int i = 0; i < wrapperListeners.size(); i++) {
oldPlayerWrapper.removeListener(wrapperListeners.get(i));
}
}
PlayerListener playerListener = new PlayerListener(this, newPlayerWrapper);
newPlayerWrapper.addListener(playerListener);
for (int i = 0; i < wrapperListeners.size(); i++) {
newPlayerWrapper.addListener(wrapperListeners.get(i));
}
this.playerListener = playerListener;
dispatchRemoteControllerTaskToLegacyStub(
@ -291,6 +303,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
if (playerListener != null) {
playerWrapper.removeListener(playerListener);
}
for (int i = 0; i < wrapperListeners.size(); i++) {
playerWrapper.removeListener(wrapperListeners.get(i));
}
wrapperListeners.clear();
});
} catch (Exception e) {
// Catch all exceptions to ensure the rest of this method to be executed as exceptions may be
@ -1073,75 +1089,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
(callback, seq) -> callback.onDeviceInfoChanged(seq, playerInfo.deviceInfo));
}
/* package */ boolean onMediaButtonEvent(Intent intent) {
KeyEvent keyEvent = DefaultActionFactory.getKeyEvent(intent);
ComponentName intentComponent = intent.getComponent();
if (!Objects.equals(intent.getAction(), Intent.ACTION_MEDIA_BUTTON)
|| (intentComponent != null
&& !Objects.equals(intentComponent.getPackageName(), context.getPackageName()))
|| keyEvent == null
|| 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;
}
Runnable command;
switch (keyEvent.getKeyCode()) {
case KEYCODE_MEDIA_PLAY_PAUSE:
command =
getPlayerWrapper().getPlayWhenReady()
? () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER)
: () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
break;
case KEYCODE_MEDIA_PLAY:
command = () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
break;
case KEYCODE_MEDIA_PAUSE:
command = () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
break;
case KEYCODE_MEDIA_NEXT: // Fall through.
case KEYCODE_MEDIA_SKIP_FORWARD:
command =
() -> sessionStub.seekToNextForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
break;
case KEYCODE_MEDIA_PREVIOUS: // Fall through.
case KEYCODE_MEDIA_SKIP_BACKWARD:
command =
() ->
sessionStub.seekToPreviousForControllerInfo(
controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
break;
case KEYCODE_MEDIA_FAST_FORWARD:
command =
() -> sessionStub.seekForwardForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
break;
case KEYCODE_MEDIA_REWIND:
command =
() -> sessionStub.seekBackForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
break;
case KEYCODE_MEDIA_STOP:
command = () -> sessionStub.stopForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
break;
default:
return false;
}
postOrRun(
getApplicationHandler(),
() -> {
command.run();
sessionStub.getConnectedControllersManager().flushCommandQueue(controllerInfo);
});
return true;
}
/* @FunctionalInterface */
interface RemoteControllerTask {

View File

@ -31,6 +31,7 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.view.KeyEvent;
import androidx.annotation.CallSuper;
import androidx.annotation.DoNotInline;
import androidx.annotation.GuardedBy;
@ -156,7 +157,7 @@ public abstract class MediaSessionService extends Service {
/** The action for {@link Intent} filter that must be declared by the service. */
public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaSessionService";
private static final String TAG = "MSessionService";
private static final String TAG = "MSSImpl";
private final Object lock;
private final Handler mainHandler;
@ -425,8 +426,9 @@ public abstract class MediaSessionService extends Service {
}
addSession(session);
}
if (!session.getImpl().onMediaButtonEvent(intent)) {
Log.w(TAG, "Ignoring unrecognized media button intent.");
@Nullable KeyEvent keyEvent = actionFactory.getKeyEvent(intent);
if (keyEvent != null) {
getMediaNotificationManager().onMediaButtonEvent(session, keyEvent);
}
} else if (session != null && actionFactory.isCustomAction(intent)) {
@Nullable String customAction = actionFactory.getCustomAction(intent);

View File

@ -39,7 +39,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@ -412,9 +411,6 @@ public class MediaSessionServiceTest {
serviceController.startCommand(/* flags= */ 0, /* startId= */ 0);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(service.callers).hasSize(1);
assertThat(service.session.isMediaNotificationController(service.callers.get(0))).isTrue();
controller.release();
serviceController.destroy();
}
@ -505,35 +501,21 @@ public class MediaSessionServiceTest {
private static final class TestServiceWithPlaybackResumption extends MediaSessionService {
private final List<MediaSession.ControllerInfo> callers;
private ImmutableList<MediaItem> mediaItems;
@Nullable private MediaSession session;
public TestServiceWithPlaybackResumption() {
callers = new ArrayList<>();
mediaItems = ImmutableList.of();
}
private List<MediaItem> mediaItems = ImmutableList.of();
public void setMediaItems(List<MediaItem> mediaItems) {
this.mediaItems = ImmutableList.copyOf(mediaItems);
this.mediaItems = mediaItems;
}
@Nullable private MediaSession session;
@Override
public void onCreate() {
super.onCreate();
Context context = ApplicationProvider.getApplicationContext();
ExoPlayer player = new TestExoPlayerBuilder(context).build();
ForwardingPlayer forwardingPlayer =
new ForwardingPlayer(player) {
@Override
public void play() {
callers.add(session.getControllerForCurrentRequest());
super.play();
}
};
session =
new MediaSession.Builder(context, forwardingPlayer)
new MediaSession.Builder(context, player)
.setCallback(
new MediaSession.Callback() {
@Override
@ -564,14 +546,11 @@ public class MediaSessionServiceTest {
@Override
public void onDestroy() {
if (session != null) {
session.getPlayer().stop();
session.getPlayer().clearMediaItems();
session.getPlayer().release();
session.release();
callers.clear();
session = null;
}
session.getPlayer().stop();
session.getPlayer().clearMediaItems();
session.getPlayer().release();
session.release();
session = null;
super.onDestroy();
}
}

View File

@ -41,10 +41,9 @@ android {
dependencies {
implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'test-session-common')
implementation project(modulePrefix + 'test-data')
implementation 'androidx.media:media:' + androidxMediaVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'androidx.test:core:' + androidxTestCoreVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation project(modulePrefix + 'test-data')
androidTestImplementation project(modulePrefix + 'lib-exoplayer')
androidTestImplementation project(modulePrefix + 'test-utils')

View File

@ -26,7 +26,6 @@ import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.fail;
import android.content.Context;
import android.os.Bundle;
@ -44,7 +43,6 @@ import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.test.session.R;
import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.MainLooperTestRule;
import androidx.media3.test.session.common.TestHandler;
import androidx.media3.test.session.common.TestUtils;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.test.core.app.ApplicationProvider;
@ -63,6 +61,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
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;
@ -74,28 +73,16 @@ import org.junit.runner.RunWith;
@LargeTest
public class MediaSessionCallbackTest {
// Prepares the main looper.
private static final String TAG = "MSessionCallbackTest";
@ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule();
@Rule
public final HandlerThreadTestRule playerThreadTestRule =
new HandlerThreadTestRule("MSessionCallbackTest:player");
@Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG);
@Rule
public final HandlerThreadTestRule controllerThreadTestRule =
new HandlerThreadTestRule("MSessionCallbackTest:controller");
@Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule();
@Rule public final MediaSessionTestRule sessionTestRule = new MediaSessionTestRule();
// Used to create controllers in the service running in a different process.
@Rule
public final RemoteControllerTestRule remoteControllerTestRule = new RemoteControllerTestRule();
// Used to create controllers on a different thread in the local process.
@Rule
public final MediaControllerTestRule controllerTestRule =
new MediaControllerTestRule(controllerThreadTestRule);
private Context context;
private MockPlayer player;
private ListeningExecutorService executorService;
@ -105,7 +92,7 @@ public class MediaSessionCallbackTest {
context = ApplicationProvider.getApplicationContext();
player =
new MockPlayer.Builder()
.setApplicationLooper(playerThreadTestRule.getHandler().getLooper())
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.build();
// Intentionally use an Executor with another thread to test asynchronous workflows involving
// background tasks.
@ -142,7 +129,7 @@ public class MediaSessionCallbackTest {
.setId("testOnConnect_correctControllerVersions")
.build());
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(controllerVersion.get()).isEqualTo(MediaLibraryInfo.VERSION_INT);
@ -198,7 +185,7 @@ public class MediaSessionCallbackTest {
"onConnect_acceptWithMissingSessionCommand_buttonDisabledAndPermissionDenied")
.build());
RemoteMediaController remoteController =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
ImmutableList<CommandButton> layout = remoteController.getCustomLayout();
@ -228,7 +215,7 @@ public class MediaSessionCallbackTest {
.setId("onConnect_emptyPlayerCommands_commandReleaseAlwaysIncluded")
.build());
RemoteMediaController remoteController =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
assertThat(remoteController.getAvailableCommands().size()).isEqualTo(1);
assertThat(remoteController.getAvailableCommands().contains(Player.COMMAND_RELEASE)).isTrue();
@ -250,7 +237,7 @@ public class MediaSessionCallbackTest {
.setCallback(callback)
.setId("testOnPostConnect_afterConnected")
.build());
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
}
@ -276,7 +263,7 @@ public class MediaSessionCallbackTest {
.setCallback(callback)
.setId("testOnPostConnect_afterConnectionRejected")
.build());
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse();
}
@ -309,7 +296,7 @@ public class MediaSessionCallbackTest {
.setId("testOnCommandRequest")
.build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.prepare();
Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
@ -371,7 +358,7 @@ public class MediaSessionCallbackTest {
.setId("testOnCustomCommand")
.build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
SessionResult result = controller.sendCustomCommand(testCommand, testArgs);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
@ -411,7 +398,7 @@ public class MediaSessionCallbackTest {
.setId("testOnSetRating")
.build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
SessionResult result = controller.setRating(testMediaId, testRating);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
@ -447,7 +434,7 @@ public class MediaSessionCallbackTest {
.setId("testOnSetRating")
.build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
SessionResult result = controller.setRating(testRating);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
@ -472,7 +459,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(mediaItem);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
@ -489,7 +476,7 @@ public class MediaSessionCallbackTest {
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.setMediaItemIncludeLocalConfiguration(mediaItemWithoutLocalConfiguration);
@ -511,7 +498,7 @@ public class MediaSessionCallbackTest {
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.setMediaItemsIncludeLocalConfiguration(mediaItemsWithoutLocalConfiguration);
@ -531,7 +518,7 @@ public class MediaSessionCallbackTest {
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.setMediaItemIncludeLocalConfiguration(mediaItemWithLocalConfiguration);
@ -551,7 +538,7 @@ public class MediaSessionCallbackTest {
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.setMediaItemsIncludeLocalConfiguration(fullMediaItems);
@ -578,7 +565,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(mediaItem, /* startPositionMs= */ 1234);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS);
@ -607,7 +594,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(mediaItem, /* resetPosition= */ true);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
@ -635,7 +622,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItems(mediaItems);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
@ -664,7 +651,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 1234);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS);
@ -695,7 +682,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItems(mediaItems, /* resetPosition= */ true);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
@ -725,7 +712,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.addMediaItem(mediaItem);
player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
@ -742,7 +729,7 @@ public class MediaSessionCallbackTest {
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.addMediaItemIncludeLocalConfiguration(mediaItemWithoutLocalConfiguration);
@ -763,7 +750,7 @@ public class MediaSessionCallbackTest {
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.addMediaItemsIncludeLocalConfiguration(mediaItemsWithoutLocalConfiguration);
@ -782,7 +769,7 @@ public class MediaSessionCallbackTest {
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.addMediaItemIncludeLocalConfiguration(mediaItemWithLocalConfiguration);
@ -802,7 +789,7 @@ public class MediaSessionCallbackTest {
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
// Default MediaSession.Callback.onAddMediaItems will be called
controller.addMediaItemsIncludeLocalConfiguration(fullMediaItems);
@ -830,7 +817,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(existingItem);
controller.addMediaItem(/* index= */ 1, mediaItem);
@ -862,7 +849,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.addMediaItems(mediaItems);
player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
@ -892,7 +879,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(existingItem);
controller.addMediaItems(/* index= */ 1, mediaItems);
@ -935,7 +922,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(mediaItem, /* startPositionMs= */ 100);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS);
@ -973,7 +960,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 100);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS);
@ -1013,7 +1000,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 100);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
@ -1052,7 +1039,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItems(mediaItems, true);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
@ -1090,7 +1077,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.play();
@ -1111,7 +1098,7 @@ public class MediaSessionCallbackTest {
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.play();
@ -1135,7 +1122,7 @@ public class MediaSessionCallbackTest {
@Override
public ListenableFuture<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(
MediaSession mediaSession, ControllerInfo controller) {
fail();
Assert.fail();
return Futures.immediateFuture(
new MediaSession.MediaItemsWithStartPosition(
MediaTestUtils.createMediaItems(/* size= */ 10),
@ -1147,7 +1134,7 @@ public class MediaSessionCallbackTest {
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.play();
@ -1186,7 +1173,7 @@ public class MediaSessionCallbackTest {
Bundle testConnectionHints = new Bundle();
testConnectionHints.putString("test_key", "test_value");
remoteControllerTestRule.createRemoteController(
controllerTestRule.createRemoteController(
session.getToken(), /* waitForConnection= */ false, testConnectionHints);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(TestUtils.equals(testConnectionHints, connectionHints.get())).isTrue();
@ -1212,107 +1199,20 @@ public class MediaSessionCallbackTest {
})
.build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controllerTestRule.createRemoteController(session.getToken());
controller.release();
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
}
@Test
public void
seekToNextMediaItem_controllerListenerTriggeredByMasking_commandNotYetArrivedAtSession()
throws Exception {
MediaItem mediaItem1 =
new MediaItem.Builder().setMediaId("id1").setUri("http://www.example.com/1").build();
MediaItem mediaItem2 =
new MediaItem.Builder().setMediaId("id2").setUri("http://www.example.com/2").build();
ExoPlayer testPlayer =
playerThreadTestRule
.getHandler()
.postAndSync(
() -> {
ExoPlayer exoPlayer = new TestExoPlayerBuilder(context).build();
exoPlayer.setMediaItems(ImmutableList.of(mediaItem1, mediaItem2));
return exoPlayer;
});
List<MediaItem> currentMediaItemsOfPlayer = new ArrayList<>();
AtomicReference<MediaController> controller = new AtomicReference<>();
List<String> eventOrder = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(2);
// Listener added to player before the the session is built and the session adds a listener.
testPlayer.addListener(
new Player.Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
currentMediaItemsOfPlayer.add(testPlayer.getCurrentMediaItem());
eventOrder.add("player.onMediaItemTransition");
}
@Override
public void onEvents(Player player, Player.Events events) {
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
// Player still has the first item. Command has not yet arrived at the session.
currentMediaItemsOfPlayer.add(testPlayer.getCurrentMediaItem());
eventOrder.add("player.onEvents");
latch.countDown();
}
}
});
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, testPlayer)
.setId(
"listener_controllerListenerTriggeredByMasking_commandNotYetArrivedAtSession")
.build());
controller.set(controllerTestRule.createController(session.getToken()));
controller
.get()
.addListener(
/* listener= */ new Player.Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
eventOrder.add("controller.onMediaItemTransition");
postToPlayerAndSync(
() -> currentMediaItemsOfPlayer.add(testPlayer.getCurrentMediaItem()));
}
@Override
public void onEvents(Player player, Player.Events events) {
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
// Triggered by masking in the same looper iteration as where
// controller.seekToNextMediaItem() is called.
eventOrder.add("controller.onEvents");
postToPlayerAndSync(
() -> currentMediaItemsOfPlayer.add(testPlayer.getCurrentMediaItem()));
latch.countDown();
}
}
});
postToControllerAndSync(controller.get()::seekToNextMediaItem);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(currentMediaItemsOfPlayer)
.containsExactly(mediaItem1, mediaItem1, mediaItem2, mediaItem2)
.inOrder();
assertThat(eventOrder)
.containsExactly(
"controller.onMediaItemTransition",
"controller.onEvents",
"player.onMediaItemTransition",
"player.onEvents")
.inOrder();
postToControllerAndSync(() -> controller.get().release());
}
@Test
public void seekToNextMediaItem_playerListenerTriggeredByMasking_immediateCallHasStaleController()
public void seekToNextMediaItem_inProcessController_correctMediaItemTransitionsEvents()
throws Exception {
MediaItem mediaItem1 =
new MediaItem.Builder().setMediaId("id1").setUri("http://www.example.com/1").build();
MediaItem mediaItem2 =
new MediaItem.Builder().setMediaId("id2").setUri("http://www.example.com/2").build();
ExoPlayer testPlayer =
playerThreadTestRule
threadTestRule
.getHandler()
.postAndSync(
() -> {
@ -1320,88 +1220,48 @@ public class MediaSessionCallbackTest {
exoPlayer.setMediaItems(ImmutableList.of(mediaItem1, mediaItem2));
return exoPlayer;
});
List<String> currentMediaIdsOfController = new ArrayList<>();
List<String> capturedMediaItemIds = new ArrayList<>();
List<Player.Events> capturedEvents = new ArrayList<>();
List<String> eventOrder = new ArrayList<>();
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<MediaController> controller = new AtomicReference<>();
// Listener added to player before the the session is built and the session adds a listener.
testPlayer.addListener(
CountDownLatch latch = new CountDownLatch(1);
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, testPlayer)
.setId("seekToNextMediaItem_inProcessController_correctMediaItemTransitionsEvents")
.build());
MediaController controller =
new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken())
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.buildAsync()
.get();
controller.addListener(
new Player.Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
postToControllerAndSync(
() ->
currentMediaIdsOfController.add(
controller.get().getCurrentMediaItem().mediaId));
eventOrder.add("player.onMediaItemTransition");
capturedMediaItemIds.add(controller.getCurrentMediaItem().mediaId);
eventOrder.add("onMediaItemTransition");
}
@Override
public void onEvents(Player player, Player.Events events) {
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
postToControllerAndSync(
() ->
currentMediaIdsOfController.add(
controller.get().getCurrentMediaItem().mediaId));
eventOrder.add("player.onEvents");
capturedMediaItemIds.add(controller.getCurrentMediaItem().mediaId);
capturedEvents.add(events);
eventOrder.add("onEvents");
latch.countDown();
}
}
});
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, testPlayer)
.setId(
"listener_playerListenerTriggeredByMasking_statusUpdateArrivedAtSameProcessController")
.build());
controller.set(controllerTestRule.createController(session.getToken()));
controller
.get()
.addListener(
new Player.Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
currentMediaIdsOfController.add(controller.get().getCurrentMediaItem().mediaId);
eventOrder.add("controller.onMediaItemTransition");
}
@Override
public void onEvents(Player player, Player.Events events) {
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
currentMediaIdsOfController.add(controller.get().getCurrentMediaItem().mediaId);
eventOrder.add("controller.onEvents");
latch.countDown();
}
}
});
postToPlayerAndSync(testPlayer::seekToNextMediaItem);
threadTestRule.getHandler().postAndSync(testPlayer::seekToNextMediaItem);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(currentMediaIdsOfController).containsExactly("id1", "id2", "id2", "id2").inOrder();
assertThat(eventOrder)
.containsExactly(
"player.onMediaItemTransition",
"controller.onMediaItemTransition",
"controller.onEvents",
"player.onEvents")
.inOrder();
}
private void postToPlayerAndSync(TestHandler.TestRunnable r) {
try {
playerThreadTestRule.getHandler().postAndSync(r);
} catch (Exception e) {
fail(e.getMessage());
}
}
private void postToControllerAndSync(TestHandler.TestRunnable r) {
try {
controllerThreadTestRule.getHandler().postAndSync(r);
} catch (Exception e) {
fail(e.getMessage());
}
assertThat(capturedMediaItemIds).containsExactly("id2", "id2").inOrder();
assertThat(eventOrder).containsExactly("onMediaItemTransition", "onEvents").inOrder();
assertThat(capturedEvents).hasSize(1);
assertThat(capturedEvents.get(0).size()).isEqualTo(2);
assertThat(capturedEvents.get(0).contains(Player.EVENT_MEDIA_ITEM_TRANSITION)).isTrue();
assertThat(capturedEvents.get(0).contains(Player.EVENT_POSITION_DISCONTINUITY)).isTrue();
}
private static MediaItem updateMediaItemWithLocalConfiguration(MediaItem mediaItem) {

View File

@ -15,13 +15,6 @@
*/
package androidx.media3.session;
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_PREVIOUS;
import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND;
import static android.view.KeyEvent.KEYCODE_MEDIA_STOP;
import static androidx.media3.common.Player.STATE_IDLE;
import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
@ -30,9 +23,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertThrows;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
@ -40,14 +31,11 @@ import android.os.SystemClock;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.text.TextUtils;
import android.view.KeyEvent;
import androidx.media.MediaSessionManager;
import androidx.media3.common.ForwardingPlayer;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.Player;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.MainLooperTestRule;
import androidx.media3.test.session.common.TestHandler;
@ -61,7 +49,6 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
@ -95,6 +82,7 @@ public class MediaSessionTest {
context = ApplicationProvider.getApplicationContext();
handler = threadTestRule.getHandler();
player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build();
session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player)
@ -103,7 +91,7 @@ public class MediaSessionTest {
new MediaSession.Callback() {
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session, ControllerInfo controller) {
MediaSession session, MediaSession.ControllerInfo controller) {
if (TextUtils.equals(
context.getPackageName(), controller.getPackageName())) {
return MediaSession.Callback.super.onConnect(session, controller);
@ -161,8 +149,7 @@ public class MediaSessionTest {
// expected. pass-through
}
// Empty string as ID is allowed.
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setId("").build());
new MediaSession.Builder(context, player).setId("").build().release();
}
@Test
@ -341,7 +328,7 @@ public class MediaSessionTest {
new MediaSession.Callback() {
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session, ControllerInfo controller) {
MediaSession session, MediaSession.ControllerInfo controller) {
Future<SessionResult> result =
session.sendCustomCommand(controller, testCommand, /* args= */ Bundle.EMPTY);
try {
@ -355,7 +342,7 @@ public class MediaSessionTest {
}
@Override
public void onPostConnect(MediaSession session, ControllerInfo controller) {
public void onPostConnect(MediaSession session, MediaSession.ControllerInfo controller) {
Future<SessionResult> result =
session.sendCustomCommand(controller, testCommand, /* args= */ Bundle.EMPTY);
try {
@ -378,6 +365,10 @@ public class MediaSessionTest {
/** Test {@link MediaSession#getSessionCompatToken()}. */
@Test
public void getSessionCompatToken_returnsCompatibleWithMediaControllerCompat() throws Exception {
String expectedControllerCompatPackageName =
(21 <= Util.SDK_INT && Util.SDK_INT < 24)
? MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER
: context.getPackageName();
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player)
@ -386,10 +377,9 @@ public class MediaSessionTest {
new MediaSession.Callback() {
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session, ControllerInfo controller) {
MediaSession session, MediaSession.ControllerInfo controller) {
if (TextUtils.equals(
getControllerCallerPackageName(controller),
controller.getPackageName())) {
expectedControllerCompatPackageName, controller.getPackageName())) {
return MediaSession.Callback.super.onConnect(session, controller);
}
return MediaSession.ConnectionResult.reject();
@ -426,7 +416,7 @@ public class MediaSessionTest {
new MediaSession.Callback() {
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session, ControllerInfo controller) {
MediaSession session, MediaSession.ControllerInfo controller) {
controllerVersionRef.set(controller.getControllerVersion());
connectedLatch.countDown();
return MediaSession.Callback.super.onConnect(session, controller);
@ -504,302 +494,4 @@ public class MediaSessionTest {
assertThat(bufferedPositionsMs).containsExactly(0L, 0L, 0L, 0L, 0L).inOrder();
}
@Test
public void onMediaButtonEvent_allSupportedKeys_notificationControllerConnected_dispatchesEvent()
throws Exception {
AtomicReference<MediaSession> session = new AtomicReference<>();
CallerCollectorPlayer callerCollectorPlayer = new CallerCollectorPlayer(player, session);
session.set(
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, callerCollectorPlayer)
.setId("getSessionCompatToken_returnsCompatibleWithMediaControllerCompat")
.setCallback(
new MediaSession.Callback() {
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session, ControllerInfo controller) {
if (TextUtils.equals(
context.getPackageName(), controller.getPackageName())) {
return MediaSession.Callback.super.onConnect(session, controller);
}
return MediaSession.ConnectionResult.reject();
}
})
.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();
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();
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_TO_NEXT, TIMEOUT_MS);
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS);
player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS);
assertThat(callerCollectorPlayer.callingControllers).hasSize(7);
for (ControllerInfo controllerInfo : callerCollectorPlayer.callingControllers) {
assertThat(session.get().isMediaNotificationController(controllerInfo)).isTrue();
}
}
@Test
public void
onMediaButtonEvent_allSupportedKeys_notificationControllerNotConnected_dispatchesEventThroughFrameworkFallback()
throws Exception {
AtomicReference<MediaSession> session = new AtomicReference<>();
CallerCollectorPlayer callerCollectorPlayer = new CallerCollectorPlayer(player, session);
session.set(
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, callerCollectorPlayer)
.setId("getSessionCompatToken_returnsCompatibleWithMediaControllerCompat")
.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();
}
})
.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();
// Fallback code path through platform session when MediaSessionImpl doesn't handle the event.
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_TO_NEXT, TIMEOUT_MS);
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS);
player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS);
assertThat(callerCollectorPlayer.callingControllers).hasSize(7);
for (ControllerInfo controllerInfo : callerCollectorPlayer.callingControllers) {
assertThat(session.get().isMediaNotificationController(controllerInfo)).isFalse();
assertThat(controllerInfo.getControllerVersion())
.isEqualTo(ControllerInfo.LEGACY_CONTROLLER_VERSION);
assertThat(controllerInfo.getPackageName())
.isEqualTo(getControllerCallerPackageName(controllerInfo));
}
}
@Test
public void onMediaButtonEvent_noKeyEvent_returnsFalse() {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
assertThat(isEventHandled).isFalse();
}
@Test
public void onMediaButtonEvent_noKeyEvent_mediaNotificationControllerConnected_returnsFalse()
throws Exception {
Bundle connectionHints = new Bundle();
connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true);
new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken())
.setConnectionHints(connectionHints)
.buildAsync()
.get();
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.removeExtra(Intent.EXTRA_KEY_EVENT);
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
assertThat(isEventHandled).isFalse();
}
@Test
public void onMediaButtonEvent_invalidKeyEvent_returnsFalse() {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
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);
assertThat(isEventHandled).isFalse();
}
@Test
public void onMediaButtonEvent_invalidKeyEvent_mediaNotificationControllerConnected_returnsFalse()
throws Exception {
Bundle connectionHints = new Bundle();
connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true);
new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken())
.setConnectionHints(connectionHints)
.buildAsync()
.get();
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
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);
assertThat(isEventHandled).isFalse();
}
@Test
public void onMediaButtonEvent_invalidAction_returnsFalse() {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.setAction("notAMediaButtonAction");
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
assertThat(isEventHandled).isFalse();
}
@Test
public void onMediaButtonEvent_invalidAction_mediaNotificationControllerConnected_returnsFalse()
throws Exception {
Bundle connectionHints = new Bundle();
connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true);
new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken())
.setConnectionHints(connectionHints)
.buildAsync()
.get();
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.setAction("notAMediaButtonAction");
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
assertThat(isEventHandled).isFalse();
}
@Test
public void onMediaButtonEvent_invalidComponent_returnsFalse() {
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.setComponent(new ComponentName("a.package", "a.class"));
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
assertThat(isEventHandled).isFalse();
}
@Test
public void
onMediaButtonEvent_invalidComponent_mediaNotificationControllerConnected_returnsFalse()
throws Exception {
Bundle connectionHints = new Bundle();
connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true);
new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken())
.setConnectionHints(connectionHints)
.buildAsync()
.get();
Intent intent = getMediaButtonIntent(KEYCODE_MEDIA_PLAY);
intent.setComponent(new ComponentName("a.package", "a.class"));
boolean isEventHandled = session.getImpl().onMediaButtonEvent(intent);
assertThat(isEventHandled).isFalse();
}
private static Intent getMediaButtonIntent(int keyCode) {
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
intent.setComponent(
new ComponentName(ApplicationProvider.getApplicationContext(), Object.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
return intent;
}
/**
* Returns the expected {@link MediaSessionManager.RemoteUserInfo#getPackageName()} of a
* controller hosted in the test companion app.
*
* <p>Before API 21 and after API 23 the package name is {@link Context#getPackageName()} of the
* {@link ApplicationProvider#getApplicationContext() application under test}.
*
* <p>The early implementations (API 21 - 23), the platform MediaSession doesn't report the caller
* package name. Instead the package of the RemoteUserInfo is set for all external controllers to
* the same {@code MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER} (see
* MediaSessionCompat.MediaSessionCallbackApi21.setCurrentControllerInfo()).
*
* <p>Calling this method should only be required to test legacy behaviour.
*/
private static String getControllerCallerPackageName(ControllerInfo controllerInfo) {
return (Util.SDK_INT < 21
|| Util.SDK_INT > 23
|| controllerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION)
? ApplicationProvider.getApplicationContext().getPackageName()
: MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER;
}
private static class CallerCollectorPlayer extends ForwardingPlayer {
private final List<ControllerInfo> callingControllers;
private final AtomicReference<MediaSession> session;
public CallerCollectorPlayer(Player player, AtomicReference<MediaSession> mediaSession) {
super(player);
this.session = mediaSession;
callingControllers = new ArrayList<>();
}
@Override
public void play() {
callingControllers.add(session.get().getControllerForCurrentRequest());
super.play();
}
@Override
public void pause() {
callingControllers.add(session.get().getControllerForCurrentRequest());
super.pause();
}
@Override
public void seekBack() {
callingControllers.add(session.get().getControllerForCurrentRequest());
super.seekBack();
}
@Override
public void seekForward() {
callingControllers.add(session.get().getControllerForCurrentRequest());
super.seekForward();
}
@Override
public void seekToNext() {
callingControllers.add(session.get().getControllerForCurrentRequest());
super.seekToNext();
}
@Override
public void seekToPrevious() {
callingControllers.add(session.get().getControllerForCurrentRequest());
super.seekToPrevious();
}
@Override
public void stop() {
callingControllers.add(session.get().getControllerForCurrentRequest());
super.stop();
}
}
}