35

Let's say I have a widget for an app that has targetSDKVersion set to 26. This widget takes between 100ms and 10s to update. Most of the time under 1s. Before Android O, if onUpdate() was called on my AppWidgetProvider, I could launch a background service to update this widget. However, Android O returns an IllegalStateException if you attempt that behavior. The obvious solution of starting a foreground service seems like an extreme measure for something that will be done in under 10s 99% of the time.

Possible solutions

  • Start a foreground service to update the widget. Annoy the user with a notification that will be gone in 10s.
  • Use JobScheduler to schedule a job as quickly as possible. Your widget may or may not get updated for a while.
  • Attempt to do the work in a broadcast receiver. Lock up the UI thread for any other apps. Yuck.
  • Attempt to do work in the widget receiver. Lock up the UI thread for any other apps. Yuck.
  • Abuse GCM to get a background service running. A lot of work and feels hacky.

I don't personally like any of the above solutions. Hopefully I'm missing something.

(Even more frustrating is that my app is already loaded into memory by the system calling onUpdate(). I don't see how loading my app into memory to call onUpdate(), but then not giving my app 1s to update the widget off the UI thread is saving anyone any battery life.)

Justin
  • 3,322
  • 2
  • 22
  • 37
  • 1
    Did you find any cleaner way to do it? I am also facing the IllegalStateException – SAIR Jan 18 '18 at 18:14

2 Answers2

11

You don't indicate what the update trigger mechanism is. You seem concerned about latency ("Your widget may or may not get updated for a while"), so I am going to assume that your concern is tied to user interaction with the app widget, such as tapping a button.

Use JobScheduler to schedule a job as quickly as possible. Your widget may or may not get updated for a while.

This is a variation on "use JobIntentService", which AFAIK is the recommended solution for this sort of scenario.

Other options include:

  • Use getForegroundService() with PendingIntent. With this, you effectively "pinky swear" that your service will call startForeground() within the ANR timeframe. If the work takes longer than a few seconds, call startForeground() to ensure that Android doesn't get cranky. This should minimize the number of time the foreground notification appears. And, if the user tapped a button and you are still busy doing work a few seconds later, you probably want to show a notification or otherwise do something to let the user know that what they asked for is still in progress.

  • Use goAsync() on BroadcastReceiver, to do work in the context of the receiver while not tying up the main application thread. I haven't tried this with Android 8.0+, so YMMV.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • thanks for the answer! I do indicate that the trigger mechanism is onUpdate() called on the AppWidgetProvider. The system calls onUpdate when the homescreen is in need of refresh, such as after a reboot, or switching between 20 apps, which forces the homescreen out of memory. I don't think this information changes your answer though. I'll test out a JobIntentService and see how that performs. Thanks! – Justin Sep 12 '17 at 16:31
  • 1
    @Justin: "I do indicate that the trigger mechanism is onUpdate() called on the AppWidgetProvider" -- FWIW, that's not what I meant by the trigger mechanism. The events that you cited (e.g., reboot) are a trigger, as is the passage of time if you are using `updatePeriodMillis`. And, there's nothing stopping you from creating a `PendingIntent` tied to a button in the app widget that winds up routing to `onUpdate()`. If your concern was `updatePeriodMillis`, you could scrap that and use `JobScheduler` for periodic updates. For the other triggers, I'd try the techniques outlined in my answer. – CommonsWare Sep 12 '17 at 16:47
  • 2
    My widget sometimes didn't show responses from updates after moving to JobIntentService. Putting logging information in, I could see that, in one case, the job was delayed 7 minutes, the all widget updates for the last seven minutes were displayed all at once. I've accepted the answer, but I've got to keep trying other approaches, as JobIntentService clearly isn't meant for anything related to UI. – Justin Sep 13 '17 at 01:37
  • 4
    Initial testing looks good for using goAsync() in the broadcast receiver! – Justin Sep 13 '17 at 02:06
  • 2
    @Justin Did you end up sticking with goAsync? – AdamWardVGP Nov 10 '17 at 20:14
  • I believe that JobScheduler is the perfect way to do this by using JobInfo.Builder.setOverrideDeadline(long) which also makes sure that job will be scheduled. read more @ https://developer.android.com/reference/android/app/job/JobInfo.Builder.html#setOverrideDeadline(long) – SAIR Jan 25 '18 at 18:23
  • @SAIR, Problem with JobScheduler is that it's not available on API < 21. You could use the new WorkManager, but here the problem is rather short minimum interval for repeating stuff, which is 15 min. – c0dehunter Sep 18 '18 at 12:42
  • Any snippets or working example? A simple one like clock widgets maybe? – stuckedunderflow Dec 29 '18 at 08:42
6

You can use WorkManager to update a widget. Uses WorkManager on devices with API 14+. You need to override fun onReceive(context: Context?, intent: Intent?) like this:

val ACTION_AUTO_UPDATE : String = "AUTO_UPDATE";

override fun onReceive(context: Context?, intent: Intent?) {
    super.onReceive(context, intent)
    if(intent?.action.equals(ACTION_AUTO_UPDATE))
    {
        val appWidgetManager = AppWidgetManager.getInstance(context)
        val thisAppWidgetComponentName = ComponentName(context!!.getPackageName(), javaClass.name)
        val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidgetComponentName)
        for (appWidgetId in appWidgetIds) {
            // update widget
        }
    }
}

And you should create PeriodicWorkRequest. You have to use for repeating work. Periodic work has a minimum interval of 15 minutes. We enqueue the periodicWork when widget is enabled:

override fun onEnabled(context: Context) {
    val periodicWorkRequest = PeriodicWorkRequest.Builder(YourWorker::class.java, 15, TimeUnit.MINUTES).build()
    WorkManager.getInstance(context).enqueueUniquePeriodicWork("YourWorker", ExistingPeriodicWorkPolicy.REPLACE,periodicWorkRequest)
}

And cancel it when widget is disabled:

override fun onDisabled(context: Context) {
    WorkManager.getInstance(context).cancelAllWork()
}

Finally we create worker class:

class YourWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
var context : Context? = null

init {
    context = ctx
}

override fun doWork(): Result {
    val alarmIntent = Intent(context, YourWidget::class.java)
    alarmIntent.action = YourWidget().ACTION_AUTO_UPDATE
    context?.sendBroadcast(alarmIntent)
    return Result.success()
}

If you want to use WorkerManager you add to build.gradle implementation 'androidx.work:work-runtime:2.3.1'

You can find the sample here.

Kasım Özdemir
  • 5,414
  • 3
  • 18
  • 35
  • 1) The sample is not contains anything related with WorkManager in the AppWidgetProviders. 2) Using of WorkManager.getInstance(context) in AppWidgetProvider will lead to IllegalStateException on some devices. Because the context could be not the app context and WorkManager probably was not initialized in the context. 3) onEnabled/onDisabled() will not help with updating widgets for onReceive() – Alex Sep 13 '20 at 08:24
  • Use getApplicationContext() in YourWorker class instead of storing the second context field – Prilaga May 18 '22 at 22:59