AlarmManager / IntentService / PendingIntent / Text Clock

Text Clock – Part 4

In the previous article we got the time displaying on our Text Clock app widget, but the time did not update. In this article we’ll use AlarmManager to get the widget updating periodically.

Previously we discussed IntentService and how it can be useful for performing small, distinct actions. AlarmManager provides us with an excellent mechanism of periodically starting an IntentService. Hopefully you will be familiar with the concept of starting an Android Service using an Intent (as it’s what we got working previously), and AlarmManager enables us to perform this same action at some time in the future and, optionally, at regular intervals. AlarmManager can perform a number of actions, not limited to starting services, it can also send broadcasts, and start activities. A PendingIntent is used to represent what action that is to be performed, and the AlarmManager schedules when this action will be performed.

So, let’s consider how we want our app widget updates to work. When the user creates an instance of our app widget on their home page, the onUpdate() method of our TextClockAppWidget will be called. Currently we simply update the time of the app widget here and nothing more, but this is where we can schedule future updates. We still want to perform an initial update, so we’ll leave the existing startService() call in there. But what we want to do is create an Alarm which will fire a PendingIntent to call startService() once a minute as the time crosses each minute boundary (i.e. when the seconds increment from 59 to 00 and the minute updates) to ensure that the time we’re displaying is accurate.

It is important to remember that the user can install multiple instances of our app widget, but we only require a single alarm to cause them all to update because they will all update at the same time, and to start multiple services to do this would be wasteful. The bad news is that AlarmManager does not allow us any option to query what alarms are currently set, so that appears to complicate things. The good news is that we can use PendingIntent to detect whether we have an alarm set. A PendingIntent will be identical if it uses the same same operation, same Intent action, data, categories, and components, and same flags, event if it created in a different thread, process, or even application. We can use this to manage our alarms.

For the purposes of this explanation, we’ll focus on a PendingIntent to start a service, but the same techniques can be applied to the other operations. We cannot create a PendingIntent directly, but PendingIntent provides a number of static factory methods which will create them for us. PendingIntent.getService() takes four arguments:

  • Context context – The Context in which this PendingIntent should start the service.
  • int requestCode – Currently ignored
  • Intent intent – The Intent that will be used as the argument to the startService() method that will be invoked on context.
  • int flags – Flags which control how and if the PendingIntent is created or updated.

If we use a value of FLAG_NO_CREATE in the flags field, it will check whether an active PendingIntent matching these parameters already exists on the device, and will return an instance of it if so, or null otherwise. So we can use this to ensure that we only ever have a single PendingIntent:

Intent update =
    new Intent( TextClockService.ACTION_UPDATE );
PendingIntent pi = PendingIntent.getService( context,
        REQUEST_CODE,
        update,
        PendingIntent.FLAG_NO_CREATE );
if ( pi == null )
{
    pi = PendingIntent.getService( context,
            REQUEST_CODE,
            update,
            PendingIntent.FLAG_CANCEL_CURRENT );
}

If we only create an alarm when we create its associated PendingIntent, then we can ensure that we also only ever have one alarm.

If we apply this to our TextClockAppWidget:

public class TextClockAppWidget extends AppWidgetProvider
{
    private static final String TAG = "TextClockWidget";
    private static final Intent update =
            new Intent( TextClockService.ACTION_UPDATE );
    private static final int REQUEST_CODE = 1;
    private Context context = null;

    @Override
    public void onUpdate( Context context,
                          AppWidgetManager appWidgetManager,
                          int[] appWidgetIds )
    {
        Log.d( TAG, "onUpdate" );
        this.context = context;
        this.context.startService( update );
        scheduleTimer();
    }

    private void scheduleTimer()
    {
        Calendar date = Calendar.getInstance();
        date.set( Calendar.SECOND, 0 );
        date.set( Calendar.MILLISECOND, 0 );
        date.add( Calendar.MINUTE, 1 );
        AlarmManager am =
                (AlarmManager) context.getSystemService(
                        Context.ALARM_SERVICE );
        PendingIntent pi = PendingIntent.getService( context,
                REQUEST_CODE,
                update,
                PendingIntent.FLAG_NO_CREATE );
        if ( pi == null )
        {
            pi = PendingIntent.getService( context,
                    REQUEST_CODE,
                    update,
                    PendingIntent.FLAG_CANCEL_CURRENT );
            am.setRepeating( AlarmManager.RTC,
                    date.getTimeInMillis(),
                    60 * 1000,
                    pi );
            Log.d( TAG, "Alarm created" );
        }
    }
}

We use Calendar object to find the next minute boundary, and we set a repeating alarm every minute starting at the next minute boundary. It is important that we specify an alarm type of RTC and not RTC_WAKEUP to preserve the battery. This is because RTC_WAKEUP will cause our service to be started even if the device is asleep and waking the device to update a widget that the user is not looking at is both pointless and extremely wasteful of battery resources. By using RTC instead, the service will be started when the device next wakes up (so we always display an accurate time), but will not cause the device to wake.

One further thing that we need to do is be a good Android citizen and remove our alarm if the user removes all instances of our app widget:

@Override
public void onDeleted( Context context,
                       int[] appWidgetIds )
{
    Log.d( TAG, "onDeleted" );
    AppWidgetManager mgr = AppWidgetManager.getInstance( context );
    int[] remainingIds = mgr.getAppWidgetIds(
            new ComponentName( context, this.getClass() ) );
    if ( remainingIds == null || remainingIds.length <= 0 )
    {
        PendingIntent pi = PendingIntent.getService( context,
                REQUEST_CODE,
                update,
                PendingIntent.FLAG_NO_CREATE );
        if ( pi != null )
        {
            AlarmManager am =
                    (AlarmManager) context.getSystemService(
                            Context.ALARM_SERVICE );
            am.cancel( pi );
            pi.cancel();
            Log.d( TAG, "Alarm cancelled" );
        }
    }
}

onDelete is called every time the user removes an app widget instance, so we check whether there are other instances, and cancel the alarm and PendingIntent if there are not. We must cancel the PendingIntent here because it will stay active on the system even if there is no associated alarm. This would break our check for an existing PendingIntent in scheduleTimer().

If we run this we now have a version of the app widget which keeps accurate time!:

widget-update

So we now have a version of the app which fulfils its basic function: to accurately display the time. This is now version 1.0.0 of our app that appeared in Google Play.

While it is functional, there are some areas that could be improved and we'll start looking at those in the next article.

The source code for this article can be found here, and TextClock is available from Google Play.

© 2013 - 2014, Mark Allison. All rights reserved.

Copyright © 2013 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.