media icon indicating copy to clipboard operation
media copied to clipboard

ForegroundServiceDidNotStartInTimeException when playing from app widgets

Open mardous opened this issue 1 month ago • 6 comments

Version

Media3 main branch

More version details

No response

Description of the bug

First, I'm sorry if this is something silly or has already been reported and fixed, but so far I've already reviewed all the related issues, and they appear to be caused by completely different situations, so they won't help resolve our problem. Furthermore, there's another one that might be more related, but it hasn't been active for a long time.

Said that, our app was recently migrated to Media3 from the old Media framework. One of its long-standing features has been the ability to resume playback from widgets on the home screen. This means that if the app is completely closed, pressing any button on the widget should cause the app to load the queue and all previous states and resume playback. This worked well until the migration to Media3. Now, even though the app starts playing, for some reason Media3 doesn't display the media notification, and the app stops after a few seconds. This bug has been reported by a large number of users recently, so we doubt it's specific to a particular Android version or manufacturer.

This is how our code looks:

MusicAppWidget:

private fun RemoteViews.setupButtons(context: Context) {
    val action = Intent(context, MainActivity::class.java)
        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)

    val pendingIntent = PendingIntent.getActivity(context, 0, action, PendingIntent.FLAG_IMMUTABLE)
    setOnClickPendingIntent(R.id.image, pendingIntent)
    setOnClickPendingIntent(R.id.media_titles, pendingIntent)

    setButtonAction(context, R.id.button_prev, PlaybackService.ACTION_PREVIOUS)
    setButtonAction(context, R.id.button_next, PlaybackService.ACTION_NEXT)
    setButtonAction(context, R.id.button_toggle_play_pause, PlaybackService.ACTION_TOGGLE_PAUSE)
}

private fun RemoteViews.setButtonAction(context: Context, buttonId: Int, action: String) {
    setOnClickPendingIntent(
        buttonId,
        PendingIntent.getForegroundService(
            context,
            0,
            Intent(context, PlaybackService::class.java).setAction(action),
            PendingIntent.FLAG_IMMUTABLE
        )
    )
}

PlaybackService:

override fun onCreate() {
    super.onCreate()
    // ...

    // Restore playback state (queue, shuffle mode, repeat mode, etc.)
    persistentStorage = PersistentStorage(this, serviceScope, player)
    persistentStorage.restoreState { items, shuffleOrder ->

        // Restoration finished
        player.setMediaItems(items.mediaItems, items.startIndex, items.startPositionMs)
        player.prepare()
        if (player.shuffleModeEnabled && shuffleOrder != null) {
            player.exoPlayer.shuffleOrder = shuffleOrder
        }

        // Process pending commands
        pendingStartCommands.forEach { command -> processCommand(command) }
        pendingStartCommands.clear()
    }

    // ...
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    when (intent?.action) {
        ACTION_NEXT,
        ACTION_PREVIOUS,
        ACTION_TOGGLE_PAUSE -> {
            if (persistentStorage.restorationState.isRestored) {
                processCommand(intent)
            } else {
                pendingStartCommands.add(intent)
            }
            return START_STICKY
        }
    }
    return super.onStartCommand(intent, flags, startId)
}

private fun processCommand(command: Intent) {
    when (command.action) {
        ACTION_TOGGLE_PAUSE -> if (isPlaying) {
            player.pause()
        } else {
            player.play()
        }
        ACTION_PREVIOUS -> player.seekToPrevious()
        ACTION_NEXT -> player.seekToNext()
    }
}

Therefore, I'd like to know if Media3 isn't designed for this (resuming playback from home widgets) or if it's a bug in the framework itself. We've made every possible change and run every test, even with Glance, and nothing seems to work.

It's also important to mention that we didn't override any default logic for handling notifications, and all of this has been delegated to Media3 itself as the default behavior.

Devices that reproduce the issue

Any device on which the app is installed

Devices that do not reproduce the issue

No response

Reproducible in the demo app?

No (It doesn't appear to have widgets support)

Reproduction steps

  1. Close the app completely, with no notifications or anything else active.
  2. Add one of the widgets to the home screen.
  3. Tap any of the widget's control buttons.

Expected result

The app starts playing from its last state and displays the media notification.

Actual result

The app starts playing without showing a notification, so the system stops it after a few seconds.

Media

Not applicable

Bug Report

  • [ ] You will email the zip file produced by adb bugreport to [email protected] after filing this issue.

mardous avatar Dec 07 '25 15:12 mardous

Thanks for your report.

I'm a bit unsure about why this doesn't work. Do you have a bug report that was taken just after the exception happens? that would be interesting to see.

Generally, the recommended way to start the service is to create a controller and connect to the service. My recommendation would be to try this instead of sending an Intent.

So instead of sending an Intent, I recommend to build a MediaController that connects to the service. This will start the service automatically and you can issue for instance a controller.play(). This would also prevent you from having to prepare the player in onCreate(). Preparing should generally not happen in onCreate() specifically if you are using a MediaLibraryService. I don't think this is the reason for the issue though.

If you can provide us with additional info like a bug report or a stack trace, this would be very helpful.

marcbaechinger avatar Dec 08 '25 09:12 marcbaechinger

Hi, thanks for the quick response.

Do you have a bug report that was taken just after the exception happens?

Yes, I just took one, I'll send it to the email address provided in a moment.

If you can provide us with additional info like a bug report or a stack trace, this would be very helpful.

Of course, I got one stack trace from a user:

Build version: 1.1.0
Current date: 2025-11-29 11:58:42
Device: INFINIX Infinix X669
OS version: Android 12 (SDK 31)

Stack trace:
android.app.ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{4678a9e u0 com.mardous.booming/.playback.PlaybackService}
at android.app.ActivityThread.throwRemoteServiceException(ActivityThread.java:2002)
at android.app.ActivityThread.access$2700(ActivityThread.java:277)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2227)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7996)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:553)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

So instead of sending an Intent, I recommend to build a MediaController that connects to the service. This will start the service automatically and you can issue for instance a controller.play()

I tried to do it that way but I'm not sure how to connect and/or use a MediaController from a widget.

Preparing should generally not happen in onCreate() specifically if you are using a MediaLibraryService

I didn't know that. Should I remove the call to player.prepare() then? I assume the UI-side MediaController will handle that afterward, right?

mardous avatar Dec 08 '25 17:12 mardous

I built a hacky widget for the Media3 session demo to understand better what the requirements are.

Widgets must use a broadcast receiver to execute code

I think my suggestion from above, that the recommended way to start the MediaSessionService is a cotnroller, is valid but infeasible for your use case. My understanding is that the only way to execute code from a widget is to send an Intent to a broadcast receiver. Because a broadcast receiver is not allowed to bind to a service by system restriction, you can't create a controller there to call a playback command on it as I suggested. While this is unfortunate, we can't change this restriction.

I tried the Intent apprach from a widget with sending an media playback Intent just as the notification would do that that we prepare for devices with API level before 31 (I think :)).

This seems to reliably work without changing code in the service. When the app is not running and a PLAY Intent is sent, the service is started and runs as a foreground service without the system complaining about starting a service from the background as per exemption in [6].

Service restrictions and widget user experience

The reason why I'm cautious with using Intents is that on recent API levels, there are restrictions around when starting a service is allowed. When using Intents an app needs to be careful, specifically with the first Intent when the service is currently not running.

In this state, the app MUST use PendingIntent.getForegroundService and send PLAY as the first command. With this:

  • the service is started as a foreground service. When simply using PendingIntent.getService the system would intercept this call as this is not allowed from the background (see [1]). This results in a button that does nothing which is confusing UX.
  • when started as a foreground service, the service must start playback so that MediaSessionService calls MediaSessionService.startForeground(Notification, ...). If this does not happen a ForegroundServiceDidNotStartInTimeException is thrown.

Once the service is started, other commands can be sent without issues, but this first Intent can cause issues.

Safe widget design

I'm totally not an expert in Widgets. From what I read a Widget doesn't have any state but it can receive broadcast intents to for instance change the layout similar to what it can do after the user would reconfigure something. Given my understanding is correct, a safe widget regarding foreground service, would have two visible states:

  1. State when the service is not running. It only offers a PlayButton with a PendingIntent to start playback. Easiest is probably to support playback resumption, as then a simple PLAY resumes playback from where the user left off the app.
  2. Once the service is playing, further commands can be safely added and will work.

I'm not sure what is the best way to send the information about the service starting to run to the widget. My guess would be an intent to the broadcast receiver (can be the widget provider itself). The broadcast receiver then can remove/add, disable/enable buttons accordingly. Hence the service could:

  • send an intent in onCreate to notify the widget about being started
  • send an intent in onDestroy() to notify the widget to notify that the service has terminated.

