Commit a2aaad65 by tianyifeng Committed by christosts

Catch FgSStartNotAllowedException when playback resumes

This fix applies to Android 12 and above.

In this fix, the `MediaSessionService` will try to start in the foreground before the session playback resumes, if ForegroundServiceStartNotAllowedException is thrown, then the app can handle the exception with their customized implementation of MediaSessionService.Listener.onForegroundServiceStartNotAllowedException. If no exception thrown, the a media notification corresponding to paused state will be sent as the consequence of successfully starting in the foreground. And when the player actually resumes, another media notification corresponding to playing state will be sent.

PiperOrigin-RevId: 501803930
(cherry picked from commit 0d0cd786)
parent b644c679
...@@ -66,6 +66,7 @@ import java.util.concurrent.TimeoutException; ...@@ -66,6 +66,7 @@ import java.util.concurrent.TimeoutException;
private int totalNotificationCount; private int totalNotificationCount;
@Nullable private MediaNotification mediaNotification; @Nullable private MediaNotification mediaNotification;
private boolean startedInForeground;
public MediaNotificationManager( public MediaNotificationManager(
MediaSessionService mediaSessionService, MediaSessionService mediaSessionService,
...@@ -80,6 +81,7 @@ import java.util.concurrent.TimeoutException; ...@@ -80,6 +81,7 @@ import java.util.concurrent.TimeoutException;
startSelfIntent = new Intent(mediaSessionService, mediaSessionService.getClass()); startSelfIntent = new Intent(mediaSessionService, mediaSessionService.getClass());
controllerMap = new HashMap<>(); controllerMap = new HashMap<>();
customLayoutMap = new HashMap<>(); customLayoutMap = new HashMap<>();
startedInForeground = false;
} }
public void addSession(MediaSession session) { public void addSession(MediaSession session) {
...@@ -163,9 +165,14 @@ import java.util.concurrent.TimeoutException; ...@@ -163,9 +165,14 @@ import java.util.concurrent.TimeoutException;
} }
} }
public void updateNotification(MediaSession session) { /**
if (!mediaSessionService.isSessionAdded(session) * Updates the notification.
|| !shouldShowNotification(session.getPlayer())) { *
* @param session A session that needs notification update.
* @param startInForegroundRequired Whether the service is required to start in the foreground.
*/
public void updateNotification(MediaSession session, boolean startInForegroundRequired) {
if (!mediaSessionService.isSessionAdded(session) || !shouldShowNotification(session)) {
maybeStopForegroundService(/* removeNotifications= */ true); maybeStopForegroundService(/* removeNotifications= */ true);
return; return;
} }
...@@ -179,18 +186,27 @@ import java.util.concurrent.TimeoutException; ...@@ -179,18 +186,27 @@ import java.util.concurrent.TimeoutException;
MediaNotification mediaNotification = MediaNotification mediaNotification =
this.mediaNotificationProvider.createNotification( this.mediaNotificationProvider.createNotification(
session, checkStateNotNull(customLayoutMap.get(session)), actionFactory, callback); session, checkStateNotNull(customLayoutMap.get(session)), actionFactory, callback);
updateNotificationInternal(session, mediaNotification); updateNotificationInternal(session, mediaNotification, startInForegroundRequired);
}
public boolean isStartedInForeground() {
return startedInForeground;
} }
private void onNotificationUpdated( private void onNotificationUpdated(
int notificationSequence, MediaSession session, MediaNotification mediaNotification) { int notificationSequence, MediaSession session, MediaNotification mediaNotification) {
if (notificationSequence == totalNotificationCount) { if (notificationSequence == totalNotificationCount) {
updateNotificationInternal(session, mediaNotification); boolean startInForegroundRequired =
MediaSessionService.shouldRunInForeground(
session, /* startInForegroundWhenPaused= */ false);
updateNotificationInternal(session, mediaNotification, startInForegroundRequired);
} }
} }
private void updateNotificationInternal( private void updateNotificationInternal(
MediaSession session, MediaNotification mediaNotification) { MediaSession session,
MediaNotification mediaNotification,
boolean startInForegroundRequired) {
if (Util.SDK_INT >= 21) { if (Util.SDK_INT >= 21) {
// Call Notification.MediaStyle#setMediaSession() indirectly. // Call Notification.MediaStyle#setMediaSession() indirectly.
android.media.session.MediaSession.Token fwkToken = android.media.session.MediaSession.Token fwkToken =
...@@ -199,17 +215,9 @@ import java.util.concurrent.TimeoutException; ...@@ -199,17 +215,9 @@ import java.util.concurrent.TimeoutException;
mediaNotification.notification.extras.putParcelable( mediaNotification.notification.extras.putParcelable(
Notification.EXTRA_MEDIA_SESSION, fwkToken); Notification.EXTRA_MEDIA_SESSION, fwkToken);
} }
this.mediaNotification = mediaNotification; this.mediaNotification = mediaNotification;
Player player = session.getPlayer(); if (startInForegroundRequired) {
if (shouldRunInForeground(player)) { startForeground(mediaNotification);
ContextCompat.startForegroundService(mediaSessionService, startSelfIntent);
if (Util.SDK_INT >= 29) {
Api29.startForeground(mediaSessionService, mediaNotification);
} else {
mediaSessionService.startForeground(
mediaNotification.notificationId, mediaNotification.notification);
}
} else { } else {
maybeStopForegroundService(/* removeNotifications= */ false); maybeStopForegroundService(/* removeNotifications= */ false);
notificationManagerCompat.notify( notificationManagerCompat.notify(
...@@ -226,19 +234,12 @@ import java.util.concurrent.TimeoutException; ...@@ -226,19 +234,12 @@ import java.util.concurrent.TimeoutException;
private void maybeStopForegroundService(boolean removeNotifications) { private void maybeStopForegroundService(boolean removeNotifications) {
List<MediaSession> sessions = mediaSessionService.getSessions(); List<MediaSession> sessions = mediaSessionService.getSessions();
for (int i = 0; i < sessions.size(); i++) { for (int i = 0; i < sessions.size(); i++) {
if (shouldRunInForeground(sessions.get(i).getPlayer())) { if (MediaSessionService.shouldRunInForeground(
sessions.get(i), /* startInForegroundWhenPaused= */ false)) {
return; return;
} }
} }
// To hide the notification on all API levels, we need to call both Service.stopForeground(true) stopForeground(removeNotifications);
// and notificationManagerCompat.cancel(notificationId).
if (Util.SDK_INT >= 24) {
Api24.stopForeground(mediaSessionService, removeNotifications);
} else {
// For pre-L devices, we must call Service.stopForeground(true) anyway as a workaround
// that prevents the media notification from being undismissable.
mediaSessionService.stopForeground(removeNotifications || Util.SDK_INT < 21);
}
if (removeNotifications && mediaNotification != null) { if (removeNotifications && mediaNotification != null) {
notificationManagerCompat.cancel(mediaNotification.notificationId); notificationManagerCompat.cancel(mediaNotification.notificationId);
// Update the notification count so that if a pending notification callback arrives (e.g., a // Update the notification count so that if a pending notification callback arrives (e.g., a
...@@ -248,16 +249,11 @@ import java.util.concurrent.TimeoutException; ...@@ -248,16 +249,11 @@ import java.util.concurrent.TimeoutException;
} }
} }
private static boolean shouldShowNotification(Player player) { private static boolean shouldShowNotification(MediaSession session) {
Player player = session.getPlayer();
return !player.getCurrentTimeline().isEmpty() && player.getPlaybackState() != Player.STATE_IDLE; return !player.getCurrentTimeline().isEmpty() && player.getPlaybackState() != Player.STATE_IDLE;
} }
private static boolean shouldRunInForeground(Player player) {
return player.getPlayWhenReady()
&& (player.getPlaybackState() == Player.STATE_READY
|| player.getPlaybackState() == Player.STATE_BUFFERING);
}
private static final class MediaControllerListener private static final class MediaControllerListener
implements MediaController.Listener, Player.Listener { implements MediaController.Listener, Player.Listener {
private final MediaSessionService mediaSessionService; private final MediaSessionService mediaSessionService;
...@@ -274,8 +270,9 @@ import java.util.concurrent.TimeoutException; ...@@ -274,8 +270,9 @@ import java.util.concurrent.TimeoutException;
} }
public void onConnected() { public void onConnected() {
if (shouldShowNotification(session.getPlayer())) { if (shouldShowNotification(session)) {
mediaSessionService.onUpdateNotification(session); mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
} }
} }
...@@ -283,7 +280,8 @@ import java.util.concurrent.TimeoutException; ...@@ -283,7 +280,8 @@ import java.util.concurrent.TimeoutException;
public ListenableFuture<SessionResult> onSetCustomLayout( public ListenableFuture<SessionResult> onSetCustomLayout(
MediaController controller, List<CommandButton> layout) { MediaController controller, List<CommandButton> layout) {
customLayoutMap.put(session, ImmutableList.copyOf(layout)); customLayoutMap.put(session, ImmutableList.copyOf(layout));
mediaSessionService.onUpdateNotification(session); mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
} }
...@@ -296,7 +294,8 @@ import java.util.concurrent.TimeoutException; ...@@ -296,7 +294,8 @@ import java.util.concurrent.TimeoutException;
Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_MEDIA_METADATA_CHANGED, Player.EVENT_MEDIA_METADATA_CHANGED,
Player.EVENT_TIMELINE_CHANGED)) { Player.EVENT_TIMELINE_CHANGED)) {
mediaSessionService.onUpdateNotification(session); mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
} }
} }
...@@ -304,8 +303,33 @@ import java.util.concurrent.TimeoutException; ...@@ -304,8 +303,33 @@ import java.util.concurrent.TimeoutException;
public void onDisconnected(MediaController controller) { public void onDisconnected(MediaController controller) {
mediaSessionService.removeSession(session); mediaSessionService.removeSession(session);
// We may need to hide the notification. // We may need to hide the notification.
mediaSessionService.onUpdateNotification(session); mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
}
}
private void startForeground(MediaNotification mediaNotification) {
ContextCompat.startForegroundService(mediaSessionService, startSelfIntent);
if (Util.SDK_INT >= 29) {
Api29.startForeground(mediaSessionService, mediaNotification);
} else {
mediaSessionService.startForeground(
mediaNotification.notificationId, mediaNotification.notification);
}
startedInForeground = true;
}
private void stopForeground(boolean removeNotifications) {
// To hide the notification on all API levels, we need to call both Service.stopForeground(true)
// and notificationManagerCompat.cancel(notificationId).
if (Util.SDK_INT >= 24) {
Api24.stopForeground(mediaSessionService, removeNotifications);
} else {
// For pre-L devices, we must call Service.stopForeground(true) anyway as a workaround
// that prevents the media notification from being undismissable.
mediaSessionService.stopForeground(removeNotifications || Util.SDK_INT < 21);
} }
startedInForeground = false;
} }
@RequiresApi(24) @RequiresApi(24)
......
...@@ -877,10 +877,15 @@ public class MediaSession { ...@@ -877,10 +877,15 @@ public class MediaSession {
} }
/** Sets the {@linkplain Listener listener}. */ /** Sets the {@linkplain Listener listener}. */
/* package */ void setListener(@Nullable Listener listener) { /* package */ void setListener(Listener listener) {
impl.setMediaSessionListener(listener); impl.setMediaSessionListener(listener);
} }
/** Clears the {@linkplain Listener listener}. */
/* package */ void clearListener() {
impl.clearMediaSessionListener();
}
private Uri getUri() { private Uri getUri() {
return impl.getUri(); return impl.getUri();
} }
...@@ -1272,6 +1277,15 @@ public class MediaSession { ...@@ -1272,6 +1277,15 @@ public class MediaSession {
* @param session The media session for which the notification requires to be refreshed. * @param session The media session for which the notification requires to be refreshed.
*/ */
void onNotificationRefreshRequired(MediaSession session); void onNotificationRefreshRequired(MediaSession session);
/**
* Called when the {@linkplain MediaSession session} receives the play command and requests from
* the listener on whether the media can be played.
*
* @param session The media session which requests if the media can be played.
* @return True if the media can be played, false otherwise.
*/
boolean onPlayRequested(MediaSession session);
} }
/** /**
......
...@@ -579,16 +579,27 @@ import org.checkerframework.checker.initialization.qual.Initialized; ...@@ -579,16 +579,27 @@ import org.checkerframework.checker.initialization.qual.Initialized;
} }
} }
/* package */ void setMediaSessionListener(@Nullable MediaSession.Listener listener) { /* package */ void setMediaSessionListener(MediaSession.Listener listener) {
this.mediaSessionListener = listener; this.mediaSessionListener = listener;
} }
/* package */ void clearMediaSessionListener() {
this.mediaSessionListener = null;
}
/* package */ void onNotificationRefreshRequired() { /* package */ void onNotificationRefreshRequired() {
if (this.mediaSessionListener != null) { if (this.mediaSessionListener != null) {
this.mediaSessionListener.onNotificationRefreshRequired(instance); this.mediaSessionListener.onNotificationRefreshRequired(instance);
} }
} }
/* package */ boolean onPlayRequested() {
if (this.mediaSessionListener != null) {
return this.mediaSessionListener.onPlayRequested(instance);
}
return true;
}
private void dispatchRemoteControllerTaskToLegacyStub(RemoteControllerTask task) { private void dispatchRemoteControllerTaskToLegacyStub(RemoteControllerTask task) {
try { try {
task.run(sessionLegacyStub.getControllerLegacyCbForBroadcast(), /* seq= */ 0); task.run(sessionLegacyStub.getControllerLegacyCbForBroadcast(), /* seq= */ 0);
......
...@@ -313,7 +313,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -313,7 +313,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
playerWrapper.seekTo( playerWrapper.seekTo(
playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
} }
playerWrapper.play(); if (sessionImpl.onPlayRequested()) {
playerWrapper.play();
}
}, },
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo());
} }
......
...@@ -611,7 +611,20 @@ import java.util.concurrent.ExecutionException; ...@@ -611,7 +611,20 @@ import java.util.concurrent.ExecutionException;
return; return;
} }
queueSessionTaskWithPlayerCommand( queueSessionTaskWithPlayerCommand(
caller, sequenceNumber, COMMAND_PLAY_PAUSE, sendSessionResultSuccess(Player::play)); caller,
sequenceNumber,
COMMAND_PLAY_PAUSE,
sendSessionResultSuccess(
player -> {
@Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
if (sessionImpl == null || sessionImpl.isReleased()) {
return;
}
if (sessionImpl.onPlayRequested()) {
player.play();
}
}));
} }
@Override @Override
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment