Well Behaved Widgetry
When we left it, my battery widget was working but wasn’t particularly well behaved. There was nothing much wrong with the functionality, but plenty wrong with the implementation.
Despite their impressive specifications, mobile devices have one very limiting factor. Battery Life.
Indeed, the impressive specifications are part of the reason that battery life can be a problem, but as application developers targeting mobile platforms there are things we can – and should – do, to ensure that we don’t make the problem worse than it needs to be.
So let’s look at the problems with my battery widget.
System Scheduled Updates
For simplicity and convenience, I declared an update interval in the meta-data for my widget.
This is bad for two reasons:
- Declared update intervals are subject to a maximum frequency (i.e. they cannot be schedule more frequently than a prescribed limit). There is a good reason for this, which is the second problem with such declared updates
- For updates scheduled in this way, the system will wake the device in order to perform the requested update. For my widget this is pointless – if the device is asleep the screen is off, so having an up-to-date battery %’age displayed is neither here nor there. But it’s worse than that. Not only will my widget wake up the device needlessly, but also when it does any other apps or widgets that were waiting patiently for the user to wake the device will also potentially take this as their cue to do some work that they otherwise needn’t
All in all, my simple widget is a real trouble maker.
Fortunately there is an alternative to scheduling updates in this way. Alarms.
Not to be confused with an “alarm clock“, on Android there is a system AlarmManager which can be used to scheduled recurring (or one off) events.
Even better, when setting up an alarm using AlarmManager, we can specify whether or not our alarm is important enough to wake up the device. If not, and if the device is asleep when our alarm would have been triggered, it will instead be triggered immediately that the device is first awoken after our alarm had elapsed.
And even better still, with the AlarmManager there are no constraints on the frequency of any recurring alarm we may wish to set (other than our own common sense and consideration for others).
So, let’s adapt the battery widget to use an alarm.
First of all, we set the update period in the meta-data to “0” to indicate that we do not wish to receive any system generated updates:
Next, we add a class var (static member) to our BatteryWidgetProvider class, to hold a reference to the AlarmManager. As we shall see, this will both act as a cache for the reference to the AlarmManager and also serve as a flag that we have an alarm set.
private class var fAlarm: AlarmManager;
Now we need to extend BatteryWidgetProvider further. Previously we only responded to ACTION_APPWIDGET_UPDATE intents, but now we need to respond to two other intents in the widget lifecycle:
- ACTION_APPWIDGET_ENABLED – an intent received when the first instance of the widget is placed on the home screen. We will use this intent to set our update alarm.
- ACTION_APPWIDGET_DISABLED – an intent received when the last instance of the widget is removed from the home screen. No prizes for guessing that we will use this intent to cancel our update alarm.
To receive these intents we must add them to the
intent-filter for the BatteryWidgetProvider
receiver entity in the AndroidManifest.
<intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_ENABLED" /> <action android:name="android.appwidget.action.APPWIDGET_DISABLED" /> </intent-filter>
Although not as important for recurring updates, we will leave the APPWIDGET_UPDATE intent in place to ensure that new widget instances are updated immediately when initially placed.
Now we can override the
onDisabled methods of the BatteryWidgetProvider class. These methods are introduced by the AppWidgetProvider ancestor class – mapping the intents onto these virtual methods is one of the conveniences it provides. Handling intents more directly is something we will come to later.
method onDisabled(aContext: Context); override; method onEnabled(aContext: Context); override;
In the implementation of the onEnabled method, we will obtain a reference to the AlarmManager and establish our alarm. If we already have a reference to the AlarmManager then our alarm is already in place and does not need to be re-established.
method BatteryWidgetProvider.onEnabled(aContext: Context); begin if NOT assigned(fAlarm) then begin fAlarm := aContext.SystemService[Service.ALARM_SERVICE] as AlarmManager; fAlarm.setRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime, 1000, updateIntent(aContext)); end; end;
We obtain a reference to the AlarmManager from the
SystemService member of a context, in this case the Context supplied with the Enabled intent.
As with just about everything in Android, Intents are involved again.
Rather than providing a callback function or any other such contrivance, an alarm is configured with an intent. In this way you can directly configure an alarm to perform (almost) any intent based action in the system, even going to far as to launch applications if you wish (not advisable – users might be confused).
In this case the intent supplied to the alarm is build by another method we shall add to the provider class,
updateIntent(Context). It is important that we have a method for reliably constructing the appropriate intent because when we cancel an alarm we do so by identifying the intent whose alarm is to be cancelled, so it must match.
But before we get to that, let’s look at the other aspects of establishing this alarm.
First, I am using the
setRepeating() method to set the alarm. As the name implies, this will establish a repeating alarm. The
set() method establishes a one-time alarm. The
setInexactRepeating() method also establishes a repeating alarm, but allows the system to vary the schedule of the alarm slightly, so as to coalesce alarms to maximise efficiency.
The first parameter to this call specified the type of alarm. There are two basic types, ELAPSED_REALTIME and RTC. ELAPSED_REALTIME alarms are specified in terms of intervals (trigger alarm in 5 seconds time), RTC alarms are specified in terms of actual clock time (trigger alarm at 1pm).
In addition, both types of alarm have a WAKEUP (ELAPSED_REALTIME_WAKEUP / RTC_WAKEUP) variant which indicates that the device should be awoken to ensure delivery of the alarm intent at the appropriate time. I specifically do not want to do this for this widget.
The next parameter indicates the initial time at which the alarm should fire. For an elapsed time alarm this should be based on
SystemClock.elapsedTime (the current-time). You could apply an offset to this time if you do not need the alarm to fire immediately, but in this case right now is as good a time as any.
The third parameter identifies the repeating interval in milliseconds. Very straight-forward.
In this case I have set a ridiculous interval of just 1 second.
Make no mistake. This is bonkers !!
Battery level is not going to alter significantly – if at all – in such a time frame and by updating so frequently all I am doing is contributing to battery drain. But I want to be certain that my alarm is firing at this stage. I will add some variation to the display of the battery percentage so that I can see the updates occuring without having to hang around waiting for the battery level to actually fall (or rise, if charging).
Once I am satisfied that everything is working I will remove that additional visual feedback and set a more appropriate interval. I might even allow the user to choose one that suits them and their device.
The final parameter is the intent associated with my new alarm. Actually it is a special sort of Intent called a PendingIntent. So let’s look at how we construct this in the
Building a PendingIntent
You can think of a PendingIntent as a sort of envelope containing an actual Intent that will be delivered at some point. Like any envelope that you pop in the post, you have to address it so that the postal service know where to send it.
First let’s consider the Intent we will trigger with our alarm.
It just so happens that in the case of this widget, I already have an ideal intent in mind. Remember that we implemented the update code for the widget in a Service, and we start a Service using… an Intent. We can just use exactly the same specification of intent for our alarm as well. All that changes is the way we deliver it.
After we have created our Intent, we then obtain a stamped, addressed envelope and slip our Intent inside it. This is achieved using an appropriate method of the PendingIntent class.
For an Intent to be broadcast to an arbitrary broadcast receiver, we would use
For an Intent to be send to an Activity, we would use
Since our Intent is intended [sic] to be delivered to a Service, we use .. again, no prizes for having guessed already .. the
method BatteryWidgetProvider.updateIntent(const aContext: Context): PendingIntent; begin var action := new Intent(aContext, typeOf(UpdateService)); result := PendingIntent.Service[aContext, 0, action, PendingIntent.FLAG_UPDATE_CURRENT]; end;
There are some parameters supplied to the
getService method (exposed by the magic of Oxygene syntax as an indexed property called, simply,
First is a Context (a common requirement which I have still yet to fully grasp myself which is why I haven’t yet explained it).
Next is an arbitrary ID value. I am not using this so 0 will do.
Then comes the Intent we wish to put inside the ‘envelope’.
And finally a flag which determines what should happen to any existing PendingIntent that matches the specification. In this case I simply update it, though nothing will ever change.
That is almost enough to get our alarm performing updates for us. There is a change we need to make to the service before it will work however, but before we do that I will finish the changes to the widget provider itself.
I still need to implement
onDisabled() to cancel any alarm that has been set.
Cancelling an Alarm
Cancelling an alarm is very straightforward:
method BatteryWidgetProvider.onDisabled(aContext: Context); begin if assigned(fAlarm) then begin fAlarm.cancel(updateIntent(aContext)); fAlarm := NIL; end; end;
We only cancel the alarm if an alarm has been set which we can determine by the fact that we have a cached reference to the AlarmManager (in fAlarm).
We cancel the alarm by calling the
cancel() method and passing in a PendingIntent. Any alarm that is set with a matching PendingIntent will be cancelled.
Having cancelled the alarm, we NIL our reference to the AlarmManager so that if/when the widget is again placed on the home screen we will know that we need to re-set the alarm in the
All that’s left is two minor changes to make to the UpdateService and we’re done.
All Part of the Service
The first and simplest change is to introduce the visual feedback that will help identify that things are working as intended.
I shall use a unit variable (an “implementation global” if you prefer). I could have used another class var (static member) on the UpdateService class, but this seemed like a good opportunity to show once again how Oxygene targets Java without being entirely constrained by the rules of that language, since Java of course doesn’t support global variables
at all as directly as this.
But bear in mind, this isn’t a recommendation just a demonstration.
The unit variable will be a simple boolean which I will toggle each time the service builds an update and use the state to alternate the color of the text of the widget (arbitrarily I chose RED and WHITE):
implementation var ticktock: Boolean; function UpdateService.buildUpdate: RemoteViews; begin ... ticktock := NOT ticktock; result := new RemoteViews(PackageName, R.layout.widgetlayout); result.setTextViewText(R.id.lblInfo, pct.toString + '%'); result.setTextColor(R.id.lblInfo, if ticktock then COLOR.RED else COLOR.WHITE); end;
Finally, we have to make an important change to the
onStart method of the UpdateService itself.
First of all, I was following an out of date example when I first implemented my service. The
onStart() method is deprecated and we should now implement
onStartCommand() instead. This is important because the return value of
onStartCommand determines how the system manages our service.
The default implementation of
onStart() and returns START_STICKY, indicating that our service should be left hanging around for as long as possible. I am guessing this replicates some early, fairly crude system management behaviour from an early Android version.
I want to be a bit more accommodating and return START_NOT_STICKY since our service is not long-running and doesn’t need to be kept hanging around and the system should be able to get rid of it if necessary. The service will simply be restarted by my widget if and when needed.
In the onStartCommand implementation itself I also need to change the way I update my widget(s). Previously the service was always started with an intent that came packaged with an array of widget ID’s identifying the widgets to be updated. This is still the case when the service is started in response to an onUpdate() intent, resulting from the placement of a new instance of the widget.
But when the service is started by my alarm, there is no array of widget ID’s. The service instead should update all instances of the widget, and so it must use a slightly different version of the AppWidgetManager.updateAppWidget() method, once which identifies the target widgets not by ID but by class name:
method UpdateService.onStartCommand(aIntent: Intent; aFlags: Integer; aStartID: Integer): Integer; begin var update := buildUpdate; if aIntent.hasExtra('ids') then begin var ids := aIntent.Extras.IntArray['ids']; AppWidgetManager.Instance[self].updateAppWidget(ids, update); end else AppWidgetManager.Instance[self].updateAppWidget(new ComponentName(self, typeOf(BatteryWidgetProvider)), update); result := Service.START_NOT_STICKY; end;
And there we go. A much more well behaved widget that will do it’s best to not drain the battery it is so carefully monitoring for us.
Well, not quite everything.
There is (at least) one more refinement we can make. For those not already au fait with such things, here’s a clue: Just because the screen on your device is off, does not mean that the device is necessarily yet asleep.
Update: This morning I also discovered a bug which is that my widget update alarm did not get re-set when my phone was awoken from what I presume was a deep sleep state that it entered over-night. I have yet to figure out why this is, but I suspect that my service and broadcast receiver are being cleaned up by the system. I am being a little bit too conservative somewhere.