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.
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:
preloadInForeground()
using a dispatcher that results in preload()
being called in a short while, but not immediatelypreload()
is called, you call sendAddDownload()
DownloadService
tries to start the service, but you are in the background, so hijinks ensueDoes 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.