I’ve been working on a new app lately – a CPU monitor live wallpaper for Android. There are some great tutorials & sample code out there if you’re looking at starting with live wallpapers. I recommend the Androgames tutorial, as well as the official Android documentation, and CubeLiveWallpaper sample application. Both tutorials take a slightly different approach, so make sure you understand the concepts (especially threading) thoroughly before writing your own wallpaper. I started off using the Androgames model, but then ended up completely rewriting the code to optimize battery use. The end result is much more like the CubeLiveWallpaper example.
While coding the settings screen for this app, I ran across two pretty major bugs in the current Android SDK. It seems the implementation of PreferenceActivity is quite broken, and has been for several versions of the SDK. In particular, ListPreference and CheckBoxPreference each have outstanding bugs which nearly every developer will encounter when trying to use these classes.
The standard method of creating a preferences screen for your application is to use a PreferenceActivity, which loads a description of the preferences from an XML file. There are a few standard preference widgets supplied, including ListPreference (for handling preferences with a specific list of possible values).
To use ListPreference, you have to specify an array of human-readable entries, and a matching array of values to be used by the program. The human-readable entries will obviously be an array of strings. For the values however, it’s easy to assume you could use a string-array or integer-array. In fact, creating a ListPreference using an integer-array will compile perfectly fine, with no warnings. But if you launch the app, and try to select a value, Android will throw a runtime exception:
Thread [<3> main] (Suspended (exception NullPointerException)) ListPreference.findIndexOfValue(String) line: 169 ListPreference.getValueIndex() line: 178 ListPreference.onPrepareDialogBuilder(AlertDialog$Builder) line: 190 ListPreference(DialogPreference).showDialog(Bundle) line: 291 ListPreference(DialogPreference).onClick() line: 262 ListPreference(Preference).performClick(PreferenceScreen) line: 810 PreferenceScreen.onItemClick(AdapterView, View, int, long) line: 182 ListView(AdapterView).performItemClick(View, int, long) line: 283 ListView.performItemClick(View, int, long) line: 3132 AbsListView$PerformClick.run() line: 1620 ViewRoot(Handler).handleCallback(Message) line: 587 ViewRoot(Handler).dispatchMessage(Message) line: 92 Looper.loop() line: 123 ActivityThread.main(String) line: 3948 Method.invokeNative(Object, Object, Class, Class, Class, int, boolean) line: not available [native method] Method.invoke(Object, Object...) line: 521 ZygoteInit$MethodAndArgsCaller.run() line: 782 ZygoteInit.main(String) line: 540 NativeStart.main(String) line: not available [native method]
Google has even confirmed this is a bug – although they’re calling it a “feature request” with medium priority. The problem is, when a developer sees a NullPointerException runtime error, they’ll naturally assume it’s their fault and look for something wrong. Whereas in this case, it’s actually a known bug in the SDK.
I spent quite a bit of time trying to debug this error myself, until I found out about the issue. The only workaround, is to use a string-array to store preferences (even if you’re storing a list of numbers), and then fetch the preference using getString(), and cast to an Integer:
refreshInterval = Integer.parseInt(prefs.getString("refresh_interval", "5000"));
It’s a bit of an ugly fix, but necessary until Google patches the SDK.
The other bug I found is more subtle, but no less annoying. It turns out that you can load the default values from XML for Shared Preferences using PreferenceManager. Simply call the function in your onCreate() method and the default values are assigned automatically at first startup. Except for CheckBoxPreferences. If you set the default value for a CheckBoxPreference to “true”, it will initialize as expected. But if you set the default to “false”, it will simply skip that preference, leaving it empty. This can lead to unexpected behaviour, and is very hard to track down.
The workaround: whenever you fetch a boolean shared preference, pass a second argument specifying the default value as “false”:
USE_GRADIENT = prefs.getBoolean("gradient_enabled", false);
If the default value was set to “true” in the XML, it will fetch that value correctly. If, however, the default value was set to “false”, the getBoolean() method will find an empty preference value, and return the second argument.
If all that talk of shared preferences and XML files sounded a little confusing, I recommend this post at kaloer.com for a solid overview of Android Preferences. The concept behind them is great, but these bugs in the implementation really dampen my enthusiasm.
Hopefully Google can release a patch (or two), and have this integrated into the next version of Android. But I’m not holding my breath. For now, it looks like I’ll just have to remember the workarounds, to avoid pointless debugging.