19

I'm using Android app widgets. I'm creating a PendingIntent object and use it in the method RemoteViews#setOnClickPendingIntent(). This is the pending intent:

// The below code is called in the onUpdate(Context, AppWidgetManager, int[]) method of the AppWidgetProvider class.

// Explicit intent
Intent intent = new Intent(context, MyService.class);
intent.setAction(MY_ACTION);
intent.putExtra(EXTRA_KEY, VALUE);

// Create the pending intent
PendingIntent pendingIntent = PendingIntent.getService(context, appWidgetId, intent, 0);

// 'views' is a RemoteViews object provided by onUpdate in the AppWidgetProvider class.
views.setOnClickPendingIntent(R.id.root_layout, pendingIntent);

The above code works as expected before Android Oreo. However, in Android Oreo it no longer starts the service if the app is swiped away from recents. (no longer active). Aren't PendingIntents excluded in Oreo's background execution limits?

For testing purposes I replaced getService with getForegroundService but the Service is still not started. Both methods show the below message in the log:

W/ActivityManager: Background start not allowed: service Intent { act=com.example.myapp.MY_ACTION flg=0x10000000 cmp=com.example.myapp/com.example.myapp.MyService bnds=[607,716][833,942] (has extras) } to com.example.myapp/com.example.myapp.MyService from pid=-1 uid=10105 pkg=com.example.myapp

Why is the Service not started, even when using getForegroundService? I tested this on a Nexus 6P running Android 8.1.0.

Thomas Vos
  • 12,271
  • 5
  • 33
  • 71
  • You may need to use something other than `0` for the flags (e.g., `FLAG_UPDATE_CURRENT`) when switching from `getService()` to `getForegroundService()`. – CommonsWare Jan 06 '18 at 18:10
  • @CommonsWare I just tried that but unfortunately it doesn't make a difference. I replaced `0` with `FLAG_UPDATE_CURRENT` but the same logs still appear. – Thomas Vos Jan 06 '18 at 18:20
  • 1
    Hmmmm... if it does not upset your environment too much, with the `getForegroundService()` edition of the code installed, reboot the device/emulator. It feels like you're getting the regular `getService()` edition of the `PendingIntent`, despite requesting the update. Those are only held in RAM, so a reboot will flush the old `PendingIntent`, ensuring that you get a foreground service `PendingIntent` after the reboot. – CommonsWare Jan 06 '18 at 18:23
  • 1
    @CommonsWare Thank you very much! A reboot fixed it. I'm now using `getForegroundService` with the flag `FLAG_UPDATE_CURRENT`. Just wondering why I can't use a normal service. I just need to send a one network request which will take about 0-10 seconds. (10 seconds timeout). Most of the times it will finish within a second. Why isn't the `PendingIntent` of an app widget in the exclusion 'whitelist'? (https://developer.android.com/about/versions/oreo/background.html#services) – Thomas Vos Jan 06 '18 at 19:14

3 Answers3

19

You can no longer start a service in background in 8.0, but you can use JobScheduler to achieve similar results. There is also a JobIntentService helper class that allows you to switch to JobScheduler from service without much refatoring. And you cannot use PendingIntent pointing to a service, but you can use one pointing to an Activity or BroadcastReceiver.

If you had a working widget pre 8.0, and now you need to make it work on android 8.0, just perform this simple steps:

  1. Change your IntentService class to JobIntentService
  2. Rename service onHandleIntent method to onHandleWork (same parameters)
  3. Add BIND_JOB_SERVICE permission to your service in the manifest:
    <service android:name=".widget.MyWidgetService"
           android:permission="android.permission.BIND_JOB_SERVICE">
    </service>
  1. To start this service, you must no longer use context.startService. Instead use enqueueWork static method (where JOB_ID is just an unique integer constant, must be the same value for all work enqueued for the same class):
    enqueueWork(context, MyWidgetService.class, JOB_ID, intent);
  1. For clicks, replace your pendingIntent that was pointing to service with a pendingIntent that points to a BroadcastReceiver. Since your subclass of AppWidgetProvider is a BroadcastReceiver itself, you might as well use it:
    Intent myIntent = new Intent(context, MyAppWidgetProvider.class);
    myIntent .setAction("SOME_UNIQUE_ACTION");
    pendingIntent = PendingIntent.getBroadcast(context, 0, myIntent, PendingIntent.FLAG_UPDATE_CURRENT);
  1. In onReceive start the service using enqueueWork (If your PendingIntent was starting the activity, just leave it be - it'll work just fine on android 8.0+):
    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        if (intent.getAction().equals("SOME_UNIQUE_ACTION")) {
            MyWidgetService.enqueueWork(.....);
        }
    }
  1. To ensure that the widget will work on old devices, make sure you have WAKE_LOCK permission in your manifest (used by JobIntentService on old devices).

