diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d52ea09c2e..dd3202363b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -35,6 +35,13 @@ * Muxers: * IMA extension: * Session: + * Keep foreground service state for an additional 10 minutes when playback + pauses, stops or fails. This allows users to resume playback within this + timeout without risking foreground service restrictions on various + devices. Note that simply calling `player.pause()` can no longer be used + to stop the foreground service before `stopSelf()` when overriding + `onTaskRemoved`, use `MediaSessionService.pauseAllPlayersAndStopSelf()` + instead. * Keep notification visible when playback enters an error or stopped state. The notification is only removed if the playlist is cleared or the player is released. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java index fc4ccdce6d..d135c66634 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -26,6 +26,7 @@ import android.content.pm.ServiceInfo; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.os.Message; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationManagerCompat; @@ -52,14 +53,17 @@ import java.util.concurrent.TimeoutException; * *
All methods must be called on the main thread.
*/
-/* package */ final class MediaNotificationManager {
+/* package */ final class MediaNotificationManager implements Handler.Callback {
private static final String TAG = "MediaNtfMng";
+ private static final int MSG_USER_ENGAGED_TIMEOUT = 1;
+ private static final long USER_ENGAGED_TIMEOUT_MS = 600_000;
private final MediaSessionService mediaSessionService;
private final MediaNotification.Provider mediaNotificationProvider;
private final MediaNotification.ActionFactory actionFactory;
private final NotificationManagerCompat notificationManagerCompat;
+ private final Handler mainHandler;
private final Executor mainExecutor;
private final Intent startSelfIntent;
private final Map It is only possible to terminate the service with {@link #stopSelf()} if this method returns
+ * {@code false}.
+ *
+ * Note that sessions are kept in foreground and this method returns {@code true} for a period
+ * of 10 minutes after they paused, stopped or failed. Use {@link #pauseAllPlayersAndStopSelf()}
+ * to pause all ongoing playbacks immediately and terminate the service.
*/
@UnstableApi
public boolean isPlaybackOngoing() {
@@ -480,13 +488,15 @@ public abstract class MediaSessionService extends Service {
}
/**
- * Pauses the player of each session managed by the service and calls {@link #stopSelf()}.
+ * Pauses the player of each session managed by the service, ensures the foreground service is
+ * stopped, and calls {@link #stopSelf()}.
*
* This terminates the service lifecycle and triggers {@link #onDestroy()} that an app can
* override to release the sessions and other resources.
*/
@UnstableApi
public void pauseAllPlayersAndStopSelf() {
+ getMediaNotificationManager().disableUserEngagedTimeout();
List If {@linkplain #isPlaybackOngoing() playback is ongoing}, the service continues running in
- * the foreground when the app is dismissed from the recent apps. Otherwise, the service is
- * stopped by calling {@link #stopSelf()} which terminates the service lifecycle and triggers
- * {@link #onDestroy()} that an app can override to release the sessions and other resources.
+ * This method can be overridden to customize the behavior of when the app is dismissed from
+ * the recent apps.
+ *
+ * The default behavior is that if {@linkplain #isPlaybackOngoing() playback is ongoing}, which
+ * means the service is already running in the foreground, and at least one media session {@link
+ * Player#isPlaying() is playing}, the service is kept running. Otherwise, playbacks are paused
+ * and the service is stopped by calling {@link #pauseAllPlayersAndStopSelf()} which terminates
+ * the service lifecycle and triggers {@link #onDestroy()} that an app can override to release the
+ * sessions and other resources.
*
* An app can safely override this method without calling super to implement a different
* behaviour, for instance unconditionally calling {@link #pauseAllPlayersAndStopSelf()} to stop
@@ -519,10 +534,10 @@ public abstract class MediaSessionService extends Service {
*/
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
- if (!isPlaybackOngoing()) {
- // The service needs to be stopped when playback is not ongoing and the service is not in the
- // foreground.
- stopSelf();
+ if (!isPlaybackOngoing() || !isAnySessionPlaying()) {
+ // The service needs to be stopped when playback is not ongoing (i.e, the service is not in
+ // the foreground). It is also force-stopped if no session is playing.
+ pauseAllPlayersAndStopSelf();
}
}
@@ -619,7 +634,7 @@ public abstract class MediaSessionService extends Service {
MediaSession session, boolean startInForegroundWhenPaused) {
try {
boolean startInForegroundRequired =
- getMediaNotificationManager().shouldRunInForeground(session, startInForegroundWhenPaused);
+ getMediaNotificationManager().shouldRunInForeground(startInForegroundWhenPaused);
onUpdateNotification(session, startInForegroundRequired);
} catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) {
if ((Util.SDK_INT >= 31) && Api31.instanceOfForegroundServiceStartNotAllowedException(e)) {
@@ -674,6 +689,16 @@ public abstract class MediaSessionService extends Service {
});
}
+ private boolean isAnySessionPlaying() {
+ List