media icon indicating copy to clipboard operation
media copied to clipboard

ForegroundServiceStartNotAllowedException when playing from a home screen widget

Open arlindiDev opened this issue 2 years ago • 13 comments

Due to the behavior changes of Android 12, the app crashes when I play a track then the app loses AudioFocus, for which I have a callback when the app gains audio focus AudioManager.AUDIOFOCUS_GAIN that's where the media player crashes The MediaNotificationHandler.java#L129 throws a ForegroundServiceStartNotAllowedException. I assume when the app loses audio focus stopForegroundServiceIfNeeded(); is called

Would we need to update the playing media to WorkManager or what is the official guide to support playing media while the app is in the background on Android 12?

  • AndroidX Media 3
  • Android version 12
  • Android device Pixel 5

arlindiDev avatar Dec 10 '21 16:12 arlindiDev

Hello, we have the exact same issue. What should we do?

Thanks!

marf avatar May 25 '22 09:05 marf

Can you describe in a bit more detail how you are handling audio focus? Are you handling it yourself in your app or are you using the internal audio focus support of ExoPlayer?

From the description above it looks like that playWhenReady changes which then stops the service from the foreground and later wants it to start in the foreground again when resuming after the audio focus is requested again. From this I conclude you are receiving a AudioManager.AUDIOFOCUS_LOSS.

If this is the case then it needs a user interaction from the foreground to start playback again, so you can't just automatically start playing again.

If my above guess is not correct, then you are receiving a AudioManager.AUDIOFOCUS_LOSS_TRANSIENT. In this case you will automatically get the audio focus back after the transient loss. In such a case you should not stop the service from foreground. instead keep it in the foreground and when you get the focus back you can continue playback without needing to start in foreground.

Can you let me know what your setup is in detail and who is calling player.play after the audio focus loss?

marcbaechinger avatar May 25 '22 15:05 marcbaechinger

We are handling the audio focus ourselves with the following code when a user plays a song:

private void requestFocus() {
        if(hasAudioFocus) return;
        Log.d(Utils.LOG, "Requesting audio focus...");

        AudioManager manager = (AudioManager)service.getSystemService(Context.AUDIO_SERVICE);
        int r;

        if(manager == null) {
            r = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
        } else if(Build.VERSION.SDK_INT >= 26) {
            focus = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
                    .setOnAudioFocusChangeListener(this)
                    .setAudioAttributes(new AudioAttributes.Builder()
                            .setUsage(AudioAttributes.USAGE_MEDIA)
                            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                            .build())
                    .setWillPauseWhenDucked(alwaysPauseOnInterruption)
                    .build();

            r = manager.requestAudioFocus(focus);
        } else {
            //noinspection deprecation
            r = manager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
        }

        hasAudioFocus = r == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
    }

In our foreground service we implemented the handling of the audio focus change that sends events to our javascript side (we have a react native app):

    @Override
    public void onAudioFocusChange(int focus) {
        Log.d(Utils.LOG, "onDuck");

        boolean permanent = false;
        boolean paused = false;
        boolean ducking = false;

        switch(focus) {
            case AudioManager.AUDIOFOCUS_GAIN:
                paused = false;
                break;
            case AudioManager.AUDIOFOCUS_LOSS:
                permanent = true;
                paused = true;
                abandonFocus();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                paused = true;
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                if (alwaysPauseOnInterruption)
                    paused = true;
                else
                    ducking = true;
                break;
            default:
                break;
        }

        if (ducking) {
            //playback.setVolumeMultiplier(0.5F);
            wasDucking = true;
        } else if (wasDucking) {
            //playback.setVolumeMultiplier(1.0F);
            wasDucking = false;
        }

        Bundle bundle = new Bundle();
        bundle.putBoolean("permanent", permanent);
        bundle.putBoolean("paused", paused);
        bundle.putBoolean("ducking", ducking);
        service.emit(MusicEvents.BUTTON_DUCK, bundle);
    }

I do not know how the users are able to reproduce the issue since we see in the crash logs this stacktrace:

