Commit 0d0cd786 by tianyifeng Committed by Rohit Singh

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
parent d1e03a41
......@@ -66,6 +66,7 @@ import java.util.concurrent.TimeoutException;
private int totalNotificationCount;
@Nullable private MediaNotification mediaNotification;
private boolean startedInForeground;
public MediaNotificationManager(
MediaSessionService mediaSessionService,
......@@ -80,6 +81,7 @@ import java.util.concurrent.TimeoutException;
startSelfIntent = new Intent(mediaSessionService, mediaSessionService.getClass());
controllerMap = new HashMap<>();
customLayoutMap = new HashMap<>();
startedInForeground = false;
}
public void addSession(MediaSession session) {
......@@ -163,9 +165,14 @@ import java.util.concurrent.TimeoutException;
}
}
public void updateNotification(MediaSession session) {
if (!mediaSessionService.isSessionAdded(session)
|| !shouldShowNotification(session.getPlayer())) {
/**
* Updates the notification.
*
* @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);
return;
}
......@@ -179,18 +186,27 @@ import java.util.concurrent.TimeoutException;
MediaNotification mediaNotification =
this.mediaNotificationProvider.createNotification(
session, checkStateNotNull(customLayoutMap.get(session)), actionFactory, callback);
updateNotificationInternal(session, mediaNotification);
updateNotificationInternal(session, mediaNotification, startInForegroundRequired);
}
public boolean isStartedInForeground() {
return startedInForeground;
}
private void onNotificationUpdated(
int notificationSequence, MediaSession session, MediaNotification mediaNotification) {
if (notificationSequence == totalNotificationCount) {
updateNotificationInternal(session, mediaNotification);
boolean startInForegroundRequired =
MediaSessionService.shouldRunInForeground(
session, /* startInForegroundWhenPaused= */ false);
updateNotificationInternal(session, mediaNotification, startInForegroundRequired);
}
}
private void updateNotificationInternal(
MediaSession session, MediaNotification mediaNotification) {
MediaSession session,
MediaNotification mediaNotification,
boolean startInForegroundRequired) {
if (Util.SDK_INT >= 21) {
// Call Notification.MediaStyle#setMediaSession() indirectly.
android.media.session.MediaSession.Token fwkToken =
......@@ -199,17 +215,9 @@ import java.util.concurrent.TimeoutException;
mediaNotification.notification.extras.putParcelable(
Notification.EXTRA_MEDIA_SESSION, fwkToken);
}
this.mediaNotification = mediaNotification;
Player player = session.getPlayer();
if (shouldRunInForeground(player)) {
ContextCompat.startForegroundService(mediaSessionService, startSelfIntent);
if (Util.SDK_INT >= 29) {
Api29.startForeground(mediaSessionService, mediaNotification);
} else {
mediaSessionService.startForeground(
mediaNotification.notificationId, mediaNotification.notification);
}
if (startInForegroundRequired) {
startForeground(mediaNotification);
} else {
maybeStopForegroundService(/* removeNotifications= */ false);
notificationManagerCompat.notify(
......@@ -226,19 +234,12 @@ import java.util.concurrent.TimeoutException;
private void maybeStopForegroundService(boolean removeNotifications) {
List<MediaSession> sessions = mediaSessionService.getSessions();
for (int i = 0; i < sessions.size(); i++) {
if (shouldRunInForeground(sessions.get(i).getPlayer())) {
if (MediaSessionService.shouldRunInForeground(
sessions.get(i), /* startInForegroundWhenPaused= */ false)) {
return;
}
}
// 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);
}
stopForeground(removeNotifications);
if (removeNotifications && mediaNotification != null) {
notificationManagerCompat.cancel(mediaNotification.notificationId);
// Update the notification count so that if a pending notification callback arrives (e.g., a
......@@ -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;
}
private static boolean shouldRunInForeground(Player player) {
return player.getPlayWhenReady()
&& (player.getPlaybackState() == Player.STATE_READY
|| player.getPlaybackState() == Player.STATE_BUFFERING);
}
private static final class MediaControllerListener
implements MediaController.Listener, Player.Listener {
private final MediaSessionService mediaSessionService;
......@@ -274,8 +270,9 @@ import java.util.concurrent.TimeoutException;
}
public void onConnected() {
if (shouldShowNotification(session.getPlayer())) {
mediaSessionService.onUpdateNotification(session);
if (shouldShowNotification(session)) {
mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
}
}
......@@ -283,7 +280,8 @@ import java.util.concurrent.TimeoutException;
public ListenableFuture<SessionResult> onSetCustomLayout(
MediaController controller, List<CommandButton> layout) {
customLayoutMap.put(session, ImmutableList.copyOf(layout));
mediaSessionService.onUpdateNotification(session);
mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
}
......@@ -296,7 +294,8 @@ import java.util.concurrent.TimeoutException;
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_MEDIA_METADATA_CHANGED,
Player.EVENT_TIMELINE_CHANGED)) {
mediaSessionService.onUpdateNotification(session);
mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
}
}
......@@ -304,8 +303,33 @@ import java.util.concurrent.TimeoutException;
public void onDisconnected(MediaController controller) {
mediaSessionService.removeSession(session);
// 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)
......
......@@ -878,10 +878,15 @@ public class MediaSession {
}
/** Sets the {@linkplain Listener listener}. */
/* package */ void setListener(@Nullable Listener listener) {
/* package */ void setListener(Listener listener) {
impl.setMediaSessionListener(listener);
}
/** Clears the {@linkplain Listener listener}. */
/* package */ void clearListener() {
impl.clearMediaSessionListener();
}
private Uri getUri() {
return impl.getUri();
}
......@@ -1273,6 +1278,15 @@ public class MediaSession {
* @param session The media session for which the notification requires to be refreshed.
*/
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);
}
/**
......
......@@ -580,16 +580,27 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
}
/* package */ void setMediaSessionListener(@Nullable MediaSession.Listener listener) {
/* package */ void setMediaSessionListener(MediaSession.Listener listener) {
this.mediaSessionListener = listener;
}
/* package */ void clearMediaSessionListener() {
this.mediaSessionListener = null;
}
/* package */ void onNotificationRefreshRequired() {
if (this.mediaSessionListener != null) {
this.mediaSessionListener.onNotificationRefreshRequired(instance);
}
}
/* package */ boolean onPlayRequested() {
if (this.mediaSessionListener != null) {
return this.mediaSessionListener.onPlayRequested(instance);
}
return true;
}
private void dispatchRemoteControllerTaskToLegacyStub(RemoteControllerTask task) {
try {
task.run(sessionLegacyStub.getControllerLegacyCbForBroadcast(), /* seq= */ 0);
......
......@@ -313,7 +313,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
playerWrapper.seekTo(
playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET);
}
playerWrapper.play();
if (sessionImpl.onPlayRequested()) {
playerWrapper.play();
}
},
sessionCompat.getCurrentControllerInfo());
}
......
......@@ -611,7 +611,20 @@ import java.util.concurrent.ExecutionException;
return;
}
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
......
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