A solution for apps

Option 1: From the code samples you sent above, I can't see how a session is created. You'd need to create a media session with the player and then pass the session into yourService.addSession(session). I think that would work.

Option 2: A better solution would be that you use the same Intent that the notfication is using. There is not API for this. I mark this issue as an enhancement, because providing an API to create these Intents would be technically straightforward. The only use case for these would be widgets though.

For now, you can craft this yourself if you want. You can basically use this code which is part of DefaultActionFactory that is used for the notification on lower API levels. These intent are handled by MediaSessionService so you don't have to overrid onStartCommand. Instead Media3 does it for you. (from [4]):

private Intent getMediaButtonIntent(int mediaKeyCode) {
    Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
    intent.setComponent(new ComponentName(service, PlaybackService.class));
    intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, mediaKeyCode));
    return intent;
  }

You can look up media key codes from KeyEvent of the Android framework [5]. Ideally, use Media3 version 1.6.0 or higher ([3]).

refs

[1] Background start not allowed

2025-12-08 17:23:33.826  1750-1771  ActivityManager         android                              W  Background start not allowed: service Intent { act=android.intent.action.MEDIA_BUTTON flg=0x10000000 xflg=0x4 cmp=androidx.media3.demo.session/.PlaybackService (has extras) } to androidx.media3.demo.session/.PlaybackService from pid=-1 uid=10330 pkg=androidx.media3.demo.session startFg?=false

[2] ForegroundServiceDidNotStartInTimeException

android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{edc9922 u0 androidx.media3.demo.session/.PlaybackService c:androidx.media3.demo.session}

[3] https://developer.android.com/jetpack/androidx/releases/media3#1.6.0

[4] https://github.com/androidx/media/blob/release/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java#L142-L147

[5] https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE

[6] https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start#background-start-restriction-exemptions

marcbaechinger avatar Dec 08 '25 18:12 marcbaechinger

Great, it seems to be working now with the proposed solution. I really appreciate it, as this bug had been delaying app updates, so this is more than enough for now. Hopefully, this API will be made public at some point.

As I mentioned before, we were taking the opportunity to migrate the widgets to Glance, so the final code looks something like this:

ControlIconGlance(
    resId = if (playbackState.isPlaying) R.drawable.ic_pause_24dp else R.drawable.ic_play_24dp,
    tint = GlanceTheme.colors.onSurface,
    contentDescription = "Play/Pause",
    modifier = GlanceModifier
        .size(28.dp)
        .clickable(playbackAction(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
)

private fun playbackAction(context: Context, mediaKeyCode: Int): Action {
    val intent = Intent(Intent.ACTION_MEDIA_BUTTON)
    intent.setComponent(ComponentName(context, PlaybackService::class.java))
    intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, mediaKeyCode))
    return actionStartService(intent, isForegroundService = true)
}

From the code samples you sent above, I can't see how a session is created.

Here is the session's initialization code (this way, in case you're interested in seeing any other point in the service initialization process, although as I said, it already seems to be working.)

The broadcast receiver then can remove/add, disable/enable buttons accordingly

I understand, we already have a PlaybackState class that contains the player's current state and is sent to the widget. I think I could use that for this.

Thank you for your help.

mardous avatar Dec 08 '25 18:12 mardous

I actually think the approach I suggested above is not going to work I'm afraid.

I think it needs some changes in the URI that we assign to a session and that is used to recognize the session by URI. For the time being I can't think of a solution without changing the session code on Media3 side.

Without a change in Media3, I don't think apps can reuse the Intent used by the notification, because you'd need a valid session UI which you can't know [2].

While this works for the first intent, any further Intent would have to know the URI in [1] which is not possible I'm afraid.

We need to think about how this can be solved from the library side. For your case I think you can make it work with custom Intents and identify your session in a way that is safe for your app (which may be easy if there is only a single session).

[1] https://github.com/androidx/media/blob/release/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java#L218-L223

[2] https://github.com/androidx/media/blob/release/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java#L144

marcbaechinger avatar Dec 09 '25 14:12 marcbaechinger

Okay, I understand. I'll start testing and post the results here.

mardous avatar Dec 09 '25 15:12 mardous