How to create an Android Battery Widget

I will go over how to create a simple battery widget for Android. The complete project used in this tutorial can be downloaded from my github. I decided to write this because I had some hard time looking for information to learn how to write a battery widget. Hopefully this will help someone.

Implementation

This widget displays the battery level with the time interval specified in the code. I use alarm to periodically check if the battery level has changed. Since we don’t want to waste the battery power, we want to keep the attempts to check the battery level to minimum.

When the widget is enabled, it starts a service that wraps 3 BroadcastReceiver objects to watch for 1. screen off, 2. screen on, and 3. when the user is passed through lock screen.

1. It will disable the alarm when the screen is turned off.

2. When the screen is turned on, it will check if the screen is on lock screen(called keyguard). If keyguard is on, it will not do anything. For example, when you have a browser open and you let the screen turn off by itself, if you turn it on right away, your phone will skip the lock screen and turn back on. To accommodate this situation, we perform this check.

3. Lastly, when the user unlocks the screen and proceeds, we want to start the alarm again.

In this app, I have the alarm set to check every 5 minutes. We don’t want to check every time the app receives Intent.ACTION_BATTERY_CHANGED. Intent.ACTION_BATTERY_CHANGED fires too often and the frequency seem to vary between devices. When I set the alarm when the device’s screen is turned on and passed beyond the keyguard, I want to make sure the widget looks at the most current battery level. To accomplish this, I set the first alarm to fire after 1 second(1000 ms) instead of letting the alarm trigger right away. Sometimes Intent.ACTION_BATTERY_CHANGED intent gets sent after we check the battery level which the order should be the opposite.

Structure of code

This app is mostly made of following.

  • BatteryWidget.java – the widget
  • ScreenMonitorService.java – wraps 3 BroadcastReceiver objects to watch for Intent.ACTION_SCREEN_OFF, Intent.ACTION_SCREEN_ON, and INTENT.ACTION_USER_PRESENT
  • res/layout/widget_layout.xml – defines how the widget will look like specified by widget_info.xml
  • res/xml/widget_info.xml – attributes of widget
  • AndroidManifest.xml – you need to define the receiver and service tags

I will go over the main points in each file. Again, the complete source code is here.

res/xml/widget_info.xml

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:updatePeriodMillis="0"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:initialLayout="@layout/widget_layout" />

android:updatePeriodMillis is set to 0 because we are not going to rely on this to do our updates.

res/layout/widget_layout.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <TextView 
        android:textIsSelectable="false"
        android:id="@+id/batteryText"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:background="#282828"
        android:padding="6dp"
        android:textColor="#ffffff"
        android:textSize="21sp" />
 
</RelativeLayout>

Nothing special here. Just a layout. Make sure you set id for the view you want to manipulate at update.

Inside <application> tag in AndriodManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<receiver
    android:name="com.migapro.battery.BatteryWidget"
    android:label="@string/app_name" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        <action android:name="com.migapro.battery.action.UPDATE" />
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/widget_info" />
</receiver>
 
<service android:name="com.migapro.battery.ScreenMonitorService"></service>

Define your widget and service. We named com.migapro.battery.action.UPDATE to use as a hook for event.

BatteryWidget.java

1
2
3
4
5
6
7
8
9
@Override
public void onEnabled(Context context) {
	super.onEnabled(context);
 
	LogFile.log("onEnabled()");
 
	turnAlarmOnOff(context, true);
	context.startService(new Intent(context, ScreenMonitorService.class));
}

I think onEnabled method is straight forward enough. Set the alarm and start the service class explained later below. onEnabled is called when the first widget is created. onReceive will be called right after this with android.appwidget.action.APPWIDGET_ENABLED intent, but we don’t need to do anything there.

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void turnAlarmOnOff(Context context, boolean turnOn) {
	AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
	Intent intent = new Intent(ACTION_BATTERY_UPDATE);
	PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
 
	if (turnOn) { // Add extra 1 sec because sometimes ACTION_BATTERY_CHANGED is called after the first alarm
		alarmManager.setInExactRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, 300 * 1000, pendingIntent);
		LogFile.log("Alarm set");
	} else {
		alarmManager.cancel(pendingIntent);
		LogFile.log("Alarm disabled");
	}
}

According to the official document for alarm, setInExactRepeating is more economic than setRepeating for battery usage. It is also recommended to use ELAPSED_REALTIME. As discussed in the beginning section of this article, the first alarm is set to fire in one second to give some time to ensure the Intent.ACTION_BATTERY_CHANGED is fired before we check the battery level.

ACTION_BATTERY_UPDATE is a String constant containing “com.migapro.battery.action.UPDATE” which we use to notify onReceive method that the alarm is firing. The second argument in PendingIntent is a request code. 0 is passed since we don’t need to distinguish between multiple widgets. We want all the widgets to display the same data simultaneously.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
	super.onUpdate(context, appWidgetManager, appWidgetIds);
 
	LogFile.log("onUpdate()");
 
	// Sometimes when the phone is booting, onUpdate method gets called before onEnabled()
	int currentLevel = calculateBatteryLevel(context);
	if (batteryChanged(currentLevel)) {
		batteryLevel = currentLevel;
		LogFile.log("Battery changed");
	}
	updateViews(context);
}
 
private boolean batteryChanged(int currentLevelLeft) {
	return (batteryLevel != currentLevelLeft);
}