Fatal Exception: java.lang.RuntimeException: Unable to create service com.guichaguri.trackplayer.service.MusicService: android.app.ForegroundServiceStartNotAllowedException: Service.startForeground() not allowed due to mAllowStartForeground false: service com.esound/com.guichaguri.trackplayer.service.MusicService
       at android.app.ActivityThread.handleCreateService(ActivityThread.java:4953)
       at android.app.ActivityThread.access$1800(ActivityThread.java:310)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2300)
       at android.os.Handler.dispatchMessage(Handler.java:106)
       at android.os.Looper.loopOnce(Looper.java:226)
       at android.os.Looper.loop(Looper.java:313)
       at android.app.ActivityThread.main(ActivityThread.java:8663)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:567)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1135)

This happens only when the app is in the background and we guessed that it might happen when the users are listening the music, they receive a call and the app loses the audio focus and then after they ended up the call the click play again in the notification to resume the music and the app tries to start the foreground service again from the background and this exception happens.

This is the code we use when a user taps the play button in the notification and we requestFocus again:

    public void onPlay() {
        Log.d(Utils.LOG, "onPlay");

        requestFocus();

        if(!receivingNoisyEvents) {
            receivingNoisyEvents = true;
            service.registerReceiver(noisyReceiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
        }

        if(!wakeLock.isHeld()) wakeLock.acquire();

        metadata.setActive(true);
    }

Thanks!

marf avatar May 25 '22 16:05 marf

I'm also developing music player and already faced the same problem. Classical scheme is to synchronize active playing state with service foreground state. Notification appears at the first play and keeps visible even when playback is stopped for the sake of usability.

So the only way to fix the problem for Android 12 is to keep foreground state forever (along with notification of course). As a user I'm quite disappointed with applications using that scheme even for old androids.

vitamin-caig avatar May 25 '22 16:05 vitamin-caig

@vitamin-caig are you suggesting to never call stopForeground when we receive an interruption and to always keep the foreground service active?

What if the operating system stops the foreground service itself? I read somewhere that if you stay in a call for example for more than one minute Android may stop the foreground service.

marf avatar May 25 '22 16:05 marf

@marf I would suggest to get rid of such a hard limitation in Android:) At least for multimedia playback services.

To be serious, of course this is impossible. But androidx can give an example of proper implementation for that cases. For example via WorkManager (extending it if required).

Of course, OS can stop your service during call. Then you just click on 'Play' button on app's notification and all is going ahead. But now it's also impossible for Android 12.

vitamin-caig avatar May 25 '22 16:05 vitamin-caig

@vitamin-caig so do you confirm that if a user clicks the 'Play' button on app's notification this exception may happen?

If the OS stops the service the notification associated should disappear too, right? Hence the user will not be able to click the 'Play' button on the notification and get the exception if I am not mistaken.

Because I tried with my phone on Android 12 and the issue is not happening but it seems to happen for other users (don't know how they manage to make the issue happen).

Where I can find an implementation of the notification via WorkManager?

marf avatar May 25 '22 16:05 marf

so do you confirm that if a user clicks the 'Play' button on app's notification this exception may happen?

Unfortunately, yes. Moreover, the same distilled issue happens when user tries to start playback from home screen widget - usually it uses intents bound directly to service. So no UI -> no foreground -> no permission for service to move itself to foreground state

If the OS stops the service the notification associated should disappear too, right?

It's up to an app. You may keep notification visible after service disabling its foreground mode. It's just convenient for users.

Where I can find an implementation of the notification via WorkManager?

I was pretty sure you are from androidx team so can contact with other maintainers:)

vitamin-caig avatar May 25 '22 17:05 vitamin-caig

the same distilled issue happens

I hope the Android team patches this issue otherwise how can developers implements widgets or notification that are bounded to a service using Android 12? Do we need to use a WorkManager?

I have also put this in the manifest for the MusicService specifying also the android:foregroundServiceType as "mediaPlayback" thinking maybe the OS could avoid arising this exception if the foregroundServiceType is mediaPlayback but without any success.

        <service android:name=".service.MusicService"
            android:enabled="true"
            android:foregroundServiceType="mediaPlayback"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MEDIA_BUTTON" />
            </intent-filter>
        </service>

marf avatar May 25 '22 17:05 marf

So the only way to fix the problem for Android 12 is to keep foreground state forever

I don't think this is correct. Can you let me know your reasoning why this is the case?

On Android 12 the System UI (notification) is sending a PendingIntent to your service and this pending intent creates a window of some seconds while your app is on a exemption list and hence is allowed to start a foreground service from the background.

You find an appropriate entry in the log in such a case which reports an allow list entry for 30 seconds:

2022-02-25 16:37:51.378 1824-8258/? I/ActivityManager: Background started FGS: Allowed [callingPackage: androidx.media3.demo.session; callingUid: 10314; uidState: SVC ; intent: Intent { act=android.intent.action.MEDIA_BUTTON cmp=androidx.media3.demo.session/.PlaybackService (has extras) }; code:START_ACTIVITY_FLAG; tempAllowListReason:<setPendingIntentAllowlistDuration,reason:NotificationManagerService,pendingintent:u0a229:android.intent.action.MEDIA_BUTTON,reasonCode:NOTIFICATION_SERVICE,duration:30000,callingUid:10229>; targetSdkVersion:33; callerTargetSdkVersion:33; startForegroundCount:8; bindFromPackage:null]

Classical scheme is to synchronize active playing state with service foreground state.

I agree to that.

  • When you receive a pause command you should stop the service from the foreground. The service is now running in background until the system decides to stop it (around 2 minutes). You stopForeground(/* removeNotification= */ false) and post the updated notification with the notification manager (updated notification now shows a Play button, for instance).

  • When then the user presses the play button within these 2 minutes from the notification. The PendingIntent is sent to your service and the service can be started in the foreground while on the allowlist. So you put the service into the foreground again.

That's how the Media3 MediaSessionService/MediaLibraryService is doing it and I think this works on S and as well on T. We removed all the ForegroundServiceStartNotAllowedException we were seeing and I'm actually confident it works and works as intented with the pattern from above. I tested this and we were able to work around all issues with the most recent Media3 sources and with S and the most recent T build. For this to work you would have to build Media3 from the main branch. The binary releases don't have all the fixes needed to work for targeting API 29 through 33 and working on S and T (media3-1.0.0-beta01 will have it and it comes soon).

The problem with the audio focus is that you don't have the exemption from the PendingIntent because you want to start the service in the foreground again, but because this is triggered by the audio focus and not a PendingIntent you are not on the allow list and you crash.

So I think when handling the audio focus and you get a loss that is tentative, then you should keep the service in the foreground until you receive the audio focus again. If you get a permanent focus loss, then you should stop foreground and not start automatically again. Instead a user needs to resume from the foreground (like from the app or notification) which again gives you the chance to start in the foreground.

I believe that the case from above

case AudioManager.AUDIOFOCUS_GAIN:

only happens automatically if you lose the audio focus tentatively. Can you let me know if this is correct or if you experience it differently?

Please let us know whether you think I am wrong. :) We really want to make sure this works according to the best practices and we may have missed some corner cases that you folks see and for which we may have to file bugs internally to make sure it is working nicely on all OS version. Until that I would insist I am right and the pattern above should be used and works. :D

marcbaechinger avatar May 25 '22 17:05 marcbaechinger

android:foregroundServiceType

Yes, confirming this does not help.

marcbaechinger avatar May 25 '22 18:05 marcbaechinger

Thanks @marcbaechinger!

The foreground service is restarted only when the user press the play button again from the notification. We do not restart it again automatically so we should already do that. The onPlay method is only called when the user press the play button from the notification, the music is not resumed automatically when the call finishes it is the user which manually resumes it :)

marf avatar May 26 '22 10:05 marf

Cool! A user pressing a button on the notification will allow you to start in the foreground as per exemption.

As far as this case

case AudioManager.AUDIOFOCUS_GAIN:
                paused = false;
                break;
            

is not called from the background you should not crash. If this case is triggered in the background (like when getting back the focus automatically after a transient loss) and paused = false makes your app call player.play() and then service.startForeground(int, not) later, then you may have the crashes you see in the crashlog.

marcbaechinger avatar May 26 '22 11:05 marcbaechinger

I'm closing this issue with a note:

Starting with Android 12 a service needs to be started in the foreground from the foreground. Pausing the player causes the service to be taken off the foreground. If this hap[pens because of a custom implementation of audio focus, playback can not be resumed from the background, because this start the service in the foreground again which causes the ForegroundServiceStartNotAllowedException.

ExoPlayer provides integrated support for audio focus management. The implementation keeps playWhenReady true when playback is suppressed and provides a suppression reason. With this, the service is not taken out of the foreground when playback is suppressed which avoids the need for starting in the foreground when playback suppression is removed.

For these reasons audio focus support provided by ExoPlayer is the preferred approach in such a scenario.

Please re-open if you think that's indicated.

marcbaechinger avatar Sep 30 '22 19:09 marcbaechinger