androidexoplayerandroid-media3

Starting an Android service only when app is in foreground


We want to start androidx.media3.exoplayer.offline.DownloadService as a non-foreground service. We only want to start the service if the app/process is in the foreground. If the app/process is not in the foreground we would expect to get a android.app.BackgroundServiceStartNotAllowedException so we need a way to detect when the app/process is in the foreground.

Seems like it should be pretty straightforward according to the ProcessLifecycleOwner docs.

It is useful for use cases where you would like to react on your app coming to the foreground or going to the background and you don't need a milliseconds accuracy in receiving lifecycle events.

So we implemented it like the code below. The preload function actually starts the service.

    suspend fun preloadInForeground(
        lifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
    ) {
        lifecycleOwner.withStateAtLeast(Lifecycle.State.STARTED) {
            preload()
        }
    }

The code that actually starts the DownloadService looks like this:

DownloadService
    .sendAddDownload(context, MyAppDownloadService::class.java, downloadRequest, /* foreground= */ false)

Unfortunately this doesn't seem to work as advertised. In production we had quite a few crash clusters. Here is an example stack trace with most of the app-specific code omitted:

android.app.BackgroundServiceStartNotAllowedException: Not allowed to start service Intent { act=androidx.media3.exoplayer.downloadService.action.ADD_DOWNLOAD cmp=**OMITTED** (has extras) }: app is in background uid UidRecord{X Y TPSL idle change:procstateprocadj procs:0 seq(Z,ZZ)}
        at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1918)
        at android.app.ContextImpl.startService(ContextImpl.java:1874)
        at android.content.ContextWrapper.startService(ContextWrapper.java:827)
        at androidx.media3.exoplayer.offline.DownloadService.startService(DownloadService:874)
        at androidx.media3.exoplayer.offline.DownloadService.sendAddDownload(DownloadService:432)
        at **OMITTED**.preload(**OMITTED**:43)
        ... *********** APP CODE OMITTED
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(BaseContinuationImpl:33)
        at kotlinx.coroutines.internal.DispatchedContinuation.resumeUndispatchedWith$kotlinx_coroutines_core(DispatchedContinuation:252)
        at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuationKt:278)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(CancellableKt:26)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(CancellableKt:21)
        at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart:88)
        at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine:123)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(BuildersKt__Builders_commonKt:52)
        at kotlinx.coroutines.BuildersKt.launch(BuildersKt:1)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(BuildersKt__Builders_commonKt:43)
        at kotlinx.coroutines.BuildersKt.launch$default(BuildersKt:1)
        at 
        ... *********** APP CODE OMITTED
        at androidx.lifecycle.WithLifecycleStateKt$suspendWithStateAtLeastUnchecked$2$observer$1.onStateChanged(WithLifecycleStateKt:182)
        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry:314)
        at androidx.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry:251)
        at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry:287)
        at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry:136)
        at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry:119)
        at androidx.lifecycle.ReportFragment$Companion.dispatch$lifecycle_runtime_release(ReportFragment:192)
        at androidx.lifecycle.ReportFragment$LifecycleCallbacks.onActivityPostStarted(ReportFragment:121)
        at android.app.Activity.dispatchActivityPostStarted(Activity.java:1520)
        at android.app.Activity.performStart(Activity.java:8662)
        at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3945)
        at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221)
        at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2426)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:211)
        at android.os.Looper.loop(Looper.java:300)
        at android.app.ActivityThread.main(ActivityThread.java:8503)
        at java.lang.reflect.Method.invoke(Method.java:-2)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:561)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:954)

Does anyone know of the correct, or at least a more reliable way, to run code only when the app/process is in the foreground?

The alternative would be to just catch and swallow the exception, but that would mean that the service will sometimes not get started (i.e. best effort). That could work for our use case but would like to avoid if possible.


Solution

  • So we implemented it like the code below

    withStateAtLeast() is a suspend fun, as is preloadInForeground() and possibly preload(). In particular, withStateAtLeast() says "sometime after the lifecycle has reached this state, schedule a coroutine to run this block". When that block runs will be dependent not only on the lifecycle but also on the choice of coroutine dispatcher and the state of that dispatcher's thread pool.

    So, imagine the following series of events:

    Does anyone know of the correct [way] to run code only when the app/process is in the foreground?

    It is arguably impossible, especially when you did not write the code. The ExoPlayer folks can do whatever they want in sendAddDownload(), for example. Right now, that is synchronous, though.

    Your best chance is to avoid all coroutines or other threading as part of the work. Reimplement preloadInForeground() to do the work immediately if right now we are in the foreground, and do something else if you're not in the foreground.