Skip to content

Commit 9090fc7

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 1db9c93 commit 9090fc7

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
@@ -1932,6 +1932,14 @@ default ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
19321932
* automatically as required. Any additional initial setup like setting playback speed, repeat
19331933
* mode or shuffle mode can be done from within this callback.
19341934
*
1935+
* <p>If the returned list is empty or an exception is returned, and the request to resume
1936+
* playback came from {@link MediaButtonReceiver}, an {@link IllegalStateException} will be
1937+
* thrown. This is because the {@link MediaButtonReceiver} has already requested a foreground
1938+
* service start, and it's not possible to avoid a {@code
1939+
* ForegroundServiceDidNotStartInTimeException} anymore. To avoid a crash as result, override
1940+
* {@link MediaButtonReceiver#shouldStartForegroundService(Context, Intent)} to return {@code
1941+
* false} if there is nothing to resume. This will avoid requesting a foreground service start.
1942+
*
19351943
* <p>The method will only be called if the {@link Player} has {@link
19361944
* Player#COMMAND_GET_CURRENT_MEDIA_ITEM} and either {@link Player#COMMAND_SET_MEDIA_ITEM} or
19371945
* {@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
@@ -1141,7 +1141,9 @@ protected MediaSessionServiceLegacyStub getLegacyBrowserService() {
11411141
* @param controller The controller requesting to play.
11421142
*/
11431143
/* package */ ListenableFuture<SessionResult> handleMediaControllerPlayRequest(
1144-
ControllerInfo controller, boolean callOnPlayerInteractionFinished) {
1144+
ControllerInfo controller,
1145+
boolean callOnPlayerInteractionFinished,
1146+
boolean mustStartForegroundService) {
11451147
SettableFuture<SessionResult> sessionFuture = SettableFuture.create();
11461148
ListenableFuture<Boolean> playRequestedFuture = onPlayRequested();
11471149
playRequestedFuture.addListener(
@@ -1195,6 +1197,26 @@ public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) {
11951197
callWithControllerForCurrentRequestSet(
11961198
controllerForRequest,
11971199
() -> {
1200+
if (mediaItemsWithStartPosition.mediaItems.isEmpty()) {
1201+
if (mustStartForegroundService) {
1202+
applicationHandler.postAtFrontOfQueue(
1203+
() -> {
1204+
throw new IllegalArgumentException(
1205+
"Callback.onPlaybackResumption must return non-empty"
1206+
+ " MediaItemsWithStartPosition if started from a"
1207+
+ " media button receiver. If there is nothing to"
1208+
+ " resume playback with, override"
1209+
+ " MediaButtonReceiver.shouldStartForegroundService()"
1210+
+ " and return false.");
1211+
});
1212+
return;
1213+
}
1214+
Log.w(
1215+
TAG,
1216+
"onPlaybackResumption() is trying to resume with empty"
1217+
+ " playlist, this will make the resumption notification"
1218+
+ " appear broken.");
1219+
}
11981220
MediaUtils.setMediaItemsWithStartIndexAndPosition(
11991221
playerWrapper, mediaItemsWithStartPosition);
12001222
Util.handlePlayButtonAction(playerWrapper);
@@ -1209,21 +1231,33 @@ public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) {
12091231

12101232
@Override
12111233
public void onFailure(Throwable t) {
1234+
RuntimeException e;
12121235
if (t instanceof UnsupportedOperationException) {
1213-
Log.w(
1214-
TAG,
1215-
"UnsupportedOperationException: Make sure to implement"
1216-
+ " MediaSession.Callback.onPlaybackResumption() if you add a media"
1217-
+ " button receiver to your manifest or if you implement the recent"
1218-
+ " media item contract with your MediaLibraryService.",
1219-
t);
1236+
e =
1237+
new UnsupportedOperationException(
1238+
"Make sure to implement MediaSession.Callback.onPlaybackResumption()"
1239+
+ " if you add a media button receiver to your manifest or if you"
1240+
+ " implement the recent media item contract with your"
1241+
+ " MediaLibraryService.",
1242+
t);
12201243
} else {
1221-
Log.e(
1222-
TAG,
1223-
"Failure calling MediaSession.Callback.onPlaybackResumption(): "
1224-
+ t.getMessage(),
1225-
t);
1244+
e =
1245+
new IllegalStateException(
1246+
"Failure calling MediaSession.Callback.onPlaybackResumption(): "
1247+
+ t.getMessage(),
1248+
t);
1249+
}
1250+
if (mustStartForegroundService) {
1251+
// MediaButtonReceiver already called startForegroundService(). If we do not
1252+
// crash ourselves, ForegroundServiceDidNotStartInTimeException will do it
1253+
// for us. Let's at least get a useful stack trace out there.
1254+
applicationHandler.postAtFrontOfQueue(
1255+
() -> {
1256+
throw e;
1257+
});
1258+
return;
12261259
}
1260+
Log.e(TAG, Objects.requireNonNull(Log.getThrowableString(e)));
12271261
// Play as requested even if playback resumption fails.
12281262
Util.handlePlayButtonAction(playerWrapper);
12291263
sessionFuture.set(new SessionResult(SessionResult.RESULT_SUCCESS));
@@ -1482,13 +1516,13 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
14821516
// Double tap detection.
14831517
int keyCode = keyEvent.getKeyCode();
14841518
boolean isTvApp = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
1519+
boolean isEventSourceMediaButtonReceiver =
1520+
callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION;
14851521
boolean doubleTapCompleted = false;
14861522
switch (keyCode) {
14871523
case KEYCODE_MEDIA_PLAY_PAUSE:
14881524
case KEYCODE_HEADSETHOOK:
1489-
if (isTvApp
1490-
|| callerInfo.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION
1491-
|| keyEvent.getRepeatCount() != 0) {
1525+
if (isTvApp || isEventSourceMediaButtonReceiver || keyEvent.getRepeatCount() != 0) {
14921526
// Double tap detection is only for mobile apps that receive a media button event from
14931527
// external sources (for instance Bluetooth) and excluding long press (repeatCount > 0).
14941528
mediaPlayPauseKeyHandler.flush();
@@ -1528,11 +1562,18 @@ private void handleAvailablePlayerCommandsChanged(Player.Commands availableComma
15281562
intent.getBooleanExtra(
15291563
MediaNotification.NOTIFICATION_DISMISSED_EVENT_KEY, /* defaultValue= */ false);
15301564
return keyEvent.getRepeatCount() > 0
1531-
|| applyMediaButtonKeyEvent(keyEvent, doubleTapCompleted, isDismissNotificationEvent);
1565+
|| applyMediaButtonKeyEvent(
1566+
keyEvent,
1567+
doubleTapCompleted,
1568+
isDismissNotificationEvent,
1569+
isEventSourceMediaButtonReceiver);
15321570
}
15331571

15341572
private boolean applyMediaButtonKeyEvent(
1535-
KeyEvent keyEvent, boolean doubleTapCompleted, boolean isDismissNotificationEvent) {
1573+
KeyEvent keyEvent,
1574+
boolean doubleTapCompleted,
1575+
boolean isDismissNotificationEvent,
1576+
boolean mustStartForegroundService) {
15361577
ControllerInfo controllerInfo = checkNotNull(instance.getMediaNotificationControllerInfo());
15371578
Runnable command;
15381579
int keyCode = keyEvent.getKeyCode();
@@ -1546,10 +1587,15 @@ private boolean applyMediaButtonKeyEvent(
15461587
command =
15471588
getPlayerWrapper().getPlayWhenReady()
15481589
? () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER)
1549-
: () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
1590+
: () ->
1591+
sessionStub.playForControllerInfo(
1592+
controllerInfo, UNKNOWN_SEQUENCE_NUMBER, mustStartForegroundService);
15501593
break;
15511594
case KEYCODE_MEDIA_PLAY:
1552-
command = () -> sessionStub.playForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
1595+
command =
1596+
() ->
1597+
sessionStub.playForControllerInfo(
1598+
controllerInfo, UNKNOWN_SEQUENCE_NUMBER, mustStartForegroundService);
15531599
break;
15541600
case KEYCODE_MEDIA_PAUSE:
15551601
command = () -> sessionStub.pauseForControllerInfo(controllerInfo, UNKNOWN_SEQUENCE_NUMBER);
@@ -2164,7 +2210,8 @@ public void setPendingPlayPauseTask(ControllerInfo controllerInfo, KeyEvent keyE
21642210
applyMediaButtonKeyEvent(
21652211
keyEvent,
21662212
/* doubleTapCompleted= */ false,
2167-
/* isDismissNotificationEvent= */ false);
2213+
/* isDismissNotificationEvent= */ false,
2214+
/* mustStartForegroundService= */ false);
21682215
} else {
21692216
sessionLegacyStub.handleMediaPlayPauseOnHandler(
21702217
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
@@ -852,7 +852,9 @@ private void dispatchSessionTaskWithPlayRequest() {
852852
controller -> {
853853
ListenableFuture<SessionResult> resultFuture =
854854
sessionImpl.handleMediaControllerPlayRequest(
855-
controller, /* callOnPlayerInteractionFinished= */ true);
855+
controller,
856+
/* callOnPlayerInteractionFinished= */ true,
857+
/* mustStartForegroundService= */ false);
856858
Futures.addCallback(
857859
resultFuture,
858860
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
@@ -761,19 +761,22 @@ public void play(@Nullable IMediaController caller, int sequenceNumber) {
761761
@Nullable
762762
ControllerInfo controller = connectedControllersManager.getController(caller.asBinder());
763763
if (controller != null) {
764-
playForControllerInfo(controller, sequenceNumber);
764+
playForControllerInfo(controller, sequenceNumber, /* mustStartForegroundService= */ false);
765765
}
766766
}
767767

768-
public void playForControllerInfo(ControllerInfo controller, int sequenceNumber) {
768+
public void playForControllerInfo(
769+
ControllerInfo controller, int sequenceNumber, boolean mustStartForegroundService) {
769770
queueSessionTaskWithPlayerCommandForControllerInfo(
770771
controller,
771772
sequenceNumber,
772773
COMMAND_PLAY_PAUSE,
773774
sendSessionResultWhenReady(
774775
(session, theController, sequenceId) ->
775776
session.handleMediaControllerPlayRequest(
776-
theController, /* callOnPlayerInteractionFinished= */ false)));
777+
theController,
778+
/* callOnPlayerInteractionFinished= */ false,
779+
/* mustStartForegroundService= */ mustStartForegroundService)));
777780
}
778781

779782
@Override

0 commit comments

Comments
 (0)