Skip to content

Commit aa97638

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 73a463b commit aa97638

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
@@ -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: 68 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));
@@ -1468,13 +1502,13 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
14681502
// Double tap detection.
14691503
int keyCode = keyEvent.getKeyCode();
14701504
boolean isTvApp = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
1505+
boolean isEventSourceMediaButtonReceiver =
1506+
callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION;
14711507
boolean doubleTapCompleted = false;
14721508
switch (keyCode) {
14731509
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
14741510
case KeyEvent.KEYCODE_HEADSETHOOK:
1475-
if (isTvApp
1476-
|| callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION
1477-
|| keyEvent.getRepeatCount() != 0) {
1511+
if (isTvApp || isEventSourceMediaButtonReceiver || keyEvent.getRepeatCount() != 0) {
14781512
// Double tap detection is only for mobile apps that receive a media button event from
14791513
// external sources (for instance Bluetooth) and excluding long press (repeatCount > 0).
14801514
mediaPlayPauseKeyHandler.flush();
@@ -1514,11 +1548,18 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
15141548
intent.getBooleanExtra(
15151549
MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY, /* defaultValue= */ false);
15161550
return keyEvent.getRepeatCount() > 0
1517-
|| applyMediaButtonKeyEvent(keyEvent, doubleTapCompleted, isDismissNotificationEvent);
1551+
|| applyMediaButtonKeyEvent(
1552+
keyEvent,
1553+
doubleTapCompleted,
1554+
isDismissNotificationEvent,
1555+
isEventSourceMediaButtonReceiver);
15181556
}
15191557

15201558
private boolean applyMediaButtonKeyEvent(
1521-
KeyEvent keyEvent, boolean doubleTapCompleted, boolean isDismissNotificationEvent) {
1559+
KeyEvent keyEvent,
1560+
boolean doubleTapCompleted,
1561+
boolean isDismissNotificationEvent,
1562+
boolean mustStartForegroundService) {
15221563
ControllerInfo controllerInfo = checkNotNull(instance.getMediaNotificationControllerInfo());
15231564
Runnable command;
15241565
int keyCode = keyEvent.getKeyCode();
@@ -1531,10 +1572,15 @@ private boolean applyMediaButtonKeyEvent(
15311572
command =
15321573
getPlayerWrapper().getPlayWhenReady()
15331574
? () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER)
1534-
: () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
1575+
: () ->
1576+
sessionStub.playForControllerInfo(
1577+
controllerInfo, UNKNOWN_SEQUENCE_NUMBER, mustStartForegroundService);
15351578
break;
15361579
case KEYCODE_MEDIA_PLAY:
1537-
command = () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
1580+
command =
1581+
() ->
1582+
sessionStub.playForControllerInfo(
1583+
controllerInfo, UNKNOWN_SEQUENCE_NUMBER, mustStartForegroundService);
15381584
break;
15391585
case KEYCODE_MEDIA_PAUSE:
15401586
command = () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
@@ -2116,7 +2162,8 @@ public void setPendingPlayPauseTask(ControllerInfo controllerInfo, KeyEvent keyE
21162162
applyMediaButtonKeyEvent(
21172163
keyEvent,
21182164
/* doubleTapCompleted= */ false,
2119-
/* isDismissNotificationEvent= */ false);
2165+
/* isDismissNotificationEvent= */ false,
2166+
/* mustStartForegroundService= */ false);
21202167
} else {
21212168
sessionLegacyStub.handleMediaPlayPauseOnHandler(
21222169
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
@@ -750,19 +750,22 @@ public void play(@Nullable IMediaController caller, int sequenceNumber) {
750750
@Nullable
751751
ControllerInfo controller = connectedControllersManager.getController(caller.asBinder());
752752
if (controller != null) {
753-
playForControllerInfo(controller, sequenceNumber);
753+
playForControllerInfo(controller, sequenceNumber, /* mustStartForegroundService= */ false);
754754
}
755755
}
756756

757-
public void playForControllerInfo(ControllerInfo controller, int sequenceNumber) {
757+
public void playForControllerInfo(
758+
ControllerInfo controller, int sequenceNumber, boolean mustStartForegroundService) {
758759
queueSessionTaskWithPlayerCommandForControllerInfo(
759760
controller,
760761
sequenceNumber,
761762
COMMAND_PLAY_PAUSE,
762763
sendSessionResultWhenReady(
763764
(session, theController, sequenceId) ->
764765
session.handleMediaControllerPlayRequest(
765-
theController, /* callOnPlayerInteractionFinished= */ false)));
766+
theController,
767+
/* callOnPlayerInteractionFinished= */ false,
768+
/* mustStartForegroundService= */ mustStartForegroundService)));
766769
}
767770

768771
@Override

0 commit comments

Comments
 (0)