Skip to content

Commit a9e2e77

Browse files
committed
session: Avoid ForegroundServiceDidNotStartInTimeException...
...and throw more useful exceptions instead. This will however not throw exceptions where foreground service start is not absolutely required, and keep logging warnings in these cases instead. Issue: #2591 Issue: #2622
1 parent 4acf6f9 commit a9e2e77

File tree

4 files changed

+82
-25
lines changed

4 files changed

+82
-25
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1873,6 +1873,14 @@ default ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
18731873
* automatically as required. Any additional initial setup like setting playback speed, repeat
18741874
* mode or shuffle mode can be done from within this callback.
18751875
*
1876+
* <p>If the returned list is empty or an exception is returned, and the request to resume
1877+
* playback came from {@link MediaButtonReceiver}, an {@link IllegalStateException} will be
1878+
* thrown. This is because the {@link MediaButtonReceiver} has already requested a foreground
1879+
* service start, and it's not possible to avoid a {@code
1880+
* ForegroundServiceDidNotStartInTimeException} anymore. To avoid a crash as result, override
1881+
* {@link MediaButtonReceiver#shouldStartForegroundService(Context, Intent)} to return {@code
1882+
* false} if there is nothing to resume. This will avoid requesting a foreground service start.
1883+
*
18761884
* <p>The method will only be called if the {@link Player} has {@link
18771885
* Player#COMMAND_GET_CURRENT_MEDIA_ITEM} and either {@link Player#COMMAND_SET_MEDIA_ITEM} or
18781886
* {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} available.

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

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,9 @@ protected MediaSessionServiceLegacyStub getLegacyBrowserService() {
11281128
* @param controller The controller requesting to play.
11291129
*/
11301130
/* package */ ListenableFuture<SessionResult> handleMediaControllerPlayRequest(
1131-
ControllerInfo controller, boolean callOnPlayerInteractionFinished) {
1131+
ControllerInfo controller,
1132+
boolean callOnPlayerInteractionFinished,
1133+
boolean mustStartForegroundService) {
11321134
SettableFuture<SessionResult> sessionFuture = SettableFuture.create();
11331135
ListenableFuture<Boolean> playRequestedFuture = onPlayRequested();
11341136
playRequestedFuture.addListener(
@@ -1182,6 +1184,26 @@ public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) {
11821184
callWithControllerForCurrentRequestSet(
11831185
controllerForRequest,
11841186
() -> {
1187+
if (mediaItemsWithStartPosition.mediaItems.isEmpty()) {
1188+
if (mustStartForegroundService) {
1189+
applicationHandler.postAtFrontOfQueue(
1190+
() -> {
1191+
throw new IllegalArgumentException(
1192+
"Callback.onPlaybackResumption must return non-empty"
1193+
+ " MediaItemsWithStartPosition if started from a"
1194+
+ " media button receiver. If there is nothing to"
1195+
+ " resume playback with, override"
1196+
+ " MediaButtonReceiver.shouldStartForegroundService()"
1197+
+ " and return false.");
1198+
});
1199+
return;
1200+
}
1201+
Log.w(
1202+
TAG,
1203+
"onPlaybackResumption() is trying to resume with empty"
1204+
+ " playlist, this will make the resumption notification"
1205+
+ " appear broken.");
1206+
}
11851207
MediaUtils.setMediaItemsWithStartIndexAndPosition(
11861208
playerWrapper, mediaItemsWithStartPosition);
11871209
Util.handlePlayButtonAction(playerWrapper);
@@ -1196,21 +1218,33 @@ public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) {
11961218

11971219
@Override
11981220
public void onFailure(Throwable t) {
1221+
RuntimeException e;
11991222
if (t instanceof UnsupportedOperationException) {
1200-
Log.w(
1201-
TAG,
1202-
"UnsupportedOperationException: Make sure to implement"
1203-
+ " MediaSession.Callback.onPlaybackResumption() if you add a media"
1204-
+ " button receiver to your manifest or if you implement the recent"
1205-
+ " media item contract with your MediaLibraryService.",
1206-
t);
1223+
e =
1224+
new UnsupportedOperationException(
1225+
"Make sure to implement MediaSession.Callback.onPlaybackResumption()"
1226+
+ " if you add a media button receiver to your manifest or if you"
1227+
+ " implement the recent media item contract with your"
1228+
+ " MediaLibraryService.",
1229+
t);
12071230
} else {
1208-
Log.e(
1209-
TAG,
1210-
"Failure calling MediaSession.Callback.onPlaybackResumption(): "
1211-
+ t.getMessage(),
1212-
t);
1231+
e =
1232+
new IllegalStateException(
1233+
"Failure calling MediaSession.Callback.onPlaybackResumption(): "
1234+
+ t.getMessage(),
1235+
t);
1236+
}
1237+
if (mustStartForegroundService) {
1238+
// MediaButtonReceiver already called startForegroundService(). If we do not
1239+
// crash ourselves, ForegroundServiceDidNotStartInTimeException will do it
1240+
// for us. Let's at least get a useful stack trace out there.
1241+
applicationHandler.postAtFrontOfQueue(
1242+
() -> {
1243+
throw e;
1244+
});
1245+
return;
12131246
}
1247+
Log.e(TAG, Objects.requireNonNull(Log.getThrowableString(e)));
12141248
// Play as requested even if playback resumption fails.
12151249
Util.handlePlayButtonAction(playerWrapper);
12161250
sessionFuture.set(new SessionResult(SessionResult.RESULT_SUCCESS));
@@ -1448,13 +1482,13 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
14481482
// Double tap detection.
14491483
int keyCode = keyEvent.getKeyCode();
14501484
boolean isTvApp = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
1485+
boolean isEventSourceMediaButtonReceiver =
1486+
callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION;
14511487
boolean doubleTapCompleted = false;
14521488
switch (keyCode) {
14531489
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
14541490
case KeyEvent.KEYCODE_HEADSETHOOK:
1455-
if (isTvApp
1456-
|| callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION
1457-
|| keyEvent.getRepeatCount() != 0) {
1491+
if (isTvApp || isEventSourceMediaButtonReceiver || keyEvent.getRepeatCount() != 0) {
14581492
// Double tap detection is only for mobile apps that receive a media button event from
14591493
// external sources (for instance Bluetooth) and excluding long press (repeatCount > 0).
14601494
mediaPlayPauseKeyHandler.flush();
@@ -1493,11 +1527,15 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
14931527
boolean isDismissNotificationEvent =
14941528
intent.getBooleanExtra(
14951529
MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY, /* defaultValue= */ false);
1496-
return applyMediaButtonKeyEvent(keyEvent, doubleTapCompleted, isDismissNotificationEvent);
1530+
return applyMediaButtonKeyEvent(
1531+
keyEvent, doubleTapCompleted, isDismissNotificationEvent, isEventSourceMediaButtonReceiver);
14971532
}
14981533

14991534
private boolean applyMediaButtonKeyEvent(
1500-
KeyEvent keyEvent, boolean doubleTapCompleted, boolean isDismissNotificationEvent) {
1535+
KeyEvent keyEvent,
1536+
boolean doubleTapCompleted,
1537+
boolean isDismissNotificationEvent,
1538+
boolean mustStartForegroundService) {
15011539
ControllerInfo controllerInfo = checkNotNull(instance.getMediaNotificationControllerInfo());
15021540
Runnable command;
15031541
int keyCode = keyEvent.getKeyCode();
@@ -1510,10 +1548,15 @@ private boolean applyMediaButtonKeyEvent(
15101548
command =
15111549
getPlayerWrapper().getPlayWhenReady()
15121550
? () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER)
1513-
: () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
1551+
: () ->
1552+
sessionStub.playForControllerInfo(
1553+
controllerInfo, UNKNOWN_SEQUENCE_NUMBER, mustStartForegroundService);
15141554
break;
15151555
case KEYCODE_MEDIA_PLAY:
1516-
command = () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
1556+
command =
1557+
() ->
1558+
sessionStub.playForControllerInfo(
1559+
controllerInfo, UNKNOWN_SEQUENCE_NUMBER, mustStartForegroundService);
15171560
break;
15181561
case KEYCODE_MEDIA_PAUSE:
15191562
command = () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
@@ -2095,7 +2138,8 @@ public void setPendingPlayPauseTask(ControllerInfo controllerInfo, KeyEvent keyE
20952138
applyMediaButtonKeyEvent(
20962139
keyEvent,
20972140
/* doubleTapCompleted= */ false,
2098-
/* isDismissNotificationEvent= */ false);
2141+
/* isDismissNotificationEvent= */ false,
2142+
/* mustStartForegroundService= */ false);
20992143
} else {
21002144
sessionLegacyStub.handleMediaPlayPauseOnHandler(
21012145
checkNotNull(controllerInfo.getRemoteUserInfo()));

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,9 @@ public void onPlay() {
610610
controller -> {
611611
ListenableFuture<SessionResult> resultFuture =
612612
sessionImpl.handleMediaControllerPlayRequest(
613-
controller, /* callOnPlayerInteractionFinished= */ true);
613+
controller,
614+
/* callOnPlayerInteractionFinished= */ true,
615+
/* mustStartForegroundService= */ false);
614616
Futures.addCallback(
615617
resultFuture,
616618
new FutureCallback<SessionResult>() {

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -768,19 +768,22 @@ public void play(@Nullable IMediaController caller, int sequenceNumber) {
768768
@Nullable
769769
ControllerInfo controller = connectedControllersManager.getController(caller.asBinder());
770770
if (controller != null) {
771-
playForControllerInfo(controller, sequenceNumber);
771+
playForControllerInfo(controller, sequenceNumber, /* mustStartForegroundService= */ false);
772772
}
773773
}
774774

775-
public void playForControllerInfo(ControllerInfo controller, int sequenceNumber) {
775+
public void playForControllerInfo(
776+
ControllerInfo controller, int sequenceNumber, boolean mustStartForegroundService) {
776777
queueSessionTaskWithPlayerCommandForControllerInfo(
777778
controller,
778779
sequenceNumber,
779780
COMMAND_PLAY_PAUSE,
780781
sendSessionResultWhenReady(
781782
(session, theController, sequenceId) ->
782783
session.handleMediaControllerPlayRequest(
783-
theController, /* callOnPlayerInteractionFinished= */ false)));
784+
theController,
785+
/* callOnPlayerInteractionFinished= */ false,
786+
/* mustStartForegroundService= */ mustStartForegroundService)));
784787
}
785788

786789
@Override

0 commit comments

Comments
 (0)