Skip to content

Commit

Permalink
Add isPlaybackOngoing and stopMediaSessionService
Browse files Browse the repository at this point in the history
This API additions help an app to implement the lifecycle of a MediaSessionService
properly and in consistency with the `MediaSessionService` being in the foreground
or not.

Not properly implementing `onTaskRemoved` is the main reason for crashes and
confusion. This change provides `MediaSessionService` with a default
implementation that avoids crashes of the service. This default implementation
uses the new API provided with this change just as an app can do.

Issue: #1219
PiperOrigin-RevId: 621874838
  • Loading branch information
marcbaechinger authored and Copybara-Service committed Apr 4, 2024
1 parent f9ed303 commit 617f989
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 9 deletions.
9 changes: 9 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@
* Fix issue where `MediaMetadata` with just non-null `extras` is not
transmitted between media controllers and sessions
([#1176](https://github.com/androidx/media/issues/1176)).
* Add `MediaSessionService.isPlaybackOngoing()` to let apps query whether
the service needs to be stopped in `onTaskRemoved()`
([#1219](https://github.com/androidx/media/issues/1219)).
* Add `MediaSessionService.pauseAllPlayersAndStopSelf()` that conveniently
allows to pause playback of all sessions and call `stopSelf` to
terminate the lifecyce of the `MediaSessionService`.
* Override `MediaSessionService.onTaskRemoved(Intent)` to provide a safe
default implementation that keeps the service running in the foreground
if playback is ongoing or stops the service otherwise.
* UI:
* Fallback to include audio track language name if `Locale` cannot
identify a display name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.OptIn
Expand Down Expand Up @@ -90,13 +89,6 @@ open class DemoPlaybackService : MediaLibraryService() {
return mediaLibrarySession
}

override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaLibrarySession.player
if (!player.playWhenReady || player.mediaItemCount == 0) {
stopSelf()
}
}

// MediaSession.setSessionActivity
// MediaSessionService.clearListener
@OptIn(UnstableApi::class)
Expand Down Expand Up @@ -166,7 +158,7 @@ open class DemoPlaybackService : MediaLibraryService() {
NotificationChannel(
CHANNEL_ID,
getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT
NotificationManager.IMPORTANCE_DEFAULT,
)
notificationManagerCompat.createNotificationChannel(channel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.postOrRun;

import android.app.Activity;
import android.app.ForegroundServiceStartNotAllowedException;
import android.app.Service;
import android.content.ComponentName;
Expand Down Expand Up @@ -469,6 +470,62 @@ private static ControllerInfo createFallbackMediaButtonCaller(Intent mediaButton
/* connectionHints= */ Bundle.EMPTY);
}

/**
* Returns whether there is a session with ongoing playback that must be paused or stopped before
* being able to terminate the service by calling {@link #stopSelf()}.
*/
@UnstableApi
public boolean isPlaybackOngoing() {
return getMediaNotificationManager().isStartedInForeground();
}

/**
* Pauses the player of each session managed by the service and calls {@link #stopSelf()}.
*
* <p>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() {
List<MediaSession> sessionList = getSessions();
for (int i = 0; i < sessionList.size(); i++) {
sessionList.get(i).getPlayer().setPlayWhenReady(false);
}
stopSelf();
}

/**
* {@inheritDoc}
*
* <p>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.
*
* <p>An app can safely override this method without calling super to implement a different
* behaviour, for instance unconditionally calling {@link #pauseAllPlayersAndStopSelf()} to stop
* the service even when playing. However, if {@linkplain #isPlaybackOngoing() playback is not
* ongoing}, the service must be terminated otherwise the service will be crashed and restarted by
* the system.
*
* <p>Note: The service <a
* href="https://developer.android.com/develop/background-work/services/bound-services#Lifecycle">can't
* be stopped</a> until all media controllers have been unbound. Hence, an app needs to release
* all internal controllers that have connected to the service (for instance from an activity in
* {@link Activity#onStop()}). If an app allows external apps to connect a {@link MediaController}
* to the service, these controllers also need to be disconnected. In such a scenario of external
* bound clients, an app needs to override this method to release the session before calling
* {@link #stopSelf()}.
*/
@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();
}
}

/**
* Called when the service is no longer used and is being removed.
*
Expand Down

0 comments on commit 617f989

Please sign in to comment.