That's it. This widget will now work properly on both new and old devices. The only real difference will be that if your 8.0 device is in doze mode it may not update widget all that often, but that shouldn't be a problem because if it is dozing, that means that user can't see your widget right now anyways.

Alexey
  • 7,262
  • 4
  • 48
  • 68
  • 1
    Hi, I tried to implement your solution. I have a problem, that when I open a new application and go back to the widget, it freezes and the buttons don't respond. Are you familiar with this? – Kram May 15 '18 at 14:07
  • @Kram Nope, I didn't have such problems. What exactly is happening? What do you mean by "open a new application"? You just click on the app icon? And what do you mean by widget freeze? You have some animations that stop running? Or it's just that widget buttons are no longer clickable? First thing that comes to mind - is your widget's PendingIntent pointing to a BroadcastReceiver or Activity (not pointing to a Service)? – Alexey May 15 '18 at 14:15
  • So by meaning another app, is click on app icon. I don't have any animation, the widget buttons just no longer clickable. The pendingIntent pointing to the appWidgetProvider. This issue happens only in android 8.1 – Kram May 15 '18 at 14:23
  • @Kram No, I dont have such problem in my widget. Have you tried setting breakpoint (or adding log statement) in AppWidgetProvider.onReceive method to ensure that this is a broadcast problem and not app logic problem? – Alexey May 15 '18 at 14:41
  • Yes, the breakpoint in the onReceive is not triggered after opening another app – Kram May 15 '18 at 14:53
  • @Kram Well, this is strange. Maybe its your particular firmware. Have you tried running on emulator? If you could provide a minimal working example, I could try running it on my device to see if I can reproduce the problem. Otherwise, I'm out of ideas. – Alexey May 15 '18 at 14:57
  • @Alexey, do you offer paid code reviews? My app has a widget and I want someone with experience to look at it. Contact me at primoz at codehunter .eu. – c0dehunter Sep 10 '18 at 12:52
  • can you share the source? – lacas Nov 21 '18 at 15:51
  • @lacas Sorry, this is from proprietary project. I don't own the source and I cannot share it. – Alexey Nov 21 '18 at 17:31
  • Thanks a lot for your great solution because in some phone ma notification tray work but in orio not works and 8+ it's work very strange. also upvote my me – jeet parmar Dec 26 '18 at 13:18
  • This doesn't work immediately for me when I clear the app from recent. If I then open the app, all the clicks fire together and I get a bunch of notifications. So the problem with this solution to me looks like it doesn't respond immediately. My phone has version 10. – fsljfke Aug 30 '20 at 20:07
12

As you note, an app widget does not count for the PendingIntent background whitelist. I do not know why — it would seem to be about on par with a PendingIntent started by a Notification. Perhaps it's an issue that the Notification is a system thing, whereas the app widget is a home screen thing. Regardless, you could:

  • Use getForegroundService(), as you have, or

  • Try getBroadcast() with an explicit Intent, and with the BroadcastReceiver starting a JobIntentService, if you do not want to raise a Notification (as a foreground service requires)

Based on your symptoms, you appear to have tripped over what I would consider to be a bug: there should be a way to switch from getService() to getForegroundService() without having to reboot. :-) I'll try to run some experiments and will file an issue if I can reproduce the problem.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • 1
    Thanks for your help! I'll stick with `getForegroundService()` for now because the action (network request) needs to be executed immediately, so a `JobIntentService` won't really work here. – Thomas Vos Jan 06 '18 at 19:52
1

Could goasyc be an option? You could change your PendingIntent to fire a BroadcastReceiver, then in the OnRecieve method, you can call goasync() Then you should be able to use the PendingResult it generates to create an async call. Still don't think you can start a service.

How to use "goAsync" for broadcastReceiver?

Yeshia
  • 51
  • 1
  • 8
  • 1
    Why the downvote? I was just adding another way of achieving the goal which was not listed so far. Goasync is an alternative to starting a service in a receiver. – Yeshia Feb 14 '18 at 22:55