androidx / media

Jetpack Media3 support libraries for media use cases, including ExoPlayer, an extensible media player for Android
https://developer.android.com/media/media3
Apache License 2.0
1.69k stars 405 forks source link

Starting DownloadService at 5AM in the background every day #1239

Open akrulec opened 7 months ago

akrulec commented 7 months ago

Hi, we use media3 library to display video and play audio in our application to take a user through a workout routine step by step. We would like to give the user an option to predownload some of this media every morning (while their phone is charging and they have a network), so that the media is available regardless of their internet connection later in the day.

I have tried the following:

However, that doesn't work, since API > 31 doesn't allow starting services from the background, since ForegroundServiceStartNotAllowedException gets thrown.

I have also tried the following:

Any ideas how to approach this? Everything that I have found originates from user pressing the button in the app, but what are recommendations if we want to offer the user the ability to pre-cache all of their media for a smoother offline experience. Thank you !

marcbaechinger commented 7 months ago

As you mention, starting foreground services without user interaction was further restricted since Android 12. You need to find one of the exemptions from that list of exemptions published here. For my understanding the choice you have is sending a cloud message to your user's devices at 5am.

Tolriq commented 7 months ago

The many joys of Android blocking more and more things without providing the necessary APIs for basic use cases ....

Until WorkManager have integration with AlarmManager you are doomed as they are fully independent, and while you are fully allowed to start a foreground service service from alarm manager you have no way to know that the worker will start in time and it triggers the ANRs you see.

You either use the periodic worker that use more battery and check if wanted time is passed (Absurd but that would be the 'official' solution for now), or you do not use WorkManager and do it manually but Google will block foreground services of type datasync soon, so you'll have to lie about what the service does, or you start a temporary foreground services before queuing the job and keep it running until the job starts.

Or you request your users to exclude your app from battery restrictions, but again counter productive.

I mean it's not like that need is a pretty standard need all solutions are worse from a battery usage point of view.

kelmer44 commented 7 months ago

I solved this by injecting downloadManager directly instead of relying on the service.

akrulec commented 7 months ago

I solved this by injecting downloadManager directly instead of relying on the service.

@kelmer44 I've noticed that is the best approach as well. However, it appears that the worker finishes before the download manager actually finished. And I often see ANRs in Firebase in this part of the code. I've also noticed that the notification tends to stay around if things don't finish as planned. Do you have an advice on how to solve that? I set setForegroundAsync in doWork:

