Starting WorkManager task from AppWidgetProvider results in endless onUpdate calls
Asked Answered
P

1

9

I am trying to create a widget and as stated in this guide,

…If your widget setup process can take several seconds (perhaps while performing web requests) and you require that your process continues, consider starting a Task using WorkManager in the onUpdate() method.

But by enqueueing work requests from within the onUpdate method, I get endless onUpdate calls roughly every half a second with the same intent being passed in. I have tried updating the widget using goAsync() and GlobalScope.launch for my network request and it works just fine. However, it is recommended to do the i/o work using the workmanager api and this is what I am trying to do.

I created an app widget through the New -> Widget -> App Widget menu template, and am trying to queue the work like so:

class NewAppWidget : AppWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager,appWidgetIds: IntArray) {
        val inputData = Data.Builder().putIntArray("ids", appWidgetIds).build()
        val workRequest = OneTimeWorkRequestBuilder<WidgetUpdateWorker>().setInputData(inputData).build()
        WorkManager.getInstance(context.applicationContext).enqueue(workRequest)
    }
}

Worker:

class WidgetUpdateWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
    override suspend fun doWork(): Result {
        val appWidgetIds = inputData.getIntArray("ids")
        val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
        delay(200L) // Simulate network call. The behavior is the same with Retrofit and a real network call
        appWidgetIds?.forEach { id ->
            val views = RemoteViews(applicationContext.packageName, R.layout.new_app_widget)
            views.setTextViewText(R.id.appwidget_text, "Setting text from worker")
            appWidgetManager.updateAppWidget(id, views)
        }
        return Result.success()
    }
}

App widget info XML:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_widget_description"
    android:initialKeyguardLayout="@layout/new_app_widget"
    android:initialLayout="@layout/new_app_widget"
    android:minWidth="180dp"
    android:minHeight="110dp"
    android:previewImage="@drawable/example_appwidget_preview"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="86400000"
    android:widgetCategory="home_screen" />

As a result, I get a constantly blinking and updating widget on the homescreen. This happens on an emulator and a real device as well.

What am I doing wrong? I am targeting API 30 and using WorkManager version androidx.work:work-runtime-ktx:2.6.0

Persinger answered 10/1, 2022 at 15:4 Comment(1)
Okay I've found a related issue and people have submitted some workarounds there: issuetracker.google.com/issues/115575872Persinger
P
13

I've found some info about this issue and I think I'll answer my own question for now.

Why this happens

According to the replies on the issue tracker, this is intended behavior. When there's no pending work, WorkManager disables its internal RescheduleReceiver, and a component's state change triggers onUpdate. onUpdate then enqueues another work request and we end up in an infinite loop. I also recommend reading CommonsWare's post which goes into detail about this.

The recommended solution by the library developers is to not "unconditionally enqueue work in onUpdate". This may be difficult to implement, and since I hadn't had time to try this advice, I'll provide a workaround below that has worked for me.

Workaround

The simplest workaround I've found so far is to set a dummy work request far out into the future in AppWidgetProvider's onEnabled so that there's always a pending work request. For example:

override fun onEnabled(context: Context) {
    val alwaysPendingWork = OneTimeWorkRequestBuilder<YourWorker>()
        .setInitialDelay(5000L, TimeUnit.DAYS)
        .build()

    WorkManager.getInstance(context).enqueueUniqueWork(
        "always_pending_work",
        ExistingWorkPolicy.KEEP,
        alwaysPendingWork
    )
}

override fun onDisabled(context: Context) {
    WorkManager.getInstance(context).cancelUniqueWork("always_pending_work")
}

Don't forget that each AppWidgetProvider must provide a unique work name. "${YourWorker::class.simpleName}_work" worked well for me.

It should also be possible (and is probably a better approach) to use WorkManager's periodic work instead of relying on widget's update interval, but from what I've read it requires additional, more complex state management. You can also consider using JobIntentService or JobScheduler instead of WorkManager here.

Persinger answered 12/1, 2022 at 17:16 Comment(4)
Thanks for the workaround! I cannot believe that this has to be done to prevent the widget from getting into an infinite loop. All other solutions are much more complicated.Percentile
I ran into the same issue and it did solve it to some extent, but after the initial delay timeout the same issue happens and onUpdate start infinite loopUnderpinning
@Underpinning how is your initial delay timing out? Is it too short? The whole point of this hacky workaround is to set a practically endless initial delay. By the way, they're working on fixing the issue and adding support for widgets. I might update the answer later at some pointPersinger
@Persinger Yes, I needed to update the widget every 30 min ... I found a better implementation on a GitHub repository, although it was using CorutineWorker since they had a lot of things going on in the background but the partial solution works fine for me with it .... check there repo here - github.com/SimpleAppProjects/SimpleWeather-AndroidUnderpinning

© 2022 - 2024 — McMap. All rights reserved.