From 1b15d5c370379a2a33f2a20681626e3975e26e38 Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 9 May 2022 12:26:50 +0100 Subject: [PATCH] MediaSessionService: allow apps to opt-out from notifications Issue: androidx/media#50 PiperOrigin-RevId: 447435259 --- .../session/MediaNotificationManager.java | 42 +++++++------ .../media3/session/MediaSessionService.java | 60 +++++++++++++++---- 2 files changed, 73 insertions(+), 29 deletions(-) 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 397d0e2bcb..6376ba641d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -73,7 +73,7 @@ import java.util.concurrent.TimeoutException; if (controllerMap.containsKey(session)) { return; } - MediaControllerListener listener = new MediaControllerListener(session); + MediaControllerListener listener = new MediaControllerListener(mediaSessionService, session); ListenableFuture controllerFuture = new MediaController.Builder(mediaSessionService, session.getToken()) .setListener(listener) @@ -118,7 +118,7 @@ import java.util.concurrent.TimeoutException; } } - private void updateNotification(MediaSession session) { + public void updateNotification(MediaSession session) { @Nullable ListenableFuture controllerFuture = controllerMap.get(session); if (controllerFuture == null) { return; @@ -132,6 +132,11 @@ import java.util.concurrent.TimeoutException; throw new IllegalStateException(e); } + if (!mediaSessionService.isSessionAdded(session) || !canStartPlayback(session.getPlayer())) { + maybeStopForegroundService(/* removeNotifications= */ true); + return; + } + int notificationSequence = ++totalNotificationCount; MediaNotification.Provider.Callback callback = notification -> @@ -140,17 +145,18 @@ import java.util.concurrent.TimeoutException; MediaNotification mediaNotification = this.mediaNotificationProvider.createNotification(mediaController, actionFactory, callback); - updateNotification(session, mediaNotification); + updateNotificationInternal(session, mediaNotification); } private void onNotificationUpdated( int notificationSequence, MediaSession session, MediaNotification mediaNotification) { if (notificationSequence == totalNotificationCount) { - updateNotification(session, mediaNotification); + updateNotificationInternal(session, mediaNotification); } } - private void updateNotification(MediaSession session, MediaNotification mediaNotification) { + private void updateNotificationInternal( + MediaSession session, MediaNotification mediaNotification) { if (Util.SDK_INT >= 21) { // Call Notification.MediaStyle#setMediaSession() indirectly. android.media.session.MediaSession.Token fwkToken = @@ -188,7 +194,7 @@ import java.util.concurrent.TimeoutException; } } // To hide the notification on all API levels, we need to call both Service.stopForeground(true) - // and notificationManagerCompat.cancelAll(). For pre-L devices, we must also call + // and notificationManagerCompat.cancel(notificationId). For pre-L devices, we must also call // Service.stopForeground(true) anyway as a workaround that prevents the media notification from // being undismissable. mediaSessionService.stopForeground(removeNotifications || Util.SDK_INT < 21); @@ -209,40 +215,40 @@ import java.util.concurrent.TimeoutException; return player.getPlaybackState() != Player.STATE_IDLE && !player.getCurrentTimeline().isEmpty(); } - private final class MediaControllerListener implements MediaController.Listener, Player.Listener { + private static final class MediaControllerListener + implements MediaController.Listener, Player.Listener { + private final MediaSessionService mediaSessionService; private final MediaSession session; - public MediaControllerListener(MediaSession session) { + public MediaControllerListener(MediaSessionService mediaSessionService, MediaSession session) { + this.mediaSessionService = mediaSessionService; this.session = session; } public void onConnected() { if (canStartPlayback(session.getPlayer())) { - updateNotification(session); + // We need to present a notification. + mediaSessionService.onUpdateNotification(session); } } @Override public void onEvents(Player player, Player.Events events) { - if (!canStartPlayback(player)) { - maybeStopForegroundService(/* removeNotifications= */ true); - return; - } - - // Limit the events on which we may update the notification to ensure we don't update the - // notification too frequently, otherwise the system may suppress notifications. + // We must limit the frequency of notification updates, otherwise the system may suppress + // them. if (events.containsAny( Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_MEDIA_METADATA_CHANGED)) { - updateNotification(session); + mediaSessionService.onUpdateNotification(session); } } @Override public void onDisconnected(MediaController controller) { mediaSessionService.removeSession(session); - maybeStopForegroundService(/* removeNotifications= */ true); + // We may need to hide the notification. + mediaSessionService.onUpdateNotification(session); } } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java index f2a0ca9fc3..e6fd108f64 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -94,14 +94,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * controller. If it's accepted, the controller will be available and keep the binding. If it's * rejected, the controller will unbind. * - *