onUpdate method is called only once in the beginning. This is most likely the first time we set the views, so we check the battery level. In this code, this method should be executed before the first alarm is fired. Basically, we don’t wait for alarm because we want to have something displayed asap.

1
2
3
4
5
6
7
8
9
private int calculateBatteryLevel(Context context) {
	LogFile.log("calculateBatteryLevel()");
 
	Intent batteryIntent = context.getApplicationContext().registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
 
	int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
	int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
	return level * 100 / scale;
}




On line 4, we go grab the battery information. We need both the level and scale to get the device’s battery level.

1
2
3
4
5
6
7
8
9
10
private void updateViews(Context context) {
	LogFile.log("updateViews()");
 
	RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
	views.setTextViewText(R.id.batteryText, batteryLevel + "%");
 
	ComponentName componentName = new ComponentName(context, BatteryWidget.class);
	AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
	appWidgetManager.updateAppWidget(componentName, views);
}

Use RemoteViews to manipulate the views on widget. This is where you work on the visual representation of widget. If you want to make it display different images according to the battery level, you can write that code here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onReceive(Context context, Intent intent) {
	super.onReceive(context, intent);
 
	LogFile.log("onReceive() " + intent.getAction());
 
	if (intent.getAction().equals(ACTION_BATTERY_UPDATE)) {
		int currentLevel = calculateBatteryLevel(context);
		if (batteryChanged(currentLevel)) {
			LogFile.log("Battery changed");
			batteryLevel = currentLevel;
			updateViews(context);
		}
	}
}

As I mentioned above, we only check the battery level if this came from alarm.

ScreenMonitorService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void registerScreenOffReceiver() {
	screenOffReceiver = new BroadcastReceiver() {
 
		@Override
		public void onReceive(Context context, Intent intent) {
			LogFile.log(intent.getAction());
			BatteryWidget.turnAlarmOnOff(context, false);
		}
 
	};
 
	registerReceiver(screenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
}
 
private void registerScreenOnReceiver() {
	screenOnReceiver = new BroadcastReceiver() {
 
		@Override
		public void onReceive(Context context, Intent intent) {
			LogFile.log(intent.getAction());
 
			KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
			if (!keyguardManager.inKeyguardRestrictedInputMode())
				BatteryWidget.turnAlarmOnOff(context, true);
		}
 
	};
 
	registerReceiver(screenOnReceiver, new IntentFilter(Intent.ACTION_SCREEN_ON));
}
 
private void registerUserPresentReceiver() {
	userPresentReceiver = new BroadcastReceiver() {
 
		@Override
		public void onReceive(Context context, Intent intent) {
			LogFile.log(intent.getAction());
 
			BatteryWidget.turnAlarmOnOff(context, true);
		}
 
	};
 
	registerReceiver(userPresentReceiver, new IntentFilter(Intent.ACTION_USER_PRESENT));
}

These are the important methods in this class. So, we said we watch for 3 broadcasts. In lines 1 – 13, we set BroadcastReceiver for screen turning off. We simply disable the alarm. Then lines 15 – 30 will instantiate BroadcastReceiver for screen turning on. We only set the alarm on if the device skips lock screen or keyguard. The last one we watch for is surpassing the keyguard. With no condition, we just start the alarm.

Looking at the log

I inserted code to take logs on events, so you can play around yourself. It will be easier to trace if you compare with the source code.

Below is the log taken at the initialization of widget. Line #1, onEnabled() is first called and #2 alarm is set. On #3, onReceive() is called following the previous event, onEnabled(), but it doesn’t do anything. We will check for battery level change only when it is called by the alarm we defined. onUpdate() is called only once and it will not be called again unless the device is rebooted. I found out that sometimes onUpdate() gets called before onEnabled() when rebooting, so we will also do a battery check there and make sure we prepare the UI. Line #7 is called because of onUpdate(), but again, we do not do anything there. Then when we receive intent with com.migapro.battery.action.UPDATE action, we go ahead and check the battery level. This onReceive method with this intent is where we will do the routine.

1
2
3
4
5
6
7
8
9
15:38:46:308 onEnabled()
15:38:46:329 Alarm set
15:38:46:347 onReceive() android.appwidget.action.APPWIDGET_ENABLED
15:38:46:385 onUpdate()
15:38:46:387 calculateBatteryLevel()
15:38:46:396 updateViews()
15:38:46:409 onReceive() android.appwidget.action.APPWIDGET_UPDATE
15:38:47:322 onReceive() com.migapro.battery.action.UPDATE
15:38:47:329 calculateBatteryLevel()

It keeps going as below. Perform a battery level check every 5 minutes. Update the views only when the battery level has changed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15:43:47:326 onReceive() com.migapro.battery.action.UPDATE
15:43:47:336 calculateBatteryLevel()
15:48:47:326 onReceive() com.migapro.battery.action.UPDATE
15:48:47:331 calculateBatteryLevel()
15:53:47:328 onReceive() com.migapro.battery.action.UPDATE
15:53:47:332 calculateBatteryLevel()
15:58:47:327 onReceive() com.migapro.battery.action.UPDATE
15:58:47:333 calculateBatteryLevel()
16:03:47:327 onReceive() com.migapro.battery.action.UPDATE
16:03:47:332 calculateBatteryLevel()
16:08:47:330 onReceive() com.migapro.battery.action.UPDATE
16:08:47:335 calculateBatteryLevel()
16:08:47:346 Battery changed
16:08:47:352 updateViews()

Comments are closed.

Categories