In my previous post I explained how I believed I had solved a problem with my widget, only to discover that it created a different problem in the process.
I had believed that IntentService based services were long-lived, but in fact this is not the case. However, the change remains valid for solving the problem of my update alarm surviving device sleep, leaving only the question of how to refactor the behaviour that using an IntentService had broken.
Devices Playing Possum
When an Android device turns off the screen, this does not necessarily mean that the device is yet asleep. Any application or widget that continues to update itself when the screen is off is pretty much wasting it’s time but more importantly is wasting precious battery as well.
Fortunately there are notifications we can receive that will allow us to adjust our behaviour when the screen goes off (and comes back on) which means we can be even more efficient in our battery use.
Yet More Intents
As you may have come to expect by now, this involves Intents. Again.
In this case we are interested in a pair of system defined intents:
Unlike the battery level information, these intents are not “sticky”. They are dynamic intents, broadcast to interested parties as and when the screen is turned on or off, whether by the system in response to a time-out, or by the user specifically.
These particular system intents are a bit unusual however.
Normally we would add an
intent-filter to our manifest to register our interest in receiving intents and identifying the entity (or entities) in our application that will respond to them. But these intents specifically cannot be declared in this way. Or rather, you can declare them but they won’t be acknowledged by the system. You won’t receive the intents.
Instead we must create and register a receiver at runtime.
Originally, I chose to create and register my receiver in the
onHandleIntent method of my
UpdateService. This was the mistake I made. I had believed that my UpdateService was “long-lived” when it wasn’t. However, deriving from IntentService did address my issue with surviving sleep.
So, for my screen state notifications all I needed to do was create an entirely new service that is long-lived.
interface ScreenStateService = public class (Service) private class var cfScreenStateReceiver: ScreenStateReceiver; public method onDestroy; override; method onBind(aIntent: Intent): IBinder; override; method onStartCommand(aIntent: Intent; aFlags, aStartID: Integer): Integer; override; end;
As before my service class overrides
onBind to provide a default implementation returning NIL.
onStartCommand override creates and registers the receiver of the
onDestroy override unregisters the receiver.
The cfScreenStateReceiver class var holds a reference to my registered receiver for as long as it remains registered.
implementation method ScreenStateService.onDestroy; begin unregisterReceiver(fScreenStateReceiver); fScreenStateReceiver := NIL; inherited; end; method ScreenStateService.onStartCommand(aIntent: Intent; aFlags: Integer; aStartID: Integer): Integer; begin if NOT assigned(fScreenStateReceiver) then begin var screenState := new IntentFilter; screenState.addAction(Intent.ACTION_SCREEN_ON); screenState.addAction(Intent.ACTION_SCREEN_OFF); fScreenStateReceiver := new ScreenStateReceiver; registerReceiver(fScreenStateReceiver, screenState); end; result := START_STICKY; end;
Creating the receiver is simple enough, as is registering it using the
registerReceiver method, though this time we pass a reference to the receiver to be received, rather than NIL, as we did with the battery information “sticky” intent.
We also pass in the IntentFilter identifying the intents this receiver should receive. This also is relatively straightforward. We simply create the new filter and add the actions of interest to it.
The key to making the service long-lived is to return Service.START_STICKY from this method.
and Prosper If You Have Lots To Process
This code was originally implemented in my existing UpdateService, and (with the addition of calls to Android Log methods) I found that my UpdateService would register the ScreenStateReceiver but was then immediately destroyed and thus immediately UNregister that receiver. This was how I learned that the notion that an IntentService is long-lived was wrong.
With an IntentService you don’t get to decide what is returned from
onStartCommand since the extension point is
onHandleIntent, with no mechanism for indicating any preferred service lifetime.
So where did I get the idea that IntentService was appropriate for long-lived services ?
The key is in distinguishing between a stated service lifetime which may determine how the service is managed after it completes processing, and the amount of time required to perform that processing itself.
The real point of IntentService is that
onHandleIntent is called by a worker thread, and thus will not block your application main thread. Thus, if you have a service which will be doing a potentially significant amount of work when handling an intent, an IntentService is a convenient way of shunting that work into a worker thread.
But once the work is done (onHandleIntent returns) the service will be quickly cleaned up.
In other words, if you have a potentially long living service, an IntentService based implementation is highly recommended. But using an IntentService does not itself make a service long lived.
In any event, I now have a suitable service which will register a receiver for the intents of interest. The question now is what to do when we receive these intents, and this is of course determined by how I implement the ScreenStateReceiver class itself.
Receiving Intents, Load and Clear
First of all, declaring the ScreenStateReceiver is trivial. It is a sub-class of BroadcastReceiver, and I only need to override one method,
interface uses android.app, android.content; type ScreenStateReceiver = public class(BroadcastReceiver) public method onReceive(aContext: Context; aIntent: Intent); override; end;
The question is: What should I do when the screen comes on or goes off ?
What I want to do is enable or disable my update alarm as appropriate. If the screen goes off, I want to stop updating my widget, and when the screen comes on resume those updates.
But the code for doing this is in my widget provider. I could perhaps duplicate the scheduling and cancellation of the alarm and in this simple case such duplication might be acceptable. I could perhaps refactor the code into class methods on the widget provider class which I can call directly from my ScreenStateReceiver. But this feels wrong for some reason (I honestly do not know if it is)
What I would prefer to do is ensure that the communication between my ScreenStateReceiver and the widget provider follows normal Android protocols and that means intents. This time, sending an intent.
I cannot send the system defined ACTION_SCREEN_ON/OFF intents themselves. Android system will not allow that with these particular intents.
But I can send a custom intent and with an
intent-filter in my manifest I can ensure that my BatteryWidgetProvider will be signalled.
So first, a couple of class constants for the intent names, to avoid silly typing mistakes:
ScreenStateReceiver = public class(BroadcastReceiver) public const SCREEN_ON = 'nz.co.deltics.SCREEN_ON'; const SCREEN_OFF = 'nz.co.deltics.SCREEN_OFF'; ... end;
And now to receive the system screen state intents and pass them on to the widget provider in the form of my corresponding custom intents:
method ScreenStateReceiver.onReceive(aContext: Context; aIntent: Intent); begin var i := new Intent(aContext, typeOf(BatteryWidgetProvider)); case aIntent.Action of Intent.ACTION_SCREEN_ON : i.Action := SCREEN_ON; Intent.ACTION_SCREEN_OFF : i.Action := SCREEN_OFF; else i := NIL; end; if assigned(i) then aContext.sendBroadcast(i) else inherited; end;
First I create the intent I will be sending. Just to simplify the rest of the method I create this before determining whether it will be needed. A bit wasteful perhaps but not a major concern I don’t think.
I then use a case statement to set the appropriate custom intent constant (string) as the Action of the intent. I simply NIL the intent reference ‘
i‘ if the intent I received turns out not to be one of the screen state intents.
This is a little demonstration of the fact that Oxygene case statements are more flexible than we are used to in Delphi: they support strings! This really isn’t your grand-daddy’s Pascal.
If I end up with an intent, I broadcast it. Otherwise I pass the buck, up to the inherited
onReceive implementation, just in case.
Now is as good time as any to add my actions to the
intent-filter for the widget provider in the manifest:
<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" /> <action android:name="nz.co.deltics.SCREEN_OFF" /> <action android:name="nz.co.deltics.SCREEN_ON" /> </intent-filter>
And finally implement an override of the onReceive method in the widget provider itself to respond to these actions:
method BatteryWidgetProvider.onReceive(aContext: Context; aIntent: Intent); begin case aIntent.Action of ScreenStateReceiver.SCREEN_ON : onEnabled(aContext); ScreenStateReceiver.SCREEN_OFF : onDisabled(aContext); else inherited; end end;
The code I needed was ready and waiting on the widget provider class in the form of the
onDisable() methods, so when I receive a SCREEN_ON/SCREEN_OFF intent, I simply call the appropriate one of those methods.
It is very important that I call the inherited implementation of
onReceive() in this case since I know for a fact that the superclass responds to a variety of other intents necessary to the functioning of an AppWidgetProvider (you may recall this is a specialisation of BroadcastReceiver).
onDisable() methods are introduced by this base class and will be called by the inherited
onReceive() implementation in response to other system generated intents.
Just one last thing and it’s all done.
Getting the Service Started
To support these screen state intents I have introduced a new service, but the intents that service is designed to support are not intents that we can declare in the manifest, so as things stand currently, this new service will never get started and will never register the receiver that my widget relies on for the screen state notifications.
Somewhere I need to start this service myself.
I decided that the
onEnabled() handler of my BatteryWidgetProvider would suit.
Starting the ScreenStateService here means that the service is certain to be started at some suitable point when I have an active instance of my widget.
Further more, the
onEnabled() method is also called whenever the device screen comes on (thanks to my handling of that intent). I am not entirely certain that it is necessary, but it surely can’t hurt that re-starting the service at this point will add further weight to the START_STICKY nature of the service, making it even less likely that the service will be cleaned up unless absolutely necessary. I think. Either way I don’t think it can hurt.
So I add the following line to my
aContext.startService(new Intent(aContext, typeOf(ScreenStateService)));
All Over Bar the Shouting
My widget now will suspend updates as soon as the screen is turned off – for whatever reason – even before the device has entered sleep, and will resume updates when the screen is turned on. Since this will start a new alarm schedule, this means the widget will also perform an initial update so that I get an accurate battery level reading instantly.
This latter point is worth highlighting.
I am running a custom ROM on my Android phone, and this incorporates a battery level reading on the default lock screen. Interestingly, this is not refreshed when the screen comes on. As a result, the battery level initially presented on my lock screen is often a little, um, optimistic.
Also worth mentioning is that I have been running my revised widget on my phone all day for the past 2 days and as far as I can tell, battery use has been no greater than usual but continues to update consistently, so it would appear that the time spent ensuring that my widget behaves itself has been well spent!
The final leg in this journey is to get my widget up in the Google Play store.
Not that I think it really has anything compelling to offer alongside the myriad other battery widgets that are already there, but it will be a useful exercise in going through the process of getting something approved for the store.
An experience which – all being well – I will then also share.
Comments are now closed.