Skip to content

Commit b5da739

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 7aac326 commit b5da739

File tree

4 files changed

+85
-25
lines changed

4 files changed

+85
-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
@@ -1896,6 +1896,14 @@ default ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
18961896
* automatically as required. Any additional initial setup like setting playback speed, repeat
18971897
* mode or shuffle mode can be done from within this callback.
18981898
*
1899+
* <p>If the returned list is empty or an exception is returned, and the request to resume
1900+
* playback came from {@link MediaButtonReceiver}, an {@link IllegalStateException} will be
1901+
* thrown. This is because the {@link MediaButtonReceiver} has already requested a foreground
1902+
* service start, and it's not possible to avoid a {@code
1903+
* ForegroundServiceDidNotStartInTimeException} anymore. To avoid a crash as result, override
1904+
* {@link MediaButtonReceiver#shouldStartForegroundService(Context, Intent)} to return {@code
1905+
* false} if there is nothing to resume. This will avoid requesting a foreground service start.
1906+
*
18991907
* <p>The method will only be called if the {@link Player} has {@link
19001908
* Player#COMMAND_GET_CURRENT_MEDIA_ITEM} and either {@link Player#COMMAND_SET_MEDIA_ITEM} or
19011909
* {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} available.

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

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

11961218
@Override
11971219
public void onFailure(Throwable t) {
1220+
RuntimeException e;
11981221
if (t instanceof UnsupportedOperationException) {
1199-
Log.w(
1200-
TAG,
1201-
"UnsupportedOperationException: Make sure to implement"
1202-
+ " MediaSession.Callback.onPlaybackResumption() if you add a media"
1203-
+ " button receiver to your manifest or if you implement the recent"
1204-
+ " media item contract with your MediaLibraryService.",
1205-
t);
1222+
e =
1223+
new UnsupportedOperationException(
1224+
"Make sure to implement MediaSession.Callback.onPlaybackResumption()"
1225+
+ " if you add a media button receiver to your manifest or if you"
1226+
+ " implement the recent media item contract with your"
1227+
+ " MediaLibraryService.",
1228+
t);
12061229
} else {
1207-
Log.e(
1208-
TAG,
1209-
"Failure calling MediaSession.Callback.onPlaybackResumption(): "
1210-
+ t.getMessage(),
1211-
t);
1230+
e =
1231+
new IllegalStateException(
1232+
"Failure calling MediaSession.Callback.onPlaybackResumption(): "
1233+
+ t.getMessage(),
1234+
t);
1235+
}
1236+
if (mustStartForegroundService) {
1237+
// MediaButtonReceiver already called startForegroundService(). If we do not
1238+
// crash ourselves, ForegroundServiceDidNotStartInTimeException will do it
1239+
// for us. Let's at least get a useful stack trace out there.
1240+
applicationHandler.postAtFrontOfQueue(
1241+
() -> {
1242+
throw e;
1243+
});
1244+
return;
12121245
}
1246+
Log.e(TAG, Objects.requireNonNull(Log.getThrowableString(e)));
12131247
// Play as requested even if playback resumption fails.
12141248
Util.handlePlayButtonAction(playerWrapper);
12151249
sessionFuture.set(new SessionResult(SessionResult.RESULT_SUCCESS));
@@ -1467,13 +1501,13 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
14671501
// Double tap detection.
14681502
int keyCode = keyEvent.getKeyCode();
14691503
boolean isTvApp = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
1504+
boolean isEventSourceMediaButtonReceiver =
1505+
callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION;
14701506
boolean doubleTapCompleted = false;
14711507
switch (keyCode) {
14721508
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
14731509
case KeyEvent.KEYCODE_HEADSETHOOK:
1474-
if (isTvApp
1475-
|| callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION
1476-
|| keyEvent.getRepeatCount() != 0) {
1510+
if (isTvApp || isEventSourceMediaButtonReceiver || keyEvent.getRepeatCount() != 0) {
14771511
// Double tap detection is only for mobile apps that receive a media button event from
14781512
// external sources (for instance Bluetooth) and excluding long press (repeatCount > 0).
14791513
mediaPlayPauseKeyHandler.flush();
@@ -1513,11 +1547,18 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
15131547
intent.getBooleanExtra(
15141548
MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY, /* defaultValue= */ false);
15151549
return keyEvent.getRepeatCount() > 0
1516-
|| applyMediaButtonKeyEvent(keyEvent, doubleTapCompleted, isDismissNotificationEvent);
1550+
|| applyMediaButtonKeyEvent(
1551+
keyEvent,
1552+
doubleTapCompleted,
1553+
isDismissNotificationEvent,
1554+
isEventSourceMediaButtonReceiver);
15171555
}
15181556

15191557
private boolean applyMediaButtonKeyEvent(
1520-
KeyEvent keyEvent, boolean doubleTapCompleted, boolean isDismissNotificationEvent) {
1558+
KeyEvent keyEvent,
1559+
boolean doubleTapCompleted,
1560+
boolean isDismissNotificationEvent,
1561+
boolean mustStartForegroundService) {
15211562
ControllerInfo controllerInfo = checkNotNull(instance.getMediaNotificationControllerInfo());
15221563
Runnable command;
15231564
int keyCode = keyEvent.getKeyCode();
@@ -1530,10 +1571,15 @@ private boolean applyMediaButtonKeyEvent(
15301571
command =
15311572
getPlayerWrapper().getPlayWhenReady()
15321573
? () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER)
1533-
: () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
1574+
: () ->
1575+
sessionStub.playForControllerInfo(
1576+
controllerInfo, UNKNOWN_SEQUENCE_NUMBER, mustStartForegroundService);
15341577
break;
15351578
case KEYCODE_MEDIA_PLAY:
1536-
command = () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
1579+
command =
1580+
() ->
1581+
sessionStub.playForControllerInfo(
1582+
controllerInfo, UNKNOWN_SEQUENCE_NUMBER, mustStartForegroundService);
15371583
break;
15381584
case KEYCODE_MEDIA_PAUSE:
15391585
command = () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
@@ -2115,7 +2161,8 @@ public void setPendingPlayPauseTask(ControllerInfo controllerInfo, KeyEvent keyE
21152161
applyMediaButtonKeyEvent(
21162162
keyEvent,
21172163
/* doubleTapCompleted= */ false,
2118-
/* isDismissNotificationEvent= */ false);
2164+
/* isDismissNotificationEvent= */ false,
2165+
/* mustStartForegroundService= */ false);
21192166
} else {
21202167
sessionLegacyStub.handleMediaPlayPauseOnHandler(
21212168
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
@@ -752,19 +752,22 @@ public void play(@Nullable IMediaController caller, int sequenceNumber) {
752752
@Nullable
753753
ControllerInfo controller = connectedControllersManager.getController(caller.asBinder());
754754
if (controller != null) {
755-
playForControllerInfo(controller, sequenceNumber);
755+
playForControllerInfo(controller, sequenceNumber, /* mustStartForegroundService= */ false);
756756
}
757757
}
758758

759-
public void playForControllerInfo(ControllerInfo controller, int sequenceNumber) {
759+
public void playForControllerInfo(
760+
ControllerInfo controller, int sequenceNumber, boolean mustStartForegroundService) {
760761
queueSessionTaskWithPlayerCommandForControllerInfo(
761762
controller,
762763
sequenceNumber,
763764
COMMAND_PLAY_PAUSE,
764765
sendSessionResultWhenReady(
765766
(session, theController, sequenceId) ->
766767
session.handleMediaControllerPlayRequest(
767-
theController, /* callOnPlayerInteractionFinished= */ false)));
768+
theController,
769+
/* callOnPlayerInteractionFinished= */ false,
770+
/* mustStartForegroundService= */ mustStartForegroundService)));
768771
}
769772

770773
@Override

0 commit comments

Comments
 (0)