override suspend fun doWork() = coroutineScope {
        setForegroundAsync(getForegroundInfo())

        return@coroutineScope withContext(Dispatchers.IO) {
            try {
                downloadMediaForWorkout(userId, workoutId, source)
                Result.success()
            } catch (e: Exception) {
                context.cancelNotification(FOREGROUND_NOTIFICATION_ID)
                Result.failure()
            }
        }

private suspend fun downloadMediaForWorkout() {
    val response = repository.getWorkoutMediaSummary
    when (response) {
            is Resource.Success -> {
                response.list.forEach {
                    downloadManager.scheduleDownload(url)
                }
                downloadManager.resumeDownloads()  // This seems to be necessary for the manager to run at all.
            }
    }
}

Thank you!

kelmer44 commented 7 months ago

I havent experienced those issues myself... are you sure those ANRs are related to the downloadmanager?

akrulec commented 7 months ago

I havent experienced those issues myself... are you sure those ANRs are related to the downloadmanager?

Here are a few stacktraces that I see. They are all a slightly different flavor. But the breadcrums show that these all happen in the media worker (either when it finished, or failed downloading the data), and are reported via Firebase. I haven't been able to reproduce it on my local device.

android.os.BinderProxy.transactNative (Native method)
android.os.BinderProxy.transact (BinderProxy.java:586)
android.app.INotificationManager$Stub$Proxy.enqueueNotificationWithTag (INotificationManager.java:3193)
android.app.NotificationManager.notifyAsUser (NotificationManager.java:748)
android.app.NotificationManager.notify (NotificationManager.java:698)
android.app.NotificationManager.notify (NotificationManager.java:674)
androidx.work.impl.foreground.SystemForegroundService$2.run (SystemForegroundService.java:147)
libandroid_runtime.so
android::android_os_MessageQueue_nativePollOnce + 44
android.os.MessageQueue.nativePollOnce (Native method)
android.os.MessageQueue.next (MessageQueue.java:349)
android.os.Looper.loopOnce (Looper.java:189)
android.os.Looper.loop (Looper.java:317)
android.app.ActivityThread.main (ActivityThread.java:8592)
java.lang.reflect.Method.invoke (Native method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:580)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:878)
libc.so
syscall + 32
1
libart.so
art::ConditionVariable::WaitHoldingLocks + 140
2
libart.so
art::JNI<false>::CallObjectMethodV + 1244
3
libandroid_runtime.so
_JNIEnv::CallObjectMethod + 124
4
libandroid_runtime.so
android::NativeDisplayEventReceiver::dispatchVsync + 68
5
libgui.so
android::DisplayEventDispatcher::handleEvent + 280
6
libutils.so
android::Looper::pollInner + 1252
7
libutils.so
android::Looper::pollOnce + 124
8
libandroid_runtime.so
android::android_os_MessageQueue_nativePollOnce + 48
android.os.MessageQueue.nativePollOnce (Native method)
android.os.MessageQueue.next (MessageQueue.java:335)
android.os.Looper.loopOnce (Looper.java:187)
android.os.Looper.loop (Looper.java:319)
android.app.ActivityThread.main (ActivityThread.java:8893)
java.lang.reflect.Method.invoke (Native method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:608)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1103)
android.os.BinderProxy.transactNative (Native method)
This Binder call may be taking too long, causing the main thread to wait, and triggering the ANR.
android.os.BinderProxy.transact (BinderProxy.java:685)
android.app.job.IJobCallback$Stub$Proxy.acknowledgeStartMessage (IJobCallback.java:434)
android.app.job.JobServiceEngine$JobHandler.ackStartMessage (JobServiceEngine.java:384)
android.app.job.JobServiceEngine$JobHandler.handleMessage (JobServiceEngine.java:196)
android.os.Handler.dispatchMessage (Handler.java:106)
android.os.Looper.loopOnce (Looper.java:257)
android.os.Looper.loop (Looper.java:368)
android.app.ActivityThread.main (ActivityThread.java:8826)
java.lang.reflect.Method.invoke (Native method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:572)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1049)
androidx.work.impl.utils.taskexecutor.TaskExecutor.executeOnTaskThread (TaskExecutor.java)
androidx.work.impl.WorkLauncherImpl.startWork (WorkLauncher.kt:59)
androidx.work.impl.background.systemjob.SystemJobService.onStartJob (SystemJobService.java:176)
android.app.job.JobService$1.onStartJob (JobService.java:106)
android.app.job.JobServiceEngine$JobHandler.handleMessage (JobServiceEngine.java:195)
android.os.Handler.dispatchMessage (Handler.java:106)
android.os.Looper.loopOnce (Looper.java:257)
android.os.Looper.loop (Looper.java:368)
android.app.ActivityThread.main (ActivityThread.java:8821)
java.lang.reflect.Method.invoke (Native method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:572)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1049)
java.util.HashMap.put (HashMap.java:608)
androidx.work.Data$Builder.putString (Data.java:828)
com.my.core.media.worker.MediaDownloadWorker$Companion.scheduleOneTimeRequest (MediaDownloadWorker.kt:198)
com.my.core.media.worker.MediaDownloadWorker$Companion.scheduleOneTimeRequest$default (MediaDownloadWorker.kt:195)
com.my.core.media.alarmmanager.MediaDownloadAlarmReceiver.onReceive (MediaDownloadAlarmReceiver.kt:15)
android.app.ActivityThread.handleReceiver (ActivityThread.java:4896)
android.app.ActivityThread.-$$Nest$mhandleReceiver (unavailable)
android.app.ActivityThread$H.handleMessage (ActivityThread.java:2498)
android.os.Handler.dispatchMessage (Handler.java:106)
android.os.Looper.loopOnce (Looper.java:230)
android.os.Looper.loop (Looper.java:319)
android.app.ActivityThread.main (ActivityThread.java:8893)
java.lang.reflect.Method.invoke (Native method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:608)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1103)