Sunday, 25 September 2011

Using the AlarmManager for updating Android Widgets

I see people asking repeatedly about how to update an Android widget more often than the restricted 30 minute interval that is enforced by the OS. Before we begin, I just want to point out and make one thing very clear: unless you have absolute reason to do so, you shouldn't be asking your widget to update more frequently than this. There probably isn't need to, to be honest. Once an hour will suffice for most people, such as if it's a weather app or checking feeds or something else that can't be made to receive broadcast or push notifications. The two main reasons behind this are as follows:

1. It will reduce the battery life of the phone.

2. Handled badly and your widget could end up making the phone completely unresponsive. Pushing updates to onscreen widgets is a somewhat costly process.

Now that you've read and understood the implications, let's move onto how to use an AlarmManager. Normally, when setting up an AppWidget, you will supply the update interval in milliseconds (as updateTimeMillis). There are two disadvantages to this. The first, as pointed out, is the minimum of 30 minutes (you'll see no warning about this if you lower it, by the way). The second, less obvious one, is that the time isn't predictable. That is to say, if you wanted to widget to always update once an hour, on the hour, it's near-impossible. Likewise, if you wanted to widget to update at 0, 15, 30 and 45 minutes past each hour, you can't do that, either.

Here's how we're going to solve this problem (note: this tutorial assumes that you already have knowledge of app widgets and services).

public class MyWidget extends AppWidgetProvider
{
    private PendingIntent service = null;

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
    {
        final AlarmManager m = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

        final Calendar TIME = Calendar.getInstance();
        TIME.set(Calendar.MINUTE, 0);
        TIME.set(Calendar.SECOND, 0);
        TIME.set(Calendar.MILLISECOND, 0);

        final Intent i = new Intent(context, MyService.class);

        if (service == null)
        {
            service = PendingIntent.getService(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
        }

        m.setRepeating(AlarmManager.RTC, TIME.getTime().getTime(), 1000 * 60, service);
    }

    @Override
    public void onDisabled(Context context)
    {
        final AlarmManager m = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

        m.cancel(service);
    }
}
and a cut down service example. The service is where we perform our logic and update the AppWidget.

public class MyService extends Service
{
    @Override
    public void onCreate()
    {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId)
    {
        buildUpdate();

        return super.onStartCommand(intent, flags, startId);
    }

    private void buildUpdate()
    {
        String lastUpdated = DateFormat.format("MMMM dd, yyyy h:mmaa", new Date()).toString();

        RemoteViews view = new RemoteViews(getPackageName(), R.layout.widget);

        view.setTextViewText(R.id.lastUpdated, lastUpdated);

        // Push update for this widget to the home screen
        ComponentName thisWidget = new ComponentName(this, MyWidget.class);
        AppWidgetManager manager = AppWidgetManager.getInstance(this);
        manager.updateAppWidget(thisWidget, view);
    }

    @Override
    public IBinder onBind(Intent intent)
    {
        return null;
    }
}
(note: you should think carefully before making your service START_STICKY, as this means the service should be explicitly stopped. Since we're running a one off procedure, we want to have this hanging around as little as possible. Don't be tempted to use a BroadcastReceiver here, as these are designed for very fast operations. Anything taking more than about 10 seconds will be seen as misbehaving by the Android OS and might be killed off)

Our Alarm is set up in the widget's onUpdate method. It shouldn't look that much different to how a standard AppWidget call to a service looks, except for that we're scheduling the alarm to do the work. Note that we're telling the widget to repeat the call once a minute. This is fine for our example, since you don't want to sit around for 15 minutes to see the results. However, in real life you will want it to do so less frequently.

We're also telling the alarm to kick off immediately. If we want it to kick off at an exact time, we'll need to create it. The simplest way is with the calendar:

Calendar TIME = Calendar.getInstance();
TIME.set(Calendar.MINUTE, 0);
TIME.set(Calendar.SECOND, 0);
TIME.set(Calendar.MILLISECOND, 0);

m.setRepeating(AlarmManager.RTC, TIME.getTime().getTime(), AlarmManager.INTERVAL_HOUR, service);
The above will kick off the service once an hour, on the hour. You'll notice that the widget is updated immediately, which is normal. It does this because it "missed" it's previous alarm of the last hour. It'll then sit quietly until the next update is due.

If you want the alarm to issue more frequently then you'll need to adjust the intervalPeriod.

Finally, we come to an important step: how to stop the alarm! You'll notice that in our class we have declared:

private PendingIntent service = null;
which is set up later in the onUpdate (if it's not null). We need to keep hold of this so that we can cancel the alarm later! We do so in the onDisable() method which will be triggered when all widgets are removed from the screen. We pass the PendingIntent to the cancel method of the AlarmManager so that it can determine which alarm it must cancel.

And there you have it. Pretty straight forward, really. If you have already created an AppWidget that calls a service, then you'll find you need only add in a handful of lines of code to have to AppWidget update more frequently, or at a more predictable interval.

13 comments:

  1. This is a very comprehensive tutorial. I was actually one of the people who had this question in my mind about how to update an Android widget more often than the restricted 30 minute interval. Thanks for providing the answer.

    white label seo

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Sorry, but this not works. onDisable is invoked from the different instance where service is null :(

    ReplyDelete
  4. Ah, a minor bug in the code. Best to check for the service being not null before attempting to disable it ;)

    ReplyDelete
  5. There is no NPE, so app not crash. Bug is, cause alarm still working even after widget off. Unfortunately I can't fing beauty sollution for this.

    ReplyDelete
  6. Is it possible to do the same update without service.

    ReplyDelete
  7. Yes, it is possible to do this without using a service.

    However, you need to keep in mind that if you do all your work in the app widget's onUpdate method, then you could cause the entire UI to lock up, if it is a long-running task.

    I'd recommend a service myself, or, if you really don't want to use one, make sure that you put all your work into a separate thread. Also, you will be limited to updates every 15 or 30 minutes at most (as mandated by the Android system). A service will enable you to do it more often.

    ReplyDelete
  8. private PendingIntent service is always null due new instances are being created upon any event. So onDisable will no switch the manager off. Use static field here.

    ReplyDelete
  9. I think that's a very good point to make and you make it well.



    View Document

    ReplyDelete
  10. In my app I want to wake up my app everyday @ 12am and do some activity.How can I use Alarm Manager to wake up my app everyday @ 12am?...please help me. thanx in advance.

    ReplyDelete
  11. I've only ever used the AlarmManager in conjunctions with widgets (as in the above example). If you want to do the same thing, it's simply a case of setting the alarm to 12:00am:

    Calendar TIME = Calendar.getInstance();
    TIME.set(Calendar.HOUR, 0);
    TIME.set(Calendar.MINUTE, 0);
    TIME.set(Calendar.SECOND, 0);
    TIME.set(Calendar.MILLISECOND, 0);

    and the interval to

    AlarmManager.INTERVAL_DAY

    That will make the app run at 12am every day.

    However, if you don't want to use a widget, then you will need to make use of broadcasts in your app to set up the alarm when the phone has completed booting. You'll need to investigate using:

    android.intent.action.BOOT_COMPLETED

    Unfortunately, I don't have any experience in using that, myself :|

    ReplyDelete
  12. I am not sure if I'm right. But from what I know, onUpdate will be called more than once. And everytime it's called you're setting a repeating intent right? If we can do it in onEnabled() instead of onUpdate() wouldn't that work?

    ReplyDelete
  13. Ah, something that's not mentioned is that the widget_provider.xml file used by the widget in this case will contain the line:

    android:updatePeriodMillis="0"

    which tells the widget never to update automatically. Since we're doing the update in the service ourselves, this is what we want to do.

    ReplyDelete