make PlayerNotificationListener better suited for foreground services

Issue: #5301
Issue: #4988
Issue: #4813
Issue: #5344
Issue: #5117
PiperOrigin-RevId: 229603354
This commit is contained in:
bachinger 2019-01-16 20:18:42 +00:00 committed by Oliver Woodman
parent ec77f737ee
commit 16a185de1d
2 changed files with 111 additions and 74 deletions

View File

@ -37,6 +37,8 @@
* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a * Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a
callback `Runnable`. callback `Runnable`.
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * 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 ### ### 2.9.4 ###

View File

@ -58,7 +58,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* player state. * player state.
* *
* <p>The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or * <p>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.
* *
* <p>If the player is released it must be removed from the manager by calling {@code * <p>If the player is released it must be removed from the manager by calling {@code
* setPlayer(null)} which will cancel the notification. * setPlayer(null)} which will cancel the notification.
@ -72,11 +72,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* are displayed. * are displayed.
* <ul> * <ul>
* <li>Corresponding setter: {@link #setUseNavigationActions(boolean)} * <li>Corresponding setter: {@link #setUseNavigationActions(boolean)}
* <li>Default: {@code true}
* </ul> * </ul>
* <li><b>{@code stopAction}</b> - Sets which stop action should be used. If set to null, the stop * <li><b>{@code usePlayPauseActions}</b> - Sets whether the play and pause actions are displayed.
* action is not displayed.
* <ul> * <ul>
* <li>Corresponding setter: {@link #setStopAction(String)}} * <li>Corresponding setter: {@link #setUsePlayPauseActions(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code useStopAction}</b> - Sets whether the stop action is displayed.
* <ul>
* <li>Corresponding setter: {@link #setUseStopAction(boolean)}
* <li>Default: {@code false}
* </ul> * </ul>
* <li><b>{@code rewindIncrementMs}</b> - Sets the rewind increment. If set to zero the rewind * <li><b>{@code rewindIncrementMs}</b> - Sets the rewind increment. If set to zero the rewind
* action is not displayed. * action is not displayed.
@ -87,7 +93,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* <li><b>{@code fastForwardIncrementMs}</b> - Sets the fast forward increment. If set to zero the * <li><b>{@code fastForwardIncrementMs}</b> - Sets the fast forward increment. If set to zero the
* fast forward action is not included in the notification. * fast forward action is not included in the notification.
* <ul> * <ul>
* <li>Corresponding setter: {@link #setFastForwardIncrementMs(long)}} * <li>Corresponding setter: {@link #setFastForwardIncrementMs(long)}
* <li>Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000) * <li>Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000)
* </ul> * </ul>
* </ul> * </ul>
@ -195,7 +201,7 @@ public class PlayerNotificationManager {
void onCustomAction(Player player, String action, Intent intent); 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 { public interface NotificationListener {
/** /**
@ -203,15 +209,41 @@ public class PlayerNotificationManager {
* *
* @param notificationId The id with which the notification has been posted. * @param notificationId The id with which the notification has been posted.
* @param notification The {@link Notification}. * @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. * Called after the notification has been cancelled.
* *
* @param notificationId The id of the notification which 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.
*
* <p>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}. */ /** Receives a {@link Bitmap}. */
@ -235,7 +267,7 @@ public class PlayerNotificationManager {
if (player != null if (player != null
&& notificationTag == currentNotificationTag && notificationTag == currentNotificationTag
&& isNotificationStarted) { && isNotificationStarted) {
updateNotification(bitmap); startOrUpdateNotification(bitmap);
} }
}); });
} }
@ -254,10 +286,15 @@ public class PlayerNotificationManager {
public static final String ACTION_FAST_FORWARD = "com.google.android.exoplayer.ffwd"; public static final String ACTION_FAST_FORWARD = "com.google.android.exoplayer.ffwd";
/** The action which rewinds. */ /** The action which rewinds. */
public static final String ACTION_REWIND = "com.google.android.exoplayer.rewind"; 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"; public static final String ACTION_STOP = "com.google.android.exoplayer.stop";
/** The extra key of the instance id of the player notification manager. */ /** The extra key of the instance id of the player notification manager. */
public static final String EXTRA_INSTANCE_ID = "INSTANCE_ID"; 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 * Visibility of notification on the lock screen. One of {@link
@ -311,6 +348,7 @@ public class PlayerNotificationManager {
private final NotificationBroadcastReceiver notificationBroadcastReceiver; private final NotificationBroadcastReceiver notificationBroadcastReceiver;
private final Map<String, NotificationCompat.Action> playbackActions; private final Map<String, NotificationCompat.Action> playbackActions;
private final Map<String, NotificationCompat.Action> customActions; private final Map<String, NotificationCompat.Action> customActions;
private final PendingIntent dismissPendingIntent;
private final int instanceId; private final int instanceId;
private final Timeline.Window window; private final Timeline.Window window;
@ -323,8 +361,7 @@ public class PlayerNotificationManager {
private @Nullable MediaSessionCompat.Token mediaSessionToken; private @Nullable MediaSessionCompat.Token mediaSessionToken;
private boolean useNavigationActions; private boolean useNavigationActions;
private boolean usePlayPauseActions; private boolean usePlayPauseActions;
private @Nullable String stopAction; private boolean useStopAction;
private @Nullable PendingIntent stopPendingIntent;
private long fastForwardMs; private long fastForwardMs;
private long rewindMs; private long rewindMs;
private int badgeIconType; private int badgeIconType;
@ -519,7 +556,6 @@ public class PlayerNotificationManager {
priority = NotificationCompat.PRIORITY_LOW; priority = NotificationCompat.PRIORITY_LOW;
fastForwardMs = DEFAULT_FAST_FORWARD_MS; fastForwardMs = DEFAULT_FAST_FORWARD_MS;
rewindMs = DEFAULT_REWIND_MS; rewindMs = DEFAULT_REWIND_MS;
stopAction = ACTION_STOP;
badgeIconType = NotificationCompat.BADGE_ICON_SMALL; badgeIconType = NotificationCompat.BADGE_ICON_SMALL;
visibility = NotificationCompat.VISIBILITY_PUBLIC; visibility = NotificationCompat.VISIBILITY_PUBLIC;
@ -535,7 +571,8 @@ public class PlayerNotificationManager {
for (String action : customActions.keySet()) { for (String action : customActions.keySet()) {
intentFilter.addAction(action); 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) { if (this.player != null) {
this.player.removeListener(playerListener); this.player.removeListener(playerListener);
if (player == null) { if (player == null) {
stopNotification(); stopNotification(/* dismissedByUser= */ false);
} }
} }
this.player = player; this.player = player;
@ -570,9 +607,7 @@ public class PlayerNotificationManager {
wasPlayWhenReady = player.getPlayWhenReady(); wasPlayWhenReady = player.getPlayWhenReady();
lastPlaybackState = player.getPlaybackState(); lastPlaybackState = player.getPlaybackState();
player.addListener(playerListener); 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 * Sets whether the stop action should be used.
* null} is passed the stop action is not displayed.
* *
* @param stopAction The name of the stop action which must be {@link #ACTION_STOP} or an action * @param useStopAction Whether to use the stop action.
* provided by the {@link CustomActionReceiver}. {@code null} to omit the stop action.
*/ */
public final void setStopAction(@Nullable String stopAction) { public final void setUseStopAction(boolean useStopAction) {
if (Util.areEqual(stopAction, this.stopAction)) { if (this.useStopAction == useStopAction) {
return; return;
} }
this.stopAction = stopAction; this.useStopAction = useStopAction;
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;
}
invalidate(); invalidate();
} }
@ -864,36 +890,50 @@ public class PlayerNotificationManager {
/** Forces an update of the notification if already started. */ /** Forces an update of the notification if already started. */
public void invalidate() { public void invalidate() {
if (isNotificationStarted && player != null) { if (isNotificationStarted && player != null) {
updateNotification(null); startOrUpdateNotification();
} }
} }
@Nullable
private Notification startOrUpdateNotification() {
return player != null ? startOrUpdateNotification(/* bitmap= */ null) : null;
}
@RequiresNonNull("player") @RequiresNonNull("player")
private Notification updateNotification(@Nullable Bitmap bitmap) { @Nullable
private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) {
Notification notification = createNotification(player, bitmap); Notification notification = createNotification(player, bitmap);
if (notification == null) {
stopNotification(/* dismissedByUser= */ false);
return null;
}
notificationManager.notify(notificationId, notification); 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; return notification;
} }
private void startOrUpdateNotification() { private void stopNotification(boolean dismissedByUser) {
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() {
if (isNotificationStarted) { if (isNotificationStarted) {
notificationManager.cancel(notificationId); if (!dismissedByUser) {
notificationManager.cancel(notificationId);
}
isNotificationStarted = false; isNotificationStarted = false;
context.unregisterReceiver(notificationBroadcastReceiver); context.unregisterReceiver(notificationBroadcastReceiver);
if (notificationListener != null) { if (notificationListener != null) {
notificationListener.onNotificationCancelled(notificationId, dismissedByUser);
notificationListener.onNotificationCancelled(notificationId); notificationListener.onNotificationCancelled(notificationId);
} }
} }
@ -904,9 +944,14 @@ public class PlayerNotificationManager {
* *
* @param player The player for which state to build a notification. * @param player The player for which state to build a notification.
* @param largeIcon The large icon to be used. * @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) { protected Notification createNotification(Player player, @Nullable Bitmap largeIcon) {
if (player.getPlaybackState() == Player.STATE_IDLE) {
return null;
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);
List<String> actionNames = getActions(player); List<String> actionNames = getActions(player);
for (int i = 0; i < actionNames.size(); i++) { for (int i = 0; i < actionNames.size(); i++) {
@ -925,14 +970,13 @@ public class PlayerNotificationManager {
mediaStyle.setMediaSession(mediaSessionToken); mediaStyle.setMediaSession(mediaSessionToken);
} }
mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(actionNames, player)); mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(actionNames, player));
// Configure stop action (eg. when user dismisses the notification when !isOngoing). // Configure dismiss action prior to API 21 ('x' button).
boolean useStopAction = stopAction != null; mediaStyle.setShowCancelButton(true);
mediaStyle.setShowCancelButton(useStopAction); mediaStyle.setCancelButtonIntent(dismissPendingIntent);
if (useStopAction && stopPendingIntent != null) { // Set intent which is sent if the user selects 'clear all'
builder.setDeleteIntent(stopPendingIntent); builder.setDeleteIntent(dismissPendingIntent);
mediaStyle.setCancelButtonIntent(stopPendingIntent);
}
builder.setStyle(mediaStyle); builder.setStyle(mediaStyle);
// Set notification properties from getters. // Set notification properties from getters.
builder builder
.setBadgeIconType(badgeIconType) .setBadgeIconType(badgeIconType)
@ -1030,8 +1074,8 @@ public class PlayerNotificationManager {
if (customActionReceiver != null) { if (customActionReceiver != null) {
stringActions.addAll(customActionReceiver.getCustomActions(player)); stringActions.addAll(customActionReceiver.getCustomActions(player));
} }
if (ACTION_STOP.equals(stopAction)) { if (useStopAction) {
stringActions.add(stopAction); stringActions.add(ACTION_STOP);
} }
return stringActions; return stringActions;
} }
@ -1176,27 +1220,20 @@ public class PlayerNotificationManager {
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if ((wasPlayWhenReady != playWhenReady && playbackState != Player.STATE_IDLE) if (wasPlayWhenReady != playWhenReady || lastPlaybackState != playbackState) {
|| lastPlaybackState != playbackState) {
startOrUpdateNotification(); startOrUpdateNotification();
wasPlayWhenReady = playWhenReady;
lastPlaybackState = playbackState;
} }
wasPlayWhenReady = playWhenReady;
lastPlaybackState = playbackState;
} }
@Override @Override
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return;
}
startOrUpdateNotification(); startOrUpdateNotification();
} }
@Override @Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return;
}
startOrUpdateNotification(); startOrUpdateNotification();
} }
@ -1207,9 +1244,6 @@ public class PlayerNotificationManager {
@Override @Override
public void onRepeatModeChanged(int repeatMode) { public void onRepeatModeChanged(int repeatMode) {
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return;
}
startOrUpdateNotification(); startOrUpdateNotification();
} }
} }
@ -1245,8 +1279,9 @@ public class PlayerNotificationManager {
} else if (ACTION_NEXT.equals(action)) { } else if (ACTION_NEXT.equals(action)) {
next(player); next(player);
} else if (ACTION_STOP.equals(action)) { } else if (ACTION_STOP.equals(action)) {
controlDispatcher.dispatchStop(player, true); controlDispatcher.dispatchStop(player, /* reset= */ true);
stopNotification(); } else if (ACTION_DISMISS.equals(action)) {
stopNotification(/* dismissedByUser= */ true);
} else if (customActionReceiver != null && customActions.containsKey(action)) { } else if (customActionReceiver != null && customActions.containsKey(action)) {
customActionReceiver.onCustomAction(player, action, intent); customActionReceiver.onCustomAction(player, action, intent);
} }