From 16a185de1d42b049608c128bc203ea968234864a Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 16 Jan 2019 20:18:42 +0000 Subject: [PATCH] make PlayerNotificationListener better suited for foreground services Issue: #5301 Issue: #4988 Issue: #4813 Issue: #5344 Issue: #5117 PiperOrigin-RevId: 229603354 --- RELEASENOTES.md | 2 + .../ui/PlayerNotificationManager.java | 183 +++++++++++------- 2 files changed, 111 insertions(+), 74 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1150edcf8c..96d1fb1ff3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,8 @@ * Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a callback `Runnable`. * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. +* Change signature of `PlayerNotificationManager.NotificationListener` to better + fit service requirements. Remove ability to set a custom stop action. ### 2.9.4 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 4d6b83ccae..103534d8ca 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -58,7 +58,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * player state. * *

The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or - * when an intent with action {@link #ACTION_STOP} is received. + * when the notification is dismissed by the user. * *

If the player is released it must be removed from the manager by calling {@code * setPlayer(null)} which will cancel the notification. @@ -72,11 +72,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * are displayed. *

- *
  • {@code stopAction} - Sets which stop action should be used. If set to null, the stop - * action is not displayed. + *
  • {@code usePlayPauseActions} - Sets whether the play and pause actions are displayed. * + *
  • {@code useStopAction} - Sets whether the stop action is displayed. + * *
  • {@code rewindIncrementMs} - Sets the rewind increment. If set to zero the rewind * action is not displayed. @@ -87,7 +93,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *
  • {@code fastForwardIncrementMs} - Sets the fast forward increment. If set to zero the * fast forward action is not included in the notification. * * @@ -195,7 +201,7 @@ public class PlayerNotificationManager { void onCustomAction(Player player, String action, Intent intent); } - /** A listener for start and cancellation of the notification. */ + /** A listener for changes to the notification. */ public interface NotificationListener { /** @@ -203,15 +209,41 @@ public class PlayerNotificationManager { * * @param notificationId The id with which the notification has been posted. * @param notification The {@link Notification}. + * @deprecated Use {@link #onNotificationPosted(int, Notification, boolean)} instead. */ - void onNotificationStarted(int notificationId, Notification notification); + @Deprecated + default void onNotificationStarted(int notificationId, Notification notification) {} /** * Called after the notification has been cancelled. * * @param notificationId The id of the notification which has been cancelled. + * @deprecated Use {@link #onNotificationCancelled(int, boolean)}. */ - void onNotificationCancelled(int notificationId); + @Deprecated + default void onNotificationCancelled(int notificationId) {} + + /** + * Called after the notification has been cancelled. + * + * @param notificationId The id of the notification which has been cancelled. + * @param dismissedByUser {@code true} if the notification is cancelled because the user + * dismissed the notification. + */ + default void onNotificationCancelled(int notificationId, boolean dismissedByUser) {} + + /** + * Called each time after the notification has been posted. + * + *

    The {@code isPlayerActive} flag indicates whether a service in which the player may run + * needs to be in the foreground. + * + * @param notificationId The id of the notification which has been posted. + * @param notification The {@link Notification}. + * @param isPlayerActive {@code true} if the player is active. + */ + default void onNotificationPosted( + int notificationId, Notification notification, boolean isPlayerActive) {} } /** Receives a {@link Bitmap}. */ @@ -235,7 +267,7 @@ public class PlayerNotificationManager { if (player != null && notificationTag == currentNotificationTag && isNotificationStarted) { - updateNotification(bitmap); + startOrUpdateNotification(bitmap); } }); } @@ -254,10 +286,15 @@ public class PlayerNotificationManager { public static final String ACTION_FAST_FORWARD = "com.google.android.exoplayer.ffwd"; /** The action which rewinds. */ public static final String ACTION_REWIND = "com.google.android.exoplayer.rewind"; - /** The action which cancels the notification and stops playback. */ + /** The action which stops playback. */ public static final String ACTION_STOP = "com.google.android.exoplayer.stop"; /** The extra key of the instance id of the player notification manager. */ public static final String EXTRA_INSTANCE_ID = "INSTANCE_ID"; + /** + * The action which is executed when the notification is dismissed. It cancels the notification + * and calls {@link NotificationListener#onNotificationCancelled(int, boolean)}. + */ + private static final String ACTION_DISMISS = "com.google.android.exoplayer.dismiss"; /** * Visibility of notification on the lock screen. One of {@link @@ -311,6 +348,7 @@ public class PlayerNotificationManager { private final NotificationBroadcastReceiver notificationBroadcastReceiver; private final Map playbackActions; private final Map customActions; + private final PendingIntent dismissPendingIntent; private final int instanceId; private final Timeline.Window window; @@ -323,8 +361,7 @@ public class PlayerNotificationManager { private @Nullable MediaSessionCompat.Token mediaSessionToken; private boolean useNavigationActions; private boolean usePlayPauseActions; - private @Nullable String stopAction; - private @Nullable PendingIntent stopPendingIntent; + private boolean useStopAction; private long fastForwardMs; private long rewindMs; private int badgeIconType; @@ -519,7 +556,6 @@ public class PlayerNotificationManager { priority = NotificationCompat.PRIORITY_LOW; fastForwardMs = DEFAULT_FAST_FORWARD_MS; rewindMs = DEFAULT_REWIND_MS; - stopAction = ACTION_STOP; badgeIconType = NotificationCompat.BADGE_ICON_SMALL; visibility = NotificationCompat.VISIBILITY_PUBLIC; @@ -535,7 +571,8 @@ public class PlayerNotificationManager { for (String action : customActions.keySet()) { intentFilter.addAction(action); } - stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent; + dismissPendingIntent = createBroadcastIntent(ACTION_DISMISS, context, instanceId); + intentFilter.addAction(ACTION_DISMISS); } /** @@ -562,7 +599,7 @@ public class PlayerNotificationManager { if (this.player != null) { this.player.removeListener(playerListener); if (player == null) { - stopNotification(); + stopNotification(/* dismissedByUser= */ false); } } this.player = player; @@ -570,9 +607,7 @@ public class PlayerNotificationManager { wasPlayWhenReady = player.getPlayWhenReady(); lastPlaybackState = player.getPlaybackState(); player.addListener(playerListener); - if (lastPlaybackState != Player.STATE_IDLE) { - startOrUpdateNotification(); - } + startOrUpdateNotification(); } } @@ -664,24 +699,15 @@ public class PlayerNotificationManager { } /** - * Sets the name of the action to be used as stop action to cancel the notification. If {@code - * null} is passed the stop action is not displayed. + * Sets whether the stop action should be used. * - * @param stopAction The name of the stop action which must be {@link #ACTION_STOP} or an action - * provided by the {@link CustomActionReceiver}. {@code null} to omit the stop action. + * @param useStopAction Whether to use the stop action. */ - public final void setStopAction(@Nullable String stopAction) { - if (Util.areEqual(stopAction, this.stopAction)) { + public final void setUseStopAction(boolean useStopAction) { + if (this.useStopAction == useStopAction) { return; } - this.stopAction = stopAction; - if (ACTION_STOP.equals(stopAction)) { - stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent; - } else if (stopAction != null) { - stopPendingIntent = Assertions.checkNotNull(customActions.get(stopAction)).actionIntent; - } else { - stopPendingIntent = null; - } + this.useStopAction = useStopAction; invalidate(); } @@ -864,36 +890,50 @@ public class PlayerNotificationManager { /** Forces an update of the notification if already started. */ public void invalidate() { if (isNotificationStarted && player != null) { - updateNotification(null); + startOrUpdateNotification(); } } + @Nullable + private Notification startOrUpdateNotification() { + return player != null ? startOrUpdateNotification(/* bitmap= */ null) : null; + } + @RequiresNonNull("player") - private Notification updateNotification(@Nullable Bitmap bitmap) { + @Nullable + private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) { Notification notification = createNotification(player, bitmap); + if (notification == null) { + stopNotification(/* dismissedByUser= */ false); + return null; + } notificationManager.notify(notificationId, notification); + if (!isNotificationStarted) { + isNotificationStarted = true; + context.registerReceiver(notificationBroadcastReceiver, intentFilter); + if (notificationListener != null) { + notificationListener.onNotificationStarted(notificationId, notification); + } + } + NotificationListener listener = notificationListener; + Player player = this.player; + if (listener != null && player != null) { + boolean isPlayerActive = + player.getPlayWhenReady() && player.getPlaybackState() != Player.STATE_IDLE; + listener.onNotificationPosted(notificationId, notification, isPlayerActive); + } return notification; } - private void startOrUpdateNotification() { - if (player != null) { - Notification notification = updateNotification(null); - if (!isNotificationStarted) { - isNotificationStarted = true; - context.registerReceiver(notificationBroadcastReceiver, intentFilter); - if (notificationListener != null) { - notificationListener.onNotificationStarted(notificationId, notification); - } - } - } - } - - private void stopNotification() { + private void stopNotification(boolean dismissedByUser) { if (isNotificationStarted) { - notificationManager.cancel(notificationId); + if (!dismissedByUser) { + notificationManager.cancel(notificationId); + } isNotificationStarted = false; context.unregisterReceiver(notificationBroadcastReceiver); if (notificationListener != null) { + notificationListener.onNotificationCancelled(notificationId, dismissedByUser); notificationListener.onNotificationCancelled(notificationId); } } @@ -904,9 +944,14 @@ public class PlayerNotificationManager { * * @param player The player for which state to build a notification. * @param largeIcon The large icon to be used. - * @return The {@link Notification} which has been built. + * @return The {@link Notification} which has been built, or {@code null} if no notification + * should be displayed. */ + @Nullable protected Notification createNotification(Player player, @Nullable Bitmap largeIcon) { + if (player.getPlaybackState() == Player.STATE_IDLE) { + return null; + } NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); List actionNames = getActions(player); for (int i = 0; i < actionNames.size(); i++) { @@ -925,14 +970,13 @@ public class PlayerNotificationManager { mediaStyle.setMediaSession(mediaSessionToken); } mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(actionNames, player)); - // Configure stop action (eg. when user dismisses the notification when !isOngoing). - boolean useStopAction = stopAction != null; - mediaStyle.setShowCancelButton(useStopAction); - if (useStopAction && stopPendingIntent != null) { - builder.setDeleteIntent(stopPendingIntent); - mediaStyle.setCancelButtonIntent(stopPendingIntent); - } + // Configure dismiss action prior to API 21 ('x' button). + mediaStyle.setShowCancelButton(true); + mediaStyle.setCancelButtonIntent(dismissPendingIntent); + // Set intent which is sent if the user selects 'clear all' + builder.setDeleteIntent(dismissPendingIntent); builder.setStyle(mediaStyle); + // Set notification properties from getters. builder .setBadgeIconType(badgeIconType) @@ -1030,8 +1074,8 @@ public class PlayerNotificationManager { if (customActionReceiver != null) { stringActions.addAll(customActionReceiver.getCustomActions(player)); } - if (ACTION_STOP.equals(stopAction)) { - stringActions.add(stopAction); + if (useStopAction) { + stringActions.add(ACTION_STOP); } return stringActions; } @@ -1176,27 +1220,20 @@ public class PlayerNotificationManager { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if ((wasPlayWhenReady != playWhenReady && playbackState != Player.STATE_IDLE) - || lastPlaybackState != playbackState) { + if (wasPlayWhenReady != playWhenReady || lastPlaybackState != playbackState) { startOrUpdateNotification(); + wasPlayWhenReady = playWhenReady; + lastPlaybackState = playbackState; } - wasPlayWhenReady = playWhenReady; - lastPlaybackState = playbackState; } @Override public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { - if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { - return; - } startOrUpdateNotification(); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { - return; - } startOrUpdateNotification(); } @@ -1207,9 +1244,6 @@ public class PlayerNotificationManager { @Override public void onRepeatModeChanged(int repeatMode) { - if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { - return; - } startOrUpdateNotification(); } } @@ -1245,8 +1279,9 @@ public class PlayerNotificationManager { } else if (ACTION_NEXT.equals(action)) { next(player); } else if (ACTION_STOP.equals(action)) { - controlDispatcher.dispatchStop(player, true); - stopNotification(); + controlDispatcher.dispatchStop(player, /* reset= */ true); + } else if (ACTION_DISMISS.equals(action)) { + stopNotification(/* dismissedByUser= */ true); } else if (customActionReceiver != null && customActions.containsKey(action)) { customActionReceiver.onCustomAction(player, action, intent); }