Skip to content

Commit f672590

Browse files
toniheicopybara-github
authored andcommitted
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: #2302 PiperOrigin-RevId: 745989590
1 parent a78d0c3 commit f672590

File tree

6 files changed

+116
-36
lines changed

6 files changed

+116
-36
lines changed

RELEASENOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
* Lower aggregation timeout for platform `MediaSession` callbacks from 500
6767
to 100 milliseconds and add an experimental setter to allow apps to
6868
configure this value.
69+
* Fix issue where notifications reappear after they have been dismissed by
70+
the user ([#2302](https://github.com/androidx/media/issues/2302)).
6971
* UI:
7072
* Enable `PlayerSurface` to work with `ExoPlayer.setVideoEffects` and
7173
`CompositionPlayer`.

libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,7 @@ public NotificationCompat.Action createCustomActionFromCustomCommandButton(
113113
public PendingIntent createMediaActionPendingIntent(
114114
MediaSession mediaSession, @Player.Command long command) {
115115
int keyCode = toKeyCode(command);
116-
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
117-
intent.setData(mediaSession.getImpl().getUri());
118-
intent.setComponent(new ComponentName(service, service.getClass()));
119-
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
116+
Intent intent = getMediaButtonIntent(mediaSession, keyCode);
120117
if (Util.SDK_INT >= 26
121118
&& command == COMMAND_PLAY_PAUSE
122119
&& !mediaSession.getPlayer().getPlayWhenReady()) {
@@ -130,6 +127,26 @@ public PendingIntent createMediaActionPendingIntent(
130127
}
131128
}
132129

130+
@Override
131+
public PendingIntent createNotificationDismissalIntent(MediaSession mediaSession) {
132+
Intent intent =
133+
getMediaButtonIntent(mediaSession, KEYCODE_MEDIA_STOP)
134+
.putExtra(MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY, true);
135+
return PendingIntent.getService(
136+
service,
137+
/* requestCode= */ KEYCODE_MEDIA_STOP,
138+
intent,
139+
Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0);
140+
}
141+
142+
private Intent getMediaButtonIntent(MediaSession mediaSession, int mediaKeyCode) {
143+
Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
144+
intent.setData(mediaSession.getImpl().getUri());
145+
intent.setComponent(new ComponentName(service, service.getClass()));
146+
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, mediaKeyCode));
147+
return intent;
148+
}
149+
133150
private int toKeyCode(@Player.Command long action) {
134151
if (action == COMMAND_SEEK_TO_NEXT_MEDIA_ITEM || action == COMMAND_SEEK_TO_NEXT) {
135152
return KEYCODE_MEDIA_NEXT;

libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
2323
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
2424
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
25-
import static androidx.media3.common.Player.COMMAND_STOP;
2625
import static androidx.media3.common.util.Assertions.checkState;
2726
import static androidx.media3.common.util.Assertions.checkStateNotNull;
2827

@@ -379,8 +378,7 @@ public final MediaNotification createNotification(
379378
Notification notification =
380379
builder
381380
.setContentIntent(mediaSession.getSessionActivity())
382-
.setDeleteIntent(
383-
actionFactory.createMediaActionPendingIntent(mediaSession, COMMAND_STOP))
381+
.setDeleteIntent(actionFactory.createNotificationDismissalIntent(mediaSession))
384382
.setOnlyAlertOnce(true)
385383
.setSmallIcon(smallIconResourceId)
386384
.setStyle(mediaStyle)

libraries/session/src/main/java/androidx/media3/session/MediaNotification.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
/** A notification for media playbacks. */
3232
public final class MediaNotification {
3333

34+
/**
35+
* Event key to indicate a media notification was dismissed.
36+
*
37+
* <p>This event key can be used as an extras key for a boolean extra on a media button pending
38+
* intent, and as as custom session command action to inform the media notification controller
39+
* that a notification was dismissed.
40+
*/
41+
@UnstableApi
42+
public static final String NOTIFICATION_DISMISSED_EVENT_KEY =
43+
"androidx.media3.session.NOTIFICATION_DISMISSED_EVENT_KEY";
44+
3445
/**
3546
* Creates {@linkplain NotificationCompat.Action actions} and {@linkplain PendingIntent pending
3647
* intents} for notifications.
@@ -99,10 +110,20 @@ NotificationCompat.Action createCustomActionFromCustomCommandButton(
99110
* Creates a {@link PendingIntent} for a media action that will be handled by the library.
100111
*
101112
* @param mediaSession The media session to which the action will be sent.
102-
* @param command The intent's command.
113+
* @param command The {@link PendingIntent}.
103114
*/
104115
PendingIntent createMediaActionPendingIntent(
105116
MediaSession mediaSession, @Player.Command long command);
117+
118+
/**
119+
* Creates a {@link PendingIntent} triggered when the notification is dismissed.
120+
*
121+
* @param mediaSession The media session for which the intent is created.
122+
* @return The {@link PendingIntent}.
123+
*/
124+
default PendingIntent createNotificationDismissalIntent(MediaSession mediaSession) {
125+
return createMediaActionPendingIntent(mediaSession, Player.COMMAND_STOP);
126+
}
106127
}
107128

108129
/**

libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static android.app.Service.STOP_FOREGROUND_DETACH;
1919
import static android.app.Service.STOP_FOREGROUND_REMOVE;
20+
import static androidx.media3.common.util.Assertions.checkNotNull;
2021
import static java.util.concurrent.TimeUnit.MILLISECONDS;
2122

2223
import android.annotation.SuppressLint;
@@ -65,7 +66,7 @@
6566
private final Handler mainHandler;
6667
private final Executor mainExecutor;
6768
private final Intent startSelfIntent;
68-
private final Map<MediaSession, ListenableFuture<MediaController>> controllerMap;
69+
private final Map<MediaSession, ControllerInfo> controllerMap;
6970

7071
private int totalNotificationCount;
7172
@Nullable private MediaNotification mediaNotification;
@@ -104,7 +105,7 @@ public void addSession(MediaSession session) {
104105
.setListener(listener)
105106
.setApplicationLooper(Looper.getMainLooper())
106107
.buildAsync();
107-
controllerMap.put(session, controllerFuture);
108+
controllerMap.put(session, new ControllerInfo(controllerFuture));
108109
controllerFuture.addListener(
109110
() -> {
110111
try {
@@ -123,9 +124,9 @@ public void addSession(MediaSession session) {
123124
}
124125

125126
public void removeSession(MediaSession session) {
126-
@Nullable ListenableFuture<MediaController> future = controllerMap.remove(session);
127-
if (future != null) {
128-
MediaController.releaseFuture(future);
127+
@Nullable ControllerInfo controllerInfo = controllerMap.remove(session);
128+
if (controllerInfo != null) {
129+
MediaController.releaseFuture(controllerInfo.controllerFuture);
129130
}
130131
}
131132

@@ -158,19 +159,8 @@ public void updateNotification(MediaSession session, boolean startInForegroundRe
158159
}
159160

160161
int notificationSequence = ++totalNotificationCount;
161-
MediaController mediaNotificationController = null;
162-
ListenableFuture<MediaController> controller = controllerMap.get(session);
163-
if (controller != null && controller.isDone()) {
164-
try {
165-
mediaNotificationController = Futures.getDone(controller);
166-
} catch (ExecutionException e) {
167-
// Ignore.
168-
}
169-
}
170162
ImmutableList<CommandButton> mediaButtonPreferences =
171-
mediaNotificationController != null
172-
? mediaNotificationController.getMediaButtonPreferences()
173-
: ImmutableList.of();
163+
checkNotNull(getConnectedControllerForSession(session)).getMediaButtonPreferences();
174164
MediaNotification.Provider.Callback callback =
175165
notification ->
176166
mainExecutor.execute(
@@ -261,6 +251,13 @@ private void onNotificationUpdated(
261251
}
262252
}
263253

254+
private void onNotificationDismissed(MediaSession session) {
255+
@Nullable ControllerInfo controllerInfo = controllerMap.get(session);
256+
if (controllerInfo != null) {
257+
controllerInfo.wasNotificationDismissed = true;
258+
}
259+
}
260+
264261
// POST_NOTIFICATIONS permission is not required for media session related notifications.
265262
// https://developer.android.com/develop/ui/views/notifications/notification-permission#exemptions-media-sessions
266263
@SuppressLint("MissingPermission")
@@ -270,8 +267,7 @@ private void updateNotificationInternal(
270267
boolean startInForegroundRequired) {
271268
// Call Notification.MediaStyle#setMediaSession() indirectly.
272269
android.media.session.MediaSession.Token fwkToken =
273-
(android.media.session.MediaSession.Token)
274-
session.getSessionCompat().getSessionToken().getToken();
270+
session.getSessionCompat().getSessionToken().getToken();
275271
mediaNotification.notification.extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, fwkToken);
276272
this.mediaNotification = mediaNotification;
277273
if (startInForegroundRequired) {
@@ -301,17 +297,25 @@ private void removeNotification() {
301297

302298
private boolean shouldShowNotification(MediaSession session) {
303299
MediaController controller = getConnectedControllerForSession(session);
304-
return controller != null && !controller.getCurrentTimeline().isEmpty();
300+
if (controller == null || controller.getCurrentTimeline().isEmpty()) {
301+
return false;
302+
}
303+
ControllerInfo controllerInfo = checkNotNull(controllerMap.get(session));
304+
if (controller.getPlaybackState() != Player.STATE_IDLE) {
305+
// Playback restarted, reset previous notification dismissed flag.
306+
controllerInfo.wasNotificationDismissed = false;
307+
}
308+
return !controllerInfo.wasNotificationDismissed;
305309
}
306310

307311
@Nullable
308312
private MediaController getConnectedControllerForSession(MediaSession session) {
309-
ListenableFuture<MediaController> controller = controllerMap.get(session);
310-
if (controller == null || !controller.isDone()) {
313+
@Nullable ControllerInfo controllerInfo = controllerMap.get(session);
314+
if (controllerInfo == null || !controllerInfo.controllerFuture.isDone()) {
311315
return null;
312316
}
313317
try {
314-
return Futures.getDone(controller);
318+
return Futures.getDone(controllerInfo.controllerFuture);
315319
} catch (ExecutionException exception) {
316320
// We should never reach this.
317321
throw new IllegalStateException(exception);
@@ -350,8 +354,7 @@ public void onFailure(Throwable t) {
350354
}
351355
}
352356

353-
private static final class MediaControllerListener
354-
implements MediaController.Listener, Player.Listener {
357+
private final class MediaControllerListener implements MediaController.Listener, Player.Listener {
355358
private final MediaSessionService mediaSessionService;
356359
private final MediaSession session;
357360

@@ -381,6 +384,17 @@ public void onAvailableSessionCommandsChanged(
381384
session, /* startInForegroundWhenPaused= */ false);
382385
}
383386

387+
@Override
388+
public ListenableFuture<SessionResult> onCustomCommand(
389+
MediaController controller, SessionCommand command, Bundle args) {
390+
@SessionResult.Code int resultCode = SessionError.ERROR_NOT_SUPPORTED;
391+
if (command.customAction.equals(MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY)) {
392+
onNotificationDismissed(session);
393+
resultCode = SessionResult.RESULT_SUCCESS;
394+
}
395+
return Futures.immediateFuture(new SessionResult(resultCode));
396+
}
397+
384398
@Override
385399
public void onDisconnected(MediaController controller) {
386400
if (mediaSessionService.isSessionAdded(session)) {
@@ -427,6 +441,18 @@ private void stopForeground(boolean removeNotifications) {
427441
startedInForeground = false;
428442
}
429443

444+
private static final class ControllerInfo {
445+
446+
public final ListenableFuture<MediaController> controllerFuture;
447+
448+
/** Indicates whether the user actively dismissed the notification. */
449+
public boolean wasNotificationDismissed;
450+
451+
public ControllerInfo(ListenableFuture<MediaController> controllerFuture) {
452+
this.controllerFuture = controllerFuture;
453+
}
454+
}
455+
430456
@RequiresApi(24)
431457
private static class Api24 {
432458

libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,10 +1322,14 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
13221322
return false;
13231323
}
13241324
// Send from media notification controller.
1325-
return applyMediaButtonKeyEvent(keyEvent, doubleTapCompleted);
1325+
boolean isDismissNotificationEvent =
1326+
intent.getBooleanExtra(
1327+
MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY, /* defaultValue= */ false);
1328+
return applyMediaButtonKeyEvent(keyEvent, doubleTapCompleted, isDismissNotificationEvent);
13261329
}
13271330

1328-
private boolean applyMediaButtonKeyEvent(KeyEvent keyEvent, boolean doubleTapCompleted) {
1331+
private boolean applyMediaButtonKeyEvent(
1332+
KeyEvent keyEvent, boolean doubleTapCompleted, boolean isDismissNotificationEvent) {
13291333
ControllerInfo controllerInfo = checkNotNull(instance.getMediaNotificationControllerInfo());
13301334
Runnable command;
13311335
int keyCode = keyEvent.getKeyCode();
@@ -1375,6 +1379,15 @@ private boolean applyMediaButtonKeyEvent(KeyEvent keyEvent, boolean doubleTapCom
13751379
postOrRun(
13761380
getApplicationHandler(),
13771381
() -> {
1382+
if (isDismissNotificationEvent) {
1383+
ListenableFuture<SessionResult> ignored =
1384+
sendCustomCommand(
1385+
controllerInfo,
1386+
new SessionCommand(
1387+
MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY,
1388+
/* extras= */ Bundle.EMPTY),
1389+
/* args= */ Bundle.EMPTY);
1390+
}
13781391
command.run();
13791392
sessionStub.getConnectedControllersManager().flushCommandQueue(controllerInfo);
13801393
});
@@ -1902,7 +1915,10 @@ public void setPendingPlayPauseTask(ControllerInfo controllerInfo, KeyEvent keyE
19021915
playPauseTask =
19031916
() -> {
19041917
if (isMediaNotificationController(controllerInfo)) {
1905-
applyMediaButtonKeyEvent(keyEvent, /* doubleTapCompleted= */ false);
1918+
applyMediaButtonKeyEvent(
1919+
keyEvent,
1920+
/* doubleTapCompleted= */ false,
1921+
/* isDismissNotificationEvent= */ false);
19061922
} else {
19071923
sessionLegacyStub.handleMediaPlayPauseOnHandler(
19081924
checkNotNull(controllerInfo.getRemoteUserInfo()));

0 commit comments

Comments
 (0)