When a playback is started on the service, the service will obtain a {@link MediaNotification} - * from the {@link MediaNotification.Provider} that's set with {@link #setMediaNotificationProvider} - * (or {@link DefaultMediaNotificationProvider}, if no provider is set), and the service will become - * a foreground - * service. It's required to keep the playback after the controller is destroyed. The service - * will become a background service when all playbacks are stopped. Apps targeting {@code SDK_INT >= - * 28} must request the permission, {@link android.Manifest.permission#FOREGROUND_SERVICE}, in order - * to make the service foreground. + *

{@link #onUpdateNotification(MediaSession)} will be called whenever a notification needs to be + * shown, updated or cancelled. The default implementation will display notifications using a + * default UI or using a {@link MediaNotification.Provider} that's set with {@link + * #setMediaNotificationProvider}. In addition, when playback starts, the service will become a foreground service. + * It's required to keep the playback after the controller is destroyed. The service will become a + * background service when all playbacks are stopped. Apps targeting {@code SDK_INT >= 28} must + * request the permission, {@link android.Manifest.permission#FOREGROUND_SERVICE}, in order to make + * the service foreground. You can control when to show or hide notifications by overriding {@link + * #onUpdateNotification(MediaSession)}. In this case, you must also start or stop the service from + * the foreground, when playback starts or stops respectively. * *

The service will be destroyed when all sessions are closed, or no controller is binding to the * service while the service is in the background. @@ -263,6 +266,16 @@ public abstract class MediaSessionService extends Service { } } + /** + * Returns whether {@code session} has been added to this service via {@link #addSession} or + * {@link #onGetSession(ControllerInfo)}. + */ + public final boolean isSessionAdded(MediaSession session) { + synchronized (lock) { + return sessions.containsKey(session.getId()); + } + } + /** * Called when a component is about to bind to the service. * @@ -373,12 +386,37 @@ public abstract class MediaSessionService extends Service { } } + /** + * Called when a notification needs to be updated. Override this method to show or cancel your own + * notifications. + * + *

This method is called whenever the service has detected a change that requires to show, + * update or cancel a notification. The method will be called on the application thread of the app + * that the service belongs to. + * + *

Override this method to create your own notification and customize the foreground handling + * of your service. + * + *

The default implementation will present a default notification or the notification provided + * by the {@link MediaNotification.Provider} that is {@link + * #setMediaNotificationProvider(MediaNotification.Provider) set} by the app. Further, the service + * is started in the foreground when + * playback is ongoing and put back into background otherwise. + * + *

Apps targeting {@code SDK_INT >= 28} must request the permission, {@link + * android.Manifest.permission#FOREGROUND_SERVICE}. + * + * @param session A session that needs notification update. + */ + public void onUpdateNotification(MediaSession session) { + getMediaNotificationManager().updateNotification(session); + } + /** * Sets the {@link MediaNotification.Provider} to customize notifications. * - *

This should be called before any session is attached to this service via {@link - * #onGetSession(ControllerInfo)} or {@link #addSession(MediaSession)}. Otherwise a default UX - * will be shown with {@link DefaultMediaNotificationProvider}. + *

This should be called before {@link #onCreate()} returns. */ @UnstableApi protected final void setMediaNotificationProvider(