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
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 ###

View File

@ -58,7 +58,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* player state.
*
* <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
* setPlayer(null)} which will cancel the notification.
@ -72,11 +72,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* are displayed.
* <ul>
* <li>Corresponding setter: {@link #setUseNavigationActions(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code stopAction}</b> - Sets which stop action should be used. If set to null, the stop
* action is not displayed.
* <li><b>{@code usePlayPauseActions}</b> - Sets whether the play and pause actions are displayed.
* <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>
* <li><b>{@code rewindIncrementMs}</b> - 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;
* <li><b>{@code fastForwardIncrementMs}</b> - Sets the fast forward increment. If set to zero the
* fast forward action is not included in the notification.
* <ul>
* <li>Corresponding setter: {@link #setFastForwardIncrementMs(long)}}
* <li>Corresponding setter: {@link #setFastForwardIncrementMs(long)}
* <li>Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000)
* </ul>
* </ul>
@ -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.
*
* <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}. */
@ -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<String, NotificationCompat.Action> playbackActions;
private final Map<String, NotificationCompat.Action> 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,11 +607,9 @@ public class PlayerNotificationManager {
wasPlayWhenReady = player.getPlayWhenReady();
lastPlaybackState = player.getPlaybackState();
player.addListener(playerListener);
if (lastPlaybackState != Player.STATE_IDLE) {
startOrUpdateNotification();
}
}
}
/**
* Sets the {@link PlaybackPreparer}.
@ -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,20 +890,24 @@ 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);
notificationManager.notify(notificationId, notification);
return notification;
if (notification == null) {
stopNotification(/* dismissedByUser= */ false);
return null;
}
private void startOrUpdateNotification() {
if (player != null) {
Notification notification = updateNotification(null);
notificationManager.notify(notificationId, notification);
if (!isNotificationStarted) {
isNotificationStarted = true;
context.registerReceiver(notificationBroadcastReceiver, intentFilter);
@ -885,15 +915,25 @@ public class PlayerNotificationManager {
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 stopNotification() {
private void stopNotification(boolean dismissedByUser) {
if (isNotificationStarted) {
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<String> 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;
}
}
@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);
}