mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Remember explicit notification dismissal
Currently, a notification may be recreated even if a user dismissed it as long as the standard conditions for a notification are true. To avoid this effect, we plumb the dismissal event to the notification controller, so that it can override its `shouldShowNotification` decision. The plumbing sets an extra on the media key intent, which the session forwards as a custom event to the media notification controller if connected. Issue: androidx/media#2302 PiperOrigin-RevId: 745989590 (cherry picked from commit f672590b2deeffb06435eb542cfe0d0630894e92)
This commit is contained in:
parent
ce0c98c4d4
commit
157fd8a260
@ -65,6 +65,8 @@
|
||||
* Lower aggregation timeout for platform `MediaSession` callbacks from 500
|
||||
to 100 milliseconds and add an experimental setter to allow apps to
|
||||
configure this value.
|
||||
* Fix issue where notifications reappear after they have been dismissed by
|
||||
the user ([#2302](https://github.com/androidx/media/issues/2302)).
|
||||
* UI:
|
||||
* Enable `PlayerSurface` to work with `ExoPlayer.setVideoEffects` and
|
||||
`CompositionPlayer`.
|
||||
|
@ -113,10 +113,7 @@ import androidx.media3.common.util.Util;
|
||||
public PendingIntent createMediaActionPendingIntent(
|
||||
MediaSession mediaSession, @Player.Command long command) {
|
||||
int keyCode = toKeyCode(command);
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
||||
intent.setData(mediaSession.getImpl().getUri());
|
||||
intent.setComponent(new ComponentName(service, service.getClass()));
|
||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
Intent intent = getMediaButtonIntent(mediaSession, keyCode);
|
||||
if (Util.SDK_INT >= 26
|
||||
&& command == COMMAND_PLAY_PAUSE
|
||||
&& !mediaSession.getPlayer().getPlayWhenReady()) {
|
||||
@ -130,6 +127,26 @@ import androidx.media3.common.util.Util;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingIntent createNotificationDismissalIntent(MediaSession mediaSession) {
|
||||
Intent intent =
|
||||
getMediaButtonIntent(mediaSession, KEYCODE_MEDIA_STOP)
|
||||
.putExtra(MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY, true);
|
||||
return PendingIntent.getService(
|
||||
service,
|
||||
/* requestCode= */ KEYCODE_MEDIA_STOP,
|
||||
intent,
|
||||
Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
}
|
||||
|
||||
private Intent getMediaButtonIntent(MediaSession mediaSession, int mediaKeyCode) {
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
||||
intent.setData(mediaSession.getImpl().getUri());
|
||||
intent.setComponent(new ComponentName(service, service.getClass()));
|
||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, mediaKeyCode));
|
||||
return intent;
|
||||
}
|
||||
|
||||
private int toKeyCode(@Player.Command long action) {
|
||||
if (action == COMMAND_SEEK_TO_NEXT_MEDIA_ITEM || action == COMMAND_SEEK_TO_NEXT) {
|
||||
return KEYCODE_MEDIA_NEXT;
|
||||
|
@ -22,7 +22,6 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
|
||||
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
|
||||
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
|
||||
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
|
||||
import static androidx.media3.common.Player.COMMAND_STOP;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
|
||||
@ -379,8 +378,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
|
||||
Notification notification =
|
||||
builder
|
||||
.setContentIntent(mediaSession.getSessionActivity())
|
||||
.setDeleteIntent(
|
||||
actionFactory.createMediaActionPendingIntent(mediaSession, COMMAND_STOP))
|
||||
.setDeleteIntent(actionFactory.createNotificationDismissalIntent(mediaSession))
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(smallIconResourceId)
|
||||
.setStyle(mediaStyle)
|
||||
|
@ -31,6 +31,17 @@ import com.google.common.collect.ImmutableList;
|
||||
/** A notification for media playbacks. */
|
||||
public final class MediaNotification {
|
||||
|
||||
/**
|
||||
* Event key to indicate a media notification was dismissed.
|
||||
*
|
||||
* <p>This event key can be used as an extras key for a boolean extra on a media button pending
|
||||
* intent, and as as custom session command action to inform the media notification controller
|
||||
* that a notification was dismissed.
|
||||
*/
|
||||
@UnstableApi
|
||||
public static final String NOTIFICATION_DISMISSED_EVENT_KEY =
|
||||
"androidx.media3.session.NOTIFICATION_DISMISSED_EVENT_KEY";
|
||||
|
||||
/**
|
||||
* Creates {@linkplain NotificationCompat.Action actions} and {@linkplain PendingIntent pending
|
||||
* intents} for notifications.
|
||||
@ -99,10 +110,20 @@ public final class MediaNotification {
|
||||
* Creates a {@link PendingIntent} for a media action that will be handled by the library.
|
||||
*
|
||||
* @param mediaSession The media session to which the action will be sent.
|
||||
* @param command The intent's command.
|
||||
* @param command The {@link PendingIntent}.
|
||||
*/
|
||||
PendingIntent createMediaActionPendingIntent(
|
||||
MediaSession mediaSession, @Player.Command long command);
|
||||
|
||||
/**
|
||||
* Creates a {@link PendingIntent} triggered when the notification is dismissed.
|
||||
*
|
||||
* @param mediaSession The media session for which the intent is created.
|
||||
* @return The {@link PendingIntent}.
|
||||
*/
|
||||
default PendingIntent createNotificationDismissalIntent(MediaSession mediaSession) {
|
||||
return createMediaActionPendingIntent(mediaSession, Player.COMMAND_STOP);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,7 @@ package androidx.media3.session;
|
||||
|
||||
import static android.app.Service.STOP_FOREGROUND_DETACH;
|
||||
import static android.app.Service.STOP_FOREGROUND_REMOVE;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@ -65,7 +66,7 @@ import java.util.concurrent.TimeoutException;
|
||||
private final Handler mainHandler;
|
||||
private final Executor mainExecutor;
|
||||
private final Intent startSelfIntent;
|
||||
private final Map<MediaSession, ListenableFuture<MediaController>> controllerMap;
|
||||
private final Map<MediaSession, ControllerInfo> controllerMap;
|
||||
|
||||
private int totalNotificationCount;
|
||||
@Nullable private MediaNotification mediaNotification;
|
||||
@ -104,7 +105,7 @@ import java.util.concurrent.TimeoutException;
|
||||
.setListener(listener)
|
||||
.setApplicationLooper(Looper.getMainLooper())
|
||||
.buildAsync();
|
||||
controllerMap.put(session, controllerFuture);
|
||||
controllerMap.put(session, new ControllerInfo(controllerFuture));
|
||||
controllerFuture.addListener(
|
||||
() -> {
|
||||
try {
|
||||
@ -123,9 +124,9 @@ import java.util.concurrent.TimeoutException;
|
||||
}
|
||||
|
||||
public void removeSession(MediaSession session) {
|
||||
@Nullable ListenableFuture<MediaController> future = controllerMap.remove(session);
|
||||
if (future != null) {
|
||||
MediaController.releaseFuture(future);
|
||||
@Nullable ControllerInfo controllerInfo = controllerMap.remove(session);
|
||||
if (controllerInfo != null) {
|
||||
MediaController.releaseFuture(controllerInfo.controllerFuture);
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,19 +159,8 @@ import java.util.concurrent.TimeoutException;
|
||||
}
|
||||
|
||||
int notificationSequence = ++totalNotificationCount;
|
||||
MediaController mediaNotificationController = null;
|
||||
ListenableFuture<MediaController> controller = controllerMap.get(session);
|
||||
if (controller != null && controller.isDone()) {
|
||||
try {
|
||||
mediaNotificationController = Futures.getDone(controller);
|
||||
} catch (ExecutionException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
ImmutableList<CommandButton> mediaButtonPreferences =
|
||||
mediaNotificationController != null
|
||||
? mediaNotificationController.getMediaButtonPreferences()
|
||||
: ImmutableList.of();
|
||||
checkNotNull(getConnectedControllerForSession(session)).getMediaButtonPreferences();
|
||||
MediaNotification.Provider.Callback callback =
|
||||
notification ->
|
||||
mainExecutor.execute(
|
||||
@ -261,6 +251,13 @@ import java.util.concurrent.TimeoutException;
|
||||
}
|
||||
}
|
||||
|
||||
private void onNotificationDismissed(MediaSession session) {
|
||||
@Nullable ControllerInfo controllerInfo = controllerMap.get(session);
|
||||
if (controllerInfo != null) {
|
||||
controllerInfo.wasNotificationDismissed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// POST_NOTIFICATIONS permission is not required for media session related notifications.
|
||||
// https://developer.android.com/develop/ui/views/notifications/notification-permission#exemptions-media-sessions
|
||||
@SuppressLint("MissingPermission")
|
||||
@ -270,7 +267,6 @@ import java.util.concurrent.TimeoutException;
|
||||
boolean startInForegroundRequired) {
|
||||
// Call Notification.MediaStyle#setMediaSession() indirectly.
|
||||
android.media.session.MediaSession.Token fwkToken =
|
||||
(android.media.session.MediaSession.Token)
|
||||
session.getSessionCompat().getSessionToken().getToken();
|
||||
mediaNotification.notification.extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, fwkToken);
|
||||
this.mediaNotification = mediaNotification;
|
||||
@ -301,17 +297,25 @@ import java.util.concurrent.TimeoutException;
|
||||
|
||||
private boolean shouldShowNotification(MediaSession session) {
|
||||
MediaController controller = getConnectedControllerForSession(session);
|
||||
return controller != null && !controller.getCurrentTimeline().isEmpty();
|
||||
if (controller == null || controller.getCurrentTimeline().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
ControllerInfo controllerInfo = checkNotNull(controllerMap.get(session));
|
||||
if (controller.getPlaybackState() != Player.STATE_IDLE) {
|
||||
// Playback restarted, reset previous notification dismissed flag.
|
||||
controllerInfo.wasNotificationDismissed = false;
|
||||
}
|
||||
return !controllerInfo.wasNotificationDismissed;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MediaController getConnectedControllerForSession(MediaSession session) {
|
||||
ListenableFuture<MediaController> controller = controllerMap.get(session);
|
||||
if (controller == null || !controller.isDone()) {
|
||||
@Nullable ControllerInfo controllerInfo = controllerMap.get(session);
|
||||
if (controllerInfo == null || !controllerInfo.controllerFuture.isDone()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Futures.getDone(controller);
|
||||
return Futures.getDone(controllerInfo.controllerFuture);
|
||||
} catch (ExecutionException exception) {
|
||||
// We should never reach this.
|
||||
throw new IllegalStateException(exception);
|
||||
@ -350,8 +354,7 @@ import java.util.concurrent.TimeoutException;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MediaControllerListener
|
||||
implements MediaController.Listener, Player.Listener {
|
||||
private final class MediaControllerListener implements MediaController.Listener, Player.Listener {
|
||||
private final MediaSessionService mediaSessionService;
|
||||
private final MediaSession session;
|
||||
|
||||
@ -381,6 +384,17 @@ import java.util.concurrent.TimeoutException;
|
||||
session, /* startInForegroundWhenPaused= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<SessionResult> onCustomCommand(
|
||||
MediaController controller, SessionCommand command, Bundle args) {
|
||||
@SessionResult.Code int resultCode = SessionError.ERROR_NOT_SUPPORTED;
|
||||
if (command.customAction.equals(MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY)) {
|
||||
onNotificationDismissed(session);
|
||||
resultCode = SessionResult.RESULT_SUCCESS;
|
||||
}
|
||||
return Futures.immediateFuture(new SessionResult(resultCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected(MediaController controller) {
|
||||
if (mediaSessionService.isSessionAdded(session)) {
|
||||
@ -427,6 +441,18 @@ import java.util.concurrent.TimeoutException;
|
||||
startedInForeground = false;
|
||||
}
|
||||
|
||||
private static final class ControllerInfo {
|
||||
|
||||
public final ListenableFuture<MediaController> controllerFuture;
|
||||
|
||||
/** Indicates whether the user actively dismissed the notification. */
|
||||
public boolean wasNotificationDismissed;
|
||||
|
||||
public ControllerInfo(ListenableFuture<MediaController> controllerFuture) {
|
||||
this.controllerFuture = controllerFuture;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(24)
|
||||
private static class Api24 {
|
||||
|
||||
|
@ -1322,10 +1322,14 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||
return false;
|
||||
}
|
||||
// Send from media notification controller.
|
||||
return applyMediaButtonKeyEvent(keyEvent, doubleTapCompleted);
|
||||
boolean isDismissNotificationEvent =
|
||||
intent.getBooleanExtra(
|
||||
MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY, /* defaultValue= */ false);
|
||||
return applyMediaButtonKeyEvent(keyEvent, doubleTapCompleted, isDismissNotificationEvent);
|
||||
}
|
||||
|
||||
private boolean applyMediaButtonKeyEvent(KeyEvent keyEvent, boolean doubleTapCompleted) {
|
||||
private boolean applyMediaButtonKeyEvent(
|
||||
KeyEvent keyEvent, boolean doubleTapCompleted, boolean isDismissNotificationEvent) {
|
||||
ControllerInfo controllerInfo = checkNotNull(instance.getMediaNotificationControllerInfo());
|
||||
Runnable command;
|
||||
int keyCode = keyEvent.getKeyCode();
|
||||
@ -1375,6 +1379,15 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||
postOrRun(
|
||||
getApplicationHandler(),
|
||||
() -> {
|
||||
if (isDismissNotificationEvent) {
|
||||
ListenableFuture<SessionResult> ignored =
|
||||
sendCustomCommand(
|
||||
controllerInfo,
|
||||
new SessionCommand(
|
||||
MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY,
|
||||
/* extras= */ Bundle.EMPTY),
|
||||
/* args= */ Bundle.EMPTY);
|
||||
}
|
||||
command.run();
|
||||
sessionStub.getConnectedControllersManager().flushCommandQueue(controllerInfo);
|
||||
});
|
||||
@ -1902,7 +1915,10 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||
playPauseTask =
|
||||
() -> {
|
||||
if (isMediaNotificationController(controllerInfo)) {
|
||||
applyMediaButtonKeyEvent(keyEvent, /* doubleTapCompleted= */ false);
|
||||
applyMediaButtonKeyEvent(
|
||||
keyEvent,
|
||||
/* doubleTapCompleted= */ false,
|
||||
/* isDismissNotificationEvent= */ false);
|
||||
} else {
|
||||
sessionLegacyStub.handleMediaPlayPauseOnHandler(
|
||||
checkNotNull(controllerInfo.getRemoteUserInfo()));
|
||||
|
Loading…
x
Reference in New Issue
Block a user