In the first instalment of this series, I implemented the basic framework of a new appwidget and established a means by which I could debug the widget code. Now it’s time to add some code worth debugging.
You may recall that the aim of my widget is to display the battery charge level so the first order of business is to figure out how to read the battery level.
It turns out that the means by which we obtain this information involves Intents. On this occasion however, we shall be working with something called a “sticky intent”.
The normal procedure when consuming intents – as far as my current understanding goes – is to create or declare an intent filter that will match the required intent. You then declare a receiver and wait for the expected notification to arrive at that receiver, together with the corresponding Intent object containing all the information necessary for the receiver to respond (or simply behave) appropriately.
A sticky intent does not require you to register a receiver however. At least, not a real one.
You simply setup the IntentFilter that will match the intent involved and register a null receiver. You then immediately are returned a reference to the sticky intent.
Information about the device battery is one such sticky intent, and the corresponding action is ACTION_BATTERY_CHANGED. So, let us first make sure we can get the required battery information, then we’ll worry about how to update the widget UI with that information.
This code in the widget onUpdate method should do the trick:
method BatteryWidgetProvider.onUpdate(aContext: Context; aWidgetManager: AppWidgetManager; aIDs: array of Integer); var filter: IntentFilter; battery: Intent; level, scale: Integer; pct: Float; begin filter := new IntentFilter(Intent.ACTION_BATTERY_CHANGED); battery := aContext.registerReceiver(NIL, filter); level := battery.IntExtra[BatteryManager.EXTRA_LEVEL, -1]; scale := battery.IntExtra[BatteryManager.EXTRA_SCALE, -1]; pct := (100 * level) / scale; end;
We create a new IntentFilter object for the standard ACTION_BATTERY_CHANGED action. We then call registerReceiver on our context, passing a NIL reference for the receiver and the filter we just created and we get back the sticky intent holding the current battery state.
From the battery Intent we can now extract the information we need which in this case is two values: the battery level and the maximum battery level. Somewhat confusingly this second value is called the scale.
These two pieces of information are contained within the Extras Bundle of the sticky intent. Each is an integer value whose names are among several defined as constants by the BatteryManager class, and there are a number of ways of extracting them.
Incidentally, the BatteryManager class resides in the android.os namespace, so this is added to the uses list of the unit containing BatteryWidgetProvider.
When implementing Intent support in my camera app, I used methods of the Extras Bundle itself, but on this occasion I am taking advantage of the fact that the Intent object itself provides methods for reading such values and in this case I have the option of using methods which allow me to supply a default value if the value I ask for does not actually exist.
The Java method is
getIntExtra( name, defaultValue ) but as you can see, I am using the fact that Oxygene presents such methods as properties with accessor indices.
So, with the battery level (BatteryManager.EXTRA_LEVEL) and the maximum battery level (BatteryManager.EXTRA_SCALE) calculating the percentage battery level is a simple matter of some basic arithmetic.
Trying to run this widget throws up a problem, in the form of a runtime exception caught and presented to us by the debugger:
I should confess that from the little bit of research I did before setting about implementing this widget, I already knew this would happen but wanted to show you the consequences.
Now, I could not find any reference to “IntentReceiver” in the Android SDK and searching for it throws up only BroadcastReceiver. There are some docs on the interwebs which appear to document a very early Android SDK with an IIntentReceiver interface and I wonder if this exception message is just perhaps a little out of date and should now say BroadcastReceiver.
In any event, our AppWidgetProvider is a BroadcastReceiver and does indeed receive intents, so whatever this IntentReceiver actually refers to, it does seem to apply to my situation.
So far our widget app contains one component – the AppWidgetProvider for our widget.
For debug builds it contains a second component – my DebugActivity.
But both of these declare an intent-filter and are thus both receivers of intents. Any call made using the Context of either of these components to registerReceiver is going to fail for the same reason.
What we need is some other component that can be invoked directly without having to respond to an intent, and which has a Context which can be used to call registerReceiver for the sticky intent required to get the battery information.
Fortunately, there is just such a component type available: A Service.
Android, At Your Service
A Service can – and often does – support one or more intent filters, but a service does not have to and if it does not it can still be invoked directly by name if required. This is exactly what we shall do in this case.
First of all, we add a new class to the project, extending the Service class. I called mine UpdateService.
interface uses android.app, android.content, android.widget; type UpdateService = public class(Service) public method onBind(aIntent: Intent): IBinder; override; method onStart(aIntent: Intent; aStartID: Integer); override; end;
I am only scratching the surface of Android Services here. Mine is a very simple service which will obtain the information required to update our widget, do the update and then … well, I’m not entirely sure. The service may simply die at that point, or it may sit around doing nothing until the OS kills it or it is invoked again. I’m not entirely sure, but I don’t think it really matters.
This service doesn’t need to worry about binding, but we do have to override the onBind method since it is abstract in the Service class. For our purposes we simply return NIL from this method.
The onStart override is where this service takes care of everything, and we will look at that shortly. But first, with this Service looking after the actual business of performing the update, our widget onUpdate code then becomes a simple call to invoke this service:
method BatteryWidgetProvider.onUpdate(aContext: Context; aWidgetManager: AppWidgetManager; aIDs: array of Integer); var updateIntent: Intent; begin updateIntent := new Intent(aContext, typeof(UpdateService)); updateIntent.putExtra('ids', aIDs); aContext.startService(updateIntent); end;
To invoke a service by name we create an Intent using a constructor identifying the calling context and a reference to the service class to be invoked. In Java this class reference would be obtained using what appears to be a static member of the Class type (
Class.class) but which is in fact a bit of Java compiler magic. In Oxygene the equivalent magic is the typeof() function.
With our freshly constructed Intent, we can then add whatever information that service may require passed to it via that Intent. In this case we provide the array of Widget ID’s being updated (the user might have placed multiple instances of our widget on their device and each has a unique ID).
The Intent.putExtra method is a massively overloaded method (24 varieties!) which supports putting a huge variety of types of information – in this case an array of Integer – as named values into the Intent Extras Bundle.
Once the intent is ready, we then pass it to the startService method of the context.
We now have a widget provider that will invoke a service to update any widget instances when required. So now let’s complete the update service itself.
Adding Updatability to the Update Service
First of all, for a service to update a widget is really quite trivial. As mentioned in part 1, widgets are updated by providing an updated layout representing the state of the widget at a point in time.
The “snapshot” is presented to the widget in the form of a new RemoteViews object containing the complete, updated layout to be applied to the widgets being updated. So, the UpdateService.onStart method is quite simple:
method UpdateService.onStart(aIntent: Intent; aStartID: Integer); var update: RemoteViews; ids: array of Integer; begin update := buildUpdate(self); ids := aIntent.Extras.IntArray['ids']; AppWidgetManager.Instance[self].updateAppWidget(ids, update); end;
We declare a RemoteViews object. We will build this in a new method – to be called buildUpdate – accepting a reference to the current context (the service itself, i.e. self). We will add this method next, but first a quick look at how the service updates the widgets with the new RemoteViews.
First I extract the array of widget ID’s identifying the widget instances to be updated, from the Extras bundle of the Intent that started the service (remember, this array of ID’s was placed there by the widget provider when it invoked the service in the onUpdate provider method).
I then obtain the appropriate instance of AppWidgetManager for the current context, conveniently provided for me in the AppWidgetManager class Instance indexed property (surprise, surprise: in Java it would be
The AppWidgetManager.updateAppWidget method has a number of overloads for updating widgets by different identifying criteria. In this case I can use the method that accepts an explicit array of widget ID’s.
All that is left now is to implement the buildUpdate method to actually construct the updated widget UI.
Building the Update
We have already seen how to obtain the current battery level so we can put that code in our buildUpdate() method for starters:
method UpdateService.buildUpdate(const aContext: Context): RemoteViews; var filter: IntentFilter; battery: Intent; level, scale: Integer; pct: Float; begin filter := new IntentFilter(Intent.ACTION_BATTERY_CHANGED); battery := aContext.registerReceiver(NIL, filter); level := battery.IntExtra[BatteryManager.EXTRA_LEVEL, -1]; scale := battery.IntExtra[BatteryManager.EXTRA_SCALE, -1]; pct := (100 * level) / scale; end;
Now all that’s required to is construct the RemoteViews object and update the layout with the battery information:
method UpdateService.buildUpdate(const aContext: Context): RemoteViews; var filter: IntentFilter; battery: Intent; level, scale, pct: Integer; begin filter := new IntentFilter(Intent.ACTION_BATTERY_CHANGED); battery := aContext.registerReceiver(NIL, filter); level := battery.IntExtra[BatteryManager.EXTRA_LEVEL, -1]; scale := battery.IntExtra[BatteryManager.EXTRA_SCALE, -1]; pct := (100 * level) div scale; // Build a remote view to update the widget UI result := new RemoteViews(aContext.PackageName, R.layout.widgetlayout); result.setTextViewText(R.id.lblInfo, pct.toString + '%'); end;
With such a simple layout for the widget UI, building the updated RemoteViews object is itself also very simple.
First we construct a RemoteViews object, employing a constructor that identifies the layout resource that we wish to use.
Again, we see a construct that would be familiar to an Eclipse or Android Studio Java developer. The res folder in our project is represented in our application by the R object, the layout folder by the R.layout member, and the layout resources (files) as the members of that folder.
We don’t have to use the same layout that we used for the initial layout of the widget. In a more sophisticated widget we might use various layouts according to runtime conditions, but in this case we only need to make one small change to the layout we already declared, which is to change the Text attribute of the TextView to the battery level that we wish to display.
A number of methods are provided by the RemoteViews object for manipulating the views within it. In this case we are setting the Text property of a TextView.
We identify the TextView in question by reference to the
@id+ id we assigned in the layout – lblInfo. Again, just as with the resources, these ID’s are created as members of the R object, this time as members of the R.id member.
I should explain that I’m not interested in anything more accurate than whole percentage points, and, on my device at least, the battery level is not reported to any greater level of accuracy anyway (0-100 only), which may be the case on all devices for all I know. So in this final version I’ve tweaked the variables and the expression for calculating the pct value to use integer division.
The text value we are setting it the string representation of the battery level percentage, with a ‘%’ sign appended.
We return this new RemoteViews object to where the buildUpdate method was called – the onStart method of our service – which in turn passes it to the AppWidgetManager which will apply it to our widget UI.
Just one little tweak is needed in the layout to ensure that the text of our widget is visible on both light and dark backgrounds, which is to add a drop-shadow effect by adding the following attributes to the TextView in the widgetlayout:
android:textColor="@android:color/white" android:shadowColor="@android:color/black" android:shadowDx="2" android:shadowDy="2" android:shadowRadius="2"
There we go.
Not perfect but, as with much else with these exercises, it will do for now. 🙂
For those of a curious disposition, here’s the full source for you to play around with:
In a real widget I might instead choose to provide the user with some control over the appearance of the widget so that they can adjust it to suit their particular preferences and device wallpaper etc.
I should also implement much more sophisticated battery management. Since my widget really should only update if the device is already awake, I should listen for notifications from the system telling me when the device enters and leaves sleep state, and use an alarm based update to only update when already awake.
Since I intend using this widget myself, I will be turning my attention to that at some point, but for now, this short series has hopefully added a little more to your knowledge of Android development with Oxygene, as the experience of putting it together has for me.