Update: I've refined the process, and produced an Android jar which can be used in your project to achieve the resource-qualifier based injection! Check it out at FrankenRobot project page!.
As the the maintainer of AnySoftKeyboard, I'm obligated to create a product which works flawlessly on various Android OS versions, starting with 1.5, and to provide advanced features as the underlying OS provides. Luckily, the Android platform provides several ways to create products which are backward compatible, e.g., custom resources per API version and Java's reflection.
In this post I'll show how backward compatibility is done in AnySoftKeyboard, and how advanced features like multi-touch, Backup Agent, Contacts dictionary, and more, are consumed without crashing the application due to ClassNotFoundException or NoSuchMethodException.
Overview
The main Android feature I use for achieving backward compatibility, is the ability to provide API level specific resources, along with a nifty software engineering (a.k.a, object oriented programming). Java's reflection is also used, but scarcely.
Practice
Contacts dictionary checkbox
The ability to query the contacts list in Android was introduced in version 2 (a.k.a API Level 5), so I wanted to limit its settings in devices with API Level 3 (OS 1.5) and 4 (OS 1.6).
This was done by providing a specific dictionary settings layout file for devices running API 5 (where the 'Contacts dictionary' checkbox is enabled) and a different layout file for devices running API 4 or 3 (where the 'Contacts dictionary' checkbox is disabled, and marked off).
In the image above, you see two folders, one for API 4 and the other for API 5. When inflating the Dictionary Settings layout, the file 'prefs_dictionaries.xml' is searched. Android will locate the best match starting with the device's API level, and then searching for lower API levels, so, if my device is API 6, Android will look for 'prefs_dictionaries.xml' under layout-v6 and since it is not there, it will look under layout-v5. In my case, it will stop there since the file was found, but if it wasn't there, it would have continue the search. In API 4 devices, it will start with layout-v4 folder.
In API 4 'prefs_dictionaries.xml' I've disabled the dictionary by including this XML layout:
<CheckBoxPreference
android:key="@string/settings_key_use_contacts_dictionary"
android:title="@string/use_contacts_dictionary"
android:persistent="true"
android:defaultValue="false"
android:summaryOn="@string/use_contacts_dictionary_not_supported_summary"
android:summaryOff="@string/use_contacts_dictionary_not_supported_summary"
android:clickable="false" android:selectable="false" android:enabled="false"/>
While in API 5 'prefs_dictionaries.xml' I've enabled it with this XML layout:
<CheckBoxPreference
android:key="@string/settings_key_use_contacts_dictionary"
android:title="@string/use_contacts_dictionary"
android:persistent="true"
android:defaultValue="@bool/settings_default_contacts_dictionary"
android:summaryOn="@string/use_contacts_dictionary_on_summary"
android:summaryOff="@string/use_contacts_dictionary_off_summary"/>
Keyboard view
The Keyboard View is the main UI and input object in AnySoftKeyboard: in some Android versions it should provide multitouch capabilities, in some should not, in some it should query the motion events in one way, in others in a different way.
To get this done, I've used a nifty trick: I use the Android's resources tree decision mechanism (the same mechanism I used in the dictionary settings layout inflation) to create an object which encapsulate the required behavior. The difference in behavior are:
- Multitouch capability detection.
- MotionEvent query functions.
- and GestureDetector creation.
Here is a simple class diagram to explain what I mean:
My resources tree looks like this:
The device_specific.xml file inflates the required FactoryView, which in turn used to create the concrete DeviceSpecific instance.
This inflation is done in the application onCreate method, which ensures that the DeviceSpecific instance exists before anything requires it.
Upon request for a layout inflation, Android will search for the most suitable layout, e.g., if I'm running under Android 1.5, which is API level 3, Android will first look for the requested layout under the folder 'layout-v3', and if it finds it there, it will load it, if not, it will search for it under 'layout'. If I'm running Android 2.2, it will search for the layout under the folder 'layout-v8', then 'layout-v7' and so on, till the default folder.
Each of those layout files points to a different class, e.g., 'layout-v8/device_specific.xml' points to 'com.anysoftkeyboard.devicespecific.FactoryView_V8':
<com.anysoftkeyboard.devicespecific.FactoryView_V8
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/DeviceSpecific"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
FactoryView_V8 instantize DeviceSpecific_V8 which includes code conforms to API level 8. What did I achieve from this? The ability to use higher API level MotionEvent methods, and query functions! The higher the level, the more functionality I can use. In v3 I can only get X,Y and event type (up, down, move, etc) while in v8 I can also get pointer ID, better multitouch capability detection and more.
Note that I haven't used ANY Java's reflection methods, or lazy loading technics (which are implementation specific!). I find my method to be much more elegant.
Tap sound
In AnySoftKeyboard, the user can select enabling click sound upon pressing a key (this option is off by default). Upon a key press, the keyboard will take the sound level of the system, and use it to produce the click sound.
In pre-Eclair times, the sound leel was [0..8], while in post-Eclair it is [0..1]. To support this API change, I query the OS for its API Level, and performs the necessary calculation according to the API level:
//pre-eclair
// volume is between 0..8 (float)
//eclair
// volume is between 0..1 (float)
if (Workarounds.getApiLevel() >= 5)
{
fxVolume = ((float)volume)/((float)maxVolume);
}
else
{
fxVolume = 8*((float)volume)/((float)maxVolume);
}
That's it.
Conclusion
By leveraging on Android's API Level specific resource mechanism, I was able to easily provide a keyboard for Android devices running 1.5 up to 2.2 or higher with no ugly reflection code (which may break), and no lazy loading technics (which may not work in some optimized Java implementations), and still use advance system functions as the OS provides.
You can always see the source code of AnySoftKeyboard here.
Notes
- Android 1.5 and 1.6 only match resources with this qualifier when it exactly matches the platform version. This is a known issue.
- Providing density specific resources (i.e., drawable-hdpi) is not supported in Android 1.5, so make sure you provide the default too (i.e., drawable). The default resources are considered as mdpi, normal qualifiers.
Good luck.