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 fastForwardIncrementMs} - Sets the fast forward increment. If set to zero the
* fast forward action is not included in the notification.
*
- * - Corresponding setter: {@link #setFastForwardIncrementMs(long)}}
+ *
- Corresponding setter: {@link #setFastForwardIncrementMs(long)}
*
- Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000)
*
*
@@ -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);
}