Resolve and dispatch media button events within Media3
Before this change, media button events are routed from `onStartCommand` of the `MediaSessionService` to the `MediaSessionCompat`, resolved by the legacy library to a session command called on `MediaSessionCompat.Callback` from where the command is delegated back to the Media3 session. With this change the keycode is resolved directly to a Media3 command that is sent to the session through the media notification controller of the session. After this change, a playback or custom command sent to the session from a notification, either as a pending intent (before API 33) or as a legacy session command, look the same and the caller is the media notification controller on all API levels. PiperOrigin-RevId: 568224123
This commit is contained in:
parent
5e05e2ec22
commit
ffd7bb5639
@ -70,6 +70,12 @@
|
|||||||
* Use the media notification controller as proxy to set available commands
|
* Use the media notification controller as proxy to set available commands
|
||||||
and custom layout used to populate the notification and the platform
|
and custom layout used to populate the notification and the platform
|
||||||
session.
|
session.
|
||||||
|
* Convert media button events that are received by
|
||||||
|
`MediaSessionService.onStartCommand()` within Media3 instead of routing
|
||||||
|
them to the platform session and back to Media3. With this, the caller
|
||||||
|
controller is always the media notification controller and apps can
|
||||||
|
easily recognize calls coming from the notification in the same way on
|
||||||
|
all supported API levels.
|
||||||
* UI:
|
* UI:
|
||||||
* Downloads:
|
* Downloads:
|
||||||
* OkHttp Extension:
|
* OkHttp Extension:
|
||||||
|
@ -388,6 +388,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
@Override
|
@Override
|
||||||
public void play() {
|
public void play() {
|
||||||
if (!isPlayerCommandAvailable(Player.COMMAND_PLAY_PAUSE)) {
|
if (!isPlayerCommandAvailable(Player.COMMAND_PLAY_PAUSE)) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Calling play() omitted due to COMMAND_PLAY_PAUSE not being available. If this play"
|
||||||
|
+ " command has started the service for instance for playback resumption, this may"
|
||||||
|
+ " prevent the service from being started into the foreground.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -522,6 +527,13 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
@Override
|
@Override
|
||||||
public void setPlayWhenReady(boolean playWhenReady) {
|
public void setPlayWhenReady(boolean playWhenReady) {
|
||||||
if (!isPlayerCommandAvailable(Player.COMMAND_PLAY_PAUSE)) {
|
if (!isPlayerCommandAvailable(Player.COMMAND_PLAY_PAUSE)) {
|
||||||
|
if (playWhenReady) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Calling play() omitted due to COMMAND_PLAY_PAUSE not being available. If this play"
|
||||||
|
+ " command has started the service for instance for playback resumption, this may"
|
||||||
|
+ " prevent the service from being started into the foreground.");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,15 @@ package androidx.media3.session;
|
|||||||
|
|
||||||
import static android.app.Service.STOP_FOREGROUND_DETACH;
|
import static android.app.Service.STOP_FOREGROUND_DETACH;
|
||||||
import static android.app.Service.STOP_FOREGROUND_REMOVE;
|
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;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
@ -25,6 +34,7 @@ import android.content.pm.ServiceInfo;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.view.KeyEvent;
|
||||||
import androidx.annotation.DoNotInline;
|
import androidx.annotation.DoNotInline;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
@ -44,7 +54,6 @@ import java.util.Map;
|
|||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,7 +74,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
private final NotificationManagerCompat notificationManagerCompat;
|
private final NotificationManagerCompat notificationManagerCompat;
|
||||||
private final Executor mainExecutor;
|
private final Executor mainExecutor;
|
||||||
private final Intent startSelfIntent;
|
private final Intent startSelfIntent;
|
||||||
private final Map<MediaSession, ListenableFuture<MediaController>> controllerMap;
|
private final Map<MediaSession, ControllerAndListener> controllerAndListenerMap;
|
||||||
|
|
||||||
private int totalNotificationCount;
|
private int totalNotificationCount;
|
||||||
@Nullable private MediaNotification mediaNotification;
|
@Nullable private MediaNotification mediaNotification;
|
||||||
@ -82,30 +91,34 @@ import java.util.concurrent.TimeoutException;
|
|||||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
mainExecutor = (runnable) -> Util.postOrRun(mainHandler, runnable);
|
mainExecutor = (runnable) -> Util.postOrRun(mainHandler, runnable);
|
||||||
startSelfIntent = new Intent(mediaSessionService, mediaSessionService.getClass());
|
startSelfIntent = new Intent(mediaSessionService, mediaSessionService.getClass());
|
||||||
controllerMap = new HashMap<>();
|
controllerAndListenerMap = new HashMap<>();
|
||||||
startedInForeground = false;
|
startedInForeground = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSession(MediaSession session) {
|
public void addSession(MediaSession session) {
|
||||||
if (controllerMap.containsKey(session)) {
|
if (controllerAndListenerMap.containsKey(session)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MediaControllerListener listener = new MediaControllerListener(mediaSessionService, session);
|
MediaControllerListener controllerListener =
|
||||||
|
new MediaControllerListener(mediaSessionService, session);
|
||||||
|
PlayerListener playerListener = new PlayerListener(mediaSessionService, session);
|
||||||
Bundle connectionHints = new Bundle();
|
Bundle connectionHints = new Bundle();
|
||||||
connectionHints.putBoolean(KEY_MEDIA_NOTIFICATION_MANAGER, true);
|
connectionHints.putBoolean(KEY_MEDIA_NOTIFICATION_MANAGER, true);
|
||||||
ListenableFuture<MediaController> controllerFuture =
|
ListenableFuture<MediaController> controllerFuture =
|
||||||
new MediaController.Builder(mediaSessionService, session.getToken())
|
new MediaController.Builder(mediaSessionService, session.getToken())
|
||||||
.setConnectionHints(connectionHints)
|
.setConnectionHints(connectionHints)
|
||||||
.setListener(listener)
|
.setListener(controllerListener)
|
||||||
.setApplicationLooper(Looper.getMainLooper())
|
.setApplicationLooper(Looper.getMainLooper())
|
||||||
.buildAsync();
|
.buildAsync();
|
||||||
controllerMap.put(session, controllerFuture);
|
controllerAndListenerMap.put(
|
||||||
|
session, new ControllerAndListener(controllerFuture, playerListener));
|
||||||
controllerFuture.addListener(
|
controllerFuture.addListener(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
MediaController controller = controllerFuture.get(/* time= */ 0, TimeUnit.MILLISECONDS);
|
// Assert connection success.
|
||||||
listener.onConnected(shouldShowNotification(session));
|
controllerFuture.get(/* time= */ 0, MILLISECONDS);
|
||||||
controller.addListener(listener);
|
controllerListener.onConnected(shouldShowNotification(session));
|
||||||
|
session.getImpl().addPlayerListener(playerListener);
|
||||||
} catch (CancellationException
|
} catch (CancellationException
|
||||||
| ExecutionException
|
| ExecutionException
|
||||||
| InterruptedException
|
| InterruptedException
|
||||||
@ -118,9 +131,52 @@ import java.util.concurrent.TimeoutException;
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void removeSession(MediaSession session) {
|
public void removeSession(MediaSession session) {
|
||||||
@Nullable ListenableFuture<MediaController> controllerFuture = controllerMap.remove(session);
|
ControllerAndListener controllerAndListener = controllerAndListenerMap.remove(session);
|
||||||
if (controllerFuture != null) {
|
if (controllerAndListener != null) {
|
||||||
MediaController.releaseFuture(controllerFuture);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +191,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
() -> {
|
() -> {
|
||||||
if (!mediaNotificationProvider.handleCustomCommand(session, action, extras)) {
|
if (!mediaNotificationProvider.handleCustomCommand(session, action, extras)) {
|
||||||
mainExecutor.execute(
|
mainExecutor.execute(
|
||||||
() -> sendCustomCommandIfCommandIsAvailable(mediaController, action));
|
() -> sendCustomCommandIfCommandIsAvailable(mediaController, action, extras));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -154,10 +210,10 @@ import java.util.concurrent.TimeoutException;
|
|||||||
|
|
||||||
int notificationSequence = ++totalNotificationCount;
|
int notificationSequence = ++totalNotificationCount;
|
||||||
MediaController mediaNotificationController = null;
|
MediaController mediaNotificationController = null;
|
||||||
@Nullable ListenableFuture<MediaController> controllerFuture = controllerMap.get(session);
|
ControllerAndListener controllerAndListener = controllerAndListenerMap.get(session);
|
||||||
if (controllerFuture != null && controllerFuture.isDone()) {
|
if (controllerAndListener != null && controllerAndListener.controller.isDone()) {
|
||||||
try {
|
try {
|
||||||
mediaNotificationController = Futures.getDone(controllerFuture);
|
mediaNotificationController = Futures.getDone(controllerAndListener.controller);
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
}
|
}
|
||||||
@ -261,12 +317,12 @@ import java.util.concurrent.TimeoutException;
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private MediaController getConnectedControllerForSession(MediaSession session) {
|
private MediaController getConnectedControllerForSession(MediaSession session) {
|
||||||
@Nullable ListenableFuture<MediaController> controllerFuture = controllerMap.get(session);
|
ControllerAndListener controllerAndListener = controllerAndListenerMap.get(session);
|
||||||
if (controllerFuture == null) {
|
if (controllerAndListener == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return Futures.getDone(controllerFuture);
|
return Futures.getDone(controllerAndListener.controller);
|
||||||
} catch (ExecutionException exception) {
|
} catch (ExecutionException exception) {
|
||||||
// We should never reach this.
|
// We should never reach this.
|
||||||
throw new IllegalStateException(exception);
|
throw new IllegalStateException(exception);
|
||||||
@ -274,7 +330,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void sendCustomCommandIfCommandIsAvailable(
|
private void sendCustomCommandIfCommandIsAvailable(
|
||||||
MediaController mediaController, String action) {
|
MediaController mediaController, String action, Bundle extras) {
|
||||||
@Nullable SessionCommand customCommand = null;
|
@Nullable SessionCommand customCommand = null;
|
||||||
for (SessionCommand command : mediaController.getAvailableSessionCommands().commands) {
|
for (SessionCommand command : mediaController.getAvailableSessionCommands().commands) {
|
||||||
if (command.commandCode == SessionCommand.COMMAND_CODE_CUSTOM
|
if (command.commandCode == SessionCommand.COMMAND_CODE_CUSTOM
|
||||||
@ -286,7 +342,8 @@ import java.util.concurrent.TimeoutException;
|
|||||||
if (customCommand != null
|
if (customCommand != null
|
||||||
&& mediaController.getAvailableSessionCommands().contains(customCommand)) {
|
&& mediaController.getAvailableSessionCommands().contains(customCommand)) {
|
||||||
ListenableFuture<SessionResult> future =
|
ListenableFuture<SessionResult> future =
|
||||||
mediaController.sendCustomCommand(customCommand, Bundle.EMPTY);
|
mediaController.sendCustomCommand(
|
||||||
|
new SessionCommand(action, extras), /* args= */ Bundle.EMPTY);
|
||||||
Futures.addCallback(
|
Futures.addCallback(
|
||||||
future,
|
future,
|
||||||
new FutureCallback<SessionResult>() {
|
new FutureCallback<SessionResult>() {
|
||||||
@ -304,8 +361,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class MediaControllerListener
|
private static final class MediaControllerListener implements MediaController.Listener {
|
||||||
implements MediaController.Listener, Player.Listener {
|
|
||||||
private final MediaSessionService mediaSessionService;
|
private final MediaSessionService mediaSessionService;
|
||||||
private final MediaSession session;
|
private final MediaSession session;
|
||||||
|
|
||||||
@ -334,6 +390,26 @@ import java.util.concurrent.TimeoutException;
|
|||||||
session, /* startInForegroundWhenPaused= */ false);
|
session, /* startInForegroundWhenPaused= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnected(MediaController controller) {
|
||||||
|
mediaSessionService.removeSession(session);
|
||||||
|
// We may need to hide the notification.
|
||||||
|
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
|
@Override
|
||||||
public void onEvents(Player player, Player.Events events) {
|
public void onEvents(Player player, Player.Events events) {
|
||||||
// We must limit the frequency of notification updates, otherwise the system may suppress
|
// We must limit the frequency of notification updates, otherwise the system may suppress
|
||||||
@ -343,18 +419,15 @@ import java.util.concurrent.TimeoutException;
|
|||||||
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
||||||
Player.EVENT_MEDIA_METADATA_CHANGED,
|
Player.EVENT_MEDIA_METADATA_CHANGED,
|
||||||
Player.EVENT_TIMELINE_CHANGED)) {
|
Player.EVENT_TIMELINE_CHANGED)) {
|
||||||
|
// 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(
|
mediaSessionService.onUpdateNotificationInternal(
|
||||||
session, /* startInForegroundWhenPaused= */ false);
|
session, /* startInForegroundWhenPaused= */ false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisconnected(MediaController controller) {
|
|
||||||
mediaSessionService.removeSession(session);
|
|
||||||
// We may need to hide the notification.
|
|
||||||
mediaSessionService.onUpdateNotificationInternal(
|
|
||||||
session, /* startInForegroundWhenPaused= */ false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi") // Using compile time constant FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
@SuppressLint("InlinedApi") // Using compile time constant FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||||
@ -382,6 +455,17 @@ import java.util.concurrent.TimeoutException;
|
|||||||
startedInForeground = false;
|
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)
|
@RequiresApi(24)
|
||||||
private static class Api24 {
|
private static class Api24 {
|
||||||
|
|
||||||
|
@ -129,6 +129,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
private boolean closed;
|
private boolean closed;
|
||||||
|
|
||||||
// Should be only accessed on the application looper
|
// Should be only accessed on the application looper
|
||||||
|
private final List<Player.Listener> wrapperListeners;
|
||||||
private long sessionPositionUpdateDelayMs;
|
private long sessionPositionUpdateDelayMs;
|
||||||
private boolean isMediaNotificationControllerConnected;
|
private boolean isMediaNotificationControllerConnected;
|
||||||
private ImmutableList<CommandButton> customLayout;
|
private ImmutableList<CommandButton> customLayout;
|
||||||
@ -154,6 +155,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
sessionStub = new MediaSessionStub(thisRef);
|
sessionStub = new MediaSessionStub(thisRef);
|
||||||
this.sessionActivity = sessionActivity;
|
this.sessionActivity = sessionActivity;
|
||||||
this.customLayout = customLayout;
|
this.customLayout = customLayout;
|
||||||
|
wrapperListeners = new ArrayList<>();
|
||||||
|
|
||||||
mainHandler = new Handler(Looper.getMainLooper());
|
mainHandler = new Handler(Looper.getMainLooper());
|
||||||
applicationHandler = new Handler(player.getApplicationLooper());
|
applicationHandler = new Handler(player.getApplicationLooper());
|
||||||
@ -231,14 +233,38 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
playerWrapper.getAvailablePlayerCommands()));
|
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(
|
private void setPlayerInternal(
|
||||||
@Nullable PlayerWrapper oldPlayerWrapper, PlayerWrapper newPlayerWrapper) {
|
@Nullable PlayerWrapper oldPlayerWrapper, PlayerWrapper newPlayerWrapper) {
|
||||||
playerWrapper = newPlayerWrapper;
|
playerWrapper = newPlayerWrapper;
|
||||||
if (oldPlayerWrapper != null) {
|
if (oldPlayerWrapper != null) {
|
||||||
oldPlayerWrapper.removeListener(checkStateNotNull(this.playerListener));
|
oldPlayerWrapper.removeListener(checkStateNotNull(this.playerListener));
|
||||||
|
for (int i = 0; i < wrapperListeners.size(); i++) {
|
||||||
|
oldPlayerWrapper.removeListener(wrapperListeners.get(i));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PlayerListener playerListener = new PlayerListener(this, newPlayerWrapper);
|
PlayerListener playerListener = new PlayerListener(this, newPlayerWrapper);
|
||||||
newPlayerWrapper.addListener(playerListener);
|
newPlayerWrapper.addListener(playerListener);
|
||||||
|
for (int i = 0; i < wrapperListeners.size(); i++) {
|
||||||
|
newPlayerWrapper.addListener(wrapperListeners.get(i));
|
||||||
|
}
|
||||||
this.playerListener = playerListener;
|
this.playerListener = playerListener;
|
||||||
|
|
||||||
dispatchRemoteControllerTaskToLegacyStub(
|
dispatchRemoteControllerTaskToLegacyStub(
|
||||||
@ -270,6 +296,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
if (playerListener != null) {
|
if (playerListener != null) {
|
||||||
playerWrapper.removeListener(playerListener);
|
playerWrapper.removeListener(playerListener);
|
||||||
}
|
}
|
||||||
|
for (int i = 0; i < wrapperListeners.size(); i++) {
|
||||||
|
playerWrapper.removeListener(wrapperListeners.get(i));
|
||||||
|
}
|
||||||
|
wrapperListeners.clear();
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Catch all exceptions to ensure the rest of this method to be executed as exceptions may be
|
// Catch all exceptions to ensure the rest of this method to be executed as exceptions may be
|
||||||
|
@ -690,6 +690,15 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!connectedControllersManager.isPlayerCommandAvailable(controller, command)) {
|
if (!connectedControllersManager.isPlayerCommandAvailable(controller, command)) {
|
||||||
|
if (command == COMMAND_PLAY_PAUSE
|
||||||
|
&& !sessionImpl.getPlayerWrapper().getPlayWhenReady()) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Calling play() omitted due to COMMAND_PLAY_PAUSE not being available. If this"
|
||||||
|
+ " play command has started the service for instance for playback"
|
||||||
|
+ " resumption, this may prevent the service from being started into the"
|
||||||
|
+ " foreground.");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int resultCode = sessionImpl.onPlayerCommandRequestOnHandler(controller, command);
|
int resultCode = sessionImpl.onPlayerCommandRequestOnHandler(controller, command);
|
||||||
|
@ -428,7 +428,7 @@ public abstract class MediaSessionService extends Service {
|
|||||||
}
|
}
|
||||||
@Nullable KeyEvent keyEvent = actionFactory.getKeyEvent(intent);
|
@Nullable KeyEvent keyEvent = actionFactory.getKeyEvent(intent);
|
||||||
if (keyEvent != null) {
|
if (keyEvent != null) {
|
||||||
session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
|
getMediaNotificationManager().onMediaButtonEvent(session, keyEvent);
|
||||||
}
|
}
|
||||||
} else if (session != null && actionFactory.isCustomAction(intent)) {
|
} else if (session != null && actionFactory.isCustomAction(intent)) {
|
||||||
@Nullable String customAction = actionFactory.getCustomAction(intent);
|
@Nullable String customAction = actionFactory.getCustomAction(intent);
|
||||||
|
@ -17,22 +17,33 @@ package androidx.media3.session;
|
|||||||
|
|
||||||
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
|
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.service.notification.StatusBarNotification;
|
import android.service.notification.StatusBarNotification;
|
||||||
|
import android.view.KeyEvent;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.ForwardingPlayer;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.media3.test.utils.TestExoPlayerBuilder;
|
import androidx.media3.test.utils.TestExoPlayerBuilder;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import org.junit.After;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
@ -43,21 +54,16 @@ import org.robolectric.shadows.ShadowLooper;
|
|||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class MediaSessionServiceTest {
|
public class MediaSessionServiceTest {
|
||||||
|
|
||||||
|
private static final int TIMEOUT_MS = 500;
|
||||||
|
|
||||||
private Context context;
|
private Context context;
|
||||||
private NotificationManager notificationManager;
|
private NotificationManager notificationManager;
|
||||||
private ServiceController<TestService> serviceController;
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
context = ApplicationProvider.getApplicationContext();
|
context = ApplicationProvider.getApplicationContext();
|
||||||
notificationManager =
|
notificationManager =
|
||||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
serviceController = Robolectric.buildService(TestService.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
public void tearDown() {
|
|
||||||
serviceController.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -66,6 +72,7 @@ public class MediaSessionServiceTest {
|
|||||||
ExoPlayer player2 = new TestExoPlayerBuilder(context).build();
|
ExoPlayer player2 = new TestExoPlayerBuilder(context).build();
|
||||||
MediaSession session1 = new MediaSession.Builder(context, player1).setId("1").build();
|
MediaSession session1 = new MediaSession.Builder(context, player1).setId("1").build();
|
||||||
MediaSession session2 = new MediaSession.Builder(context, player2).setId("2").build();
|
MediaSession session2 = new MediaSession.Builder(context, player2).setId("2").build();
|
||||||
|
ServiceController<TestService> serviceController = Robolectric.buildService(TestService.class);
|
||||||
TestService service = serviceController.create().get();
|
TestService service = serviceController.create().get();
|
||||||
service.setMediaNotificationProvider(
|
service.setMediaNotificationProvider(
|
||||||
new DefaultMediaNotificationProvider(
|
new DefaultMediaNotificationProvider(
|
||||||
@ -92,6 +99,7 @@ public class MediaSessionServiceTest {
|
|||||||
session2.release();
|
session2.release();
|
||||||
player1.release();
|
player1.release();
|
||||||
player2.release();
|
player2.release();
|
||||||
|
serviceController.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -105,6 +113,7 @@ public class MediaSessionServiceTest {
|
|||||||
ExoPlayer player2 = new TestExoPlayerBuilder(context).setLooper(thread2.getLooper()).build();
|
ExoPlayer player2 = new TestExoPlayerBuilder(context).setLooper(thread2.getLooper()).build();
|
||||||
MediaSession session1 = new MediaSession.Builder(context, player1).setId("1").build();
|
MediaSession session1 = new MediaSession.Builder(context, player1).setId("1").build();
|
||||||
MediaSession session2 = new MediaSession.Builder(context, player2).setId("2").build();
|
MediaSession session2 = new MediaSession.Builder(context, player2).setId("2").build();
|
||||||
|
ServiceController<TestService> serviceController = Robolectric.buildService(TestService.class);
|
||||||
TestService service = serviceController.create().get();
|
TestService service = serviceController.create().get();
|
||||||
service.setMediaNotificationProvider(
|
service.setMediaNotificationProvider(
|
||||||
new DefaultMediaNotificationProvider(
|
new DefaultMediaNotificationProvider(
|
||||||
@ -112,7 +121,6 @@ public class MediaSessionServiceTest {
|
|||||||
session -> 2000 + Integer.parseInt(session.getId()),
|
session -> 2000 + Integer.parseInt(session.getId()),
|
||||||
DefaultMediaNotificationProvider.DEFAULT_CHANNEL_ID,
|
DefaultMediaNotificationProvider.DEFAULT_CHANNEL_ID,
|
||||||
DefaultMediaNotificationProvider.DEFAULT_CHANNEL_NAME_RESOURCE_ID));
|
DefaultMediaNotificationProvider.DEFAULT_CHANNEL_NAME_RESOURCE_ID));
|
||||||
|
|
||||||
service.addSession(session1);
|
service.addSession(session1);
|
||||||
service.addSession(session2);
|
service.addSession(session2);
|
||||||
// Start the players so that we also create notifications for them.
|
// Start the players so that we also create notifications for them.
|
||||||
@ -141,6 +149,7 @@ public class MediaSessionServiceTest {
|
|||||||
new Handler(thread2.getLooper()).post(player2::release);
|
new Handler(thread2.getLooper()).post(player2::release);
|
||||||
thread1.quit();
|
thread1.quit();
|
||||||
thread2.quit();
|
thread2.quit();
|
||||||
|
serviceController.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -183,6 +192,7 @@ public class MediaSessionServiceTest {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
ServiceController<TestService> serviceController = Robolectric.buildService(TestService.class);
|
||||||
TestService service = serviceController.create().get();
|
TestService service = serviceController.create().get();
|
||||||
service.setMediaNotificationProvider(
|
service.setMediaNotificationProvider(
|
||||||
new DefaultMediaNotificationProvider(
|
new DefaultMediaNotificationProvider(
|
||||||
@ -229,6 +239,7 @@ public class MediaSessionServiceTest {
|
|||||||
.isEqualTo("customAction2");
|
.isEqualTo("customAction2");
|
||||||
session.release();
|
session.release();
|
||||||
player.release();
|
player.release();
|
||||||
|
serviceController.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -272,6 +283,7 @@ public class MediaSessionServiceTest {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
ServiceController<TestService> serviceController = Robolectric.buildService(TestService.class);
|
||||||
TestService service = serviceController.create().get();
|
TestService service = serviceController.create().get();
|
||||||
service.setMediaNotificationProvider(
|
service.setMediaNotificationProvider(
|
||||||
new DefaultMediaNotificationProvider(
|
new DefaultMediaNotificationProvider(
|
||||||
@ -317,6 +329,156 @@ public class MediaSessionServiceTest {
|
|||||||
|
|
||||||
session.release();
|
session.release();
|
||||||
player.release();
|
player.release();
|
||||||
|
serviceController.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onStartCommand_mediaButtonEvent_pausedByMediaNotificationController()
|
||||||
|
throws InterruptedException {
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
|
AtomicReference<MediaSession> session = new AtomicReference<>();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
ForwardingPlayer forwardingPlayer =
|
||||||
|
new ForwardingPlayer(player) {
|
||||||
|
@Override
|
||||||
|
public void pause() {
|
||||||
|
super.pause();
|
||||||
|
if (session
|
||||||
|
.get()
|
||||||
|
.isMediaNotificationController(session.get().getControllerForCurrentRequest())) {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
session.set(new MediaSession.Builder(context, forwardingPlayer).build());
|
||||||
|
Intent playIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
||||||
|
playIntent.setData(session.get().getUri());
|
||||||
|
playIntent.putExtra(
|
||||||
|
Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE));
|
||||||
|
ServiceController<TestService> serviceController =
|
||||||
|
Robolectric.buildService(TestService.class, playIntent);
|
||||||
|
TestService service = serviceController.create().get();
|
||||||
|
service.addSession(session.get());
|
||||||
|
player.setMediaItems(ImmutableList.of(MediaItem.fromUri("asset:///media/mp4/sample.mp4")));
|
||||||
|
player.play();
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
serviceController.startCommand(/* flags= */ 0, /* startId= */ 0);
|
||||||
|
|
||||||
|
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
assertThat(player.getPlayWhenReady()).isFalse();
|
||||||
|
session.get().release();
|
||||||
|
player.release();
|
||||||
|
serviceController.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onStartCommand_playbackResumption_calledByMediaNotificationController()
|
||||||
|
throws InterruptedException, ExecutionException, TimeoutException {
|
||||||
|
Intent playIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
||||||
|
playIntent.putExtra(
|
||||||
|
Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY));
|
||||||
|
ServiceController<TestServiceWithPlaybackResumption> serviceController =
|
||||||
|
Robolectric.buildService(TestServiceWithPlaybackResumption.class, playIntent);
|
||||||
|
TestServiceWithPlaybackResumption service = serviceController.create().get();
|
||||||
|
service.setMediaItems(
|
||||||
|
ImmutableList.of(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setMediaId("media-id-0")
|
||||||
|
.setUri("asset:///media/mp4/sample.mp4")
|
||||||
|
.build()));
|
||||||
|
MediaController controller =
|
||||||
|
new MediaController.Builder(context, service.session.getToken())
|
||||||
|
.buildAsync()
|
||||||
|
.get(TIMEOUT_MS, MILLISECONDS);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
controller.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onEvents(Player player, Player.Events events) {
|
||||||
|
if (events.contains(Player.EVENT_TIMELINE_CHANGED)
|
||||||
|
&& player.getMediaItemCount() == 1
|
||||||
|
&& player.getCurrentMediaItem().mediaId.equals("media-id-0")
|
||||||
|
&& events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)
|
||||||
|
&& player.getPlayWhenReady()
|
||||||
|
&& events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
|
||||||
|
&& player.getPlaybackState() == Player.STATE_BUFFERING) {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
serviceController.startCommand(/* flags= */ 0, /* startId= */ 0);
|
||||||
|
|
||||||
|
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
serviceController.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onStartCommand_customCommands_deliveredByMediaNotificationController()
|
||||||
|
throws InterruptedException {
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
|
AtomicReference<MediaSession> sessionRef = new AtomicReference<>();
|
||||||
|
SessionCommand expectedCustomCommand = new SessionCommand("enable_shuffle", Bundle.EMPTY);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
sessionRef.set(
|
||||||
|
new MediaSession.Builder(context, player)
|
||||||
|
.setCallback(
|
||||||
|
new MediaSession.Callback() {
|
||||||
|
@Override
|
||||||
|
public MediaSession.ConnectionResult onConnect(
|
||||||
|
MediaSession session, MediaSession.ControllerInfo controller) {
|
||||||
|
if (session.getUri().equals(sessionRef.get().getUri())
|
||||||
|
&& session.isMediaNotificationController(controller)) {
|
||||||
|
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||||
|
.setAvailableSessionCommands(
|
||||||
|
new SessionCommands.Builder().add(expectedCustomCommand).build())
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
return MediaSession.ConnectionResult.reject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<SessionResult> onCustomCommand(
|
||||||
|
MediaSession session,
|
||||||
|
MediaSession.ControllerInfo controller,
|
||||||
|
SessionCommand customCommand,
|
||||||
|
Bundle args) {
|
||||||
|
if (session.getUri().equals(sessionRef.get().getUri())
|
||||||
|
&& session.isMediaNotificationController(controller)
|
||||||
|
&& customCommand.equals(expectedCustomCommand)
|
||||||
|
&& customCommand
|
||||||
|
.customExtras
|
||||||
|
.getString("expectedKey", /* defaultValue= */ "")
|
||||||
|
.equals("expectedValue")
|
||||||
|
&& args.isEmpty()) {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build());
|
||||||
|
MediaSession session = sessionRef.get();
|
||||||
|
Intent customCommandIntent = new Intent("androidx.media3.session.CUSTOM_NOTIFICATION_ACTION");
|
||||||
|
customCommandIntent.setData(session.getUri());
|
||||||
|
customCommandIntent.putExtra(
|
||||||
|
"androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION", "enable_shuffle");
|
||||||
|
Bundle extras = new Bundle();
|
||||||
|
extras.putString("expectedKey", "expectedValue");
|
||||||
|
customCommandIntent.putExtra(
|
||||||
|
"androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION_EXTRAS", extras);
|
||||||
|
ServiceController<TestService> serviceController =
|
||||||
|
Robolectric.buildService(TestService.class, customCommandIntent);
|
||||||
|
TestService service = serviceController.create().get();
|
||||||
|
service.addSession(session);
|
||||||
|
|
||||||
|
serviceController.startCommand(/* flags= */ 0, /* startId= */ 0);
|
||||||
|
|
||||||
|
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||||
|
session.release();
|
||||||
|
player.release();
|
||||||
|
serviceController.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -336,4 +498,60 @@ public class MediaSessionServiceTest {
|
|||||||
return null; // No need to support binding or pending intents for this test.
|
return null; // No need to support binding or pending intents for this test.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class TestServiceWithPlaybackResumption extends MediaSessionService {
|
||||||
|
|
||||||
|
private List<MediaItem> mediaItems = ImmutableList.of();
|
||||||
|
|
||||||
|
public void setMediaItems(List<MediaItem> mediaItems) {
|
||||||
|
this.mediaItems = mediaItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable private MediaSession session;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
|
session =
|
||||||
|
new MediaSession.Builder(context, player)
|
||||||
|
.setCallback(
|
||||||
|
new MediaSession.Callback() {
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<MediaSession.MediaItemsWithStartPosition>
|
||||||
|
onPlaybackResumption(
|
||||||
|
MediaSession mediaSession, MediaSession.ControllerInfo controller) {
|
||||||
|
// Automatic playback resumption is expected to be called only from the media
|
||||||
|
// notification controller. So we call it here only if the callback is
|
||||||
|
// actually called from the media notification controller (or a fake of it).
|
||||||
|
if (mediaSession.isMediaNotificationController(controller)) {
|
||||||
|
return Futures.immediateFuture(
|
||||||
|
new MediaSession.MediaItemsWithStartPosition(
|
||||||
|
mediaItems,
|
||||||
|
/* startIndex= */ 0,
|
||||||
|
/* startPositionMs= */ C.TIME_UNSET));
|
||||||
|
}
|
||||||
|
return Futures.immediateFailedFuture(new UnsupportedOperationException());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
session.getPlayer().stop();
|
||||||
|
session.getPlayer().clearMediaItems();
|
||||||
|
session.getPlayer().release();
|
||||||
|
session.release();
|
||||||
|
session = null;
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user