Android targeting system

August 02, 2012 »know-how

I recently started supporting API Level 9 in my current project, which brought a problem with the ActionBar along. I had used it extensively in the previous development (which was targeting post-Honeycomb devices), but it’s only available on API Level 11 and later.

Long story short, I started using ActionBarSherlock to get the ActionBar working on all devices and discovered an interesting fact about the Android building system and it’s targeting mechanics.

Targeting in Android

When compiling a Java application with a Java 7 compiler, you can’t use it with a Java 6 interpreter. The interpreter will tell you, that it can’t interpret the produced byte-code, even thought you’re not using any Java 7 language features. If you want to compile with the latest compiler but make your byte-code runnable on older JVM instances, you’ll need to tell the compiler to do so (using the -target-flag).

In Android, you can declare what platform-versions you support in your manifest-file, using the <uses-sdk>-element and it’s android:minSdkVersion and android:targetSdkVersion-attributes. The difference between those “targeting mechanics” is, that Android does not care against which platform version the application was compiled.

If you declare your application to be compatible with API Level 4, Android will happily install it, even if you compiled it against Android 4.1 (API Level 16).

Pro

This allows your application to use new API calls on newer platform, but still fully support older Android versions. It is possible to check for the running Android version (at runtime) and decide what methods to use (described further down).

This way, you can make use of newer (more convenient) functions on Android devices with higher API Levels and use the older (available) functions on devices with lower API Levels.

Contra

But the big problem with this system is, that you loose a huge amount of compile-time security.

When declaring your minSdkVersion, you effectively promise, that it will run on this version or higher. The thing with promises is, that people tend to not keep them.

If you declare your minimum SDK version to be API Level 6 and you use methods/classes which where added in API Level 11, the compiler will not complain at compile-time. But your Android device/emulator will complain at execution-time.

This is, where the Android Lint tool comes in handy.

Android Lint

The tool website gives the following summary about it’s functionality:

Android Lint is a new tool introduced in ADT 16 (and Tools 16) which scans Android project sources for potential bugs (which can not be found at compile-time). It is available both as a command line tool, as well as integrated with Eclipse, and IntelliJ. […]

As mentioned above, when building against a newer version of the platform, there is no way for the compiler to know on what versions the application needs to run. This is one thing that Lint can check for you.

IntelliJ problems

In the quote above, the IntelliJ integration is mentioned, although in it’s current state (IntelliJ 11.1.3), it’s not really worth mentioning.

The IntelliJ settings include just a few of the many many checks that Lint can perform. My particular problem is, that the NewApi-check (which we’ll further discuss in just a minute) is not included at all.

Also, when manually performing any Lint-checking on an IntelliJ project, there is a second problem.

Checking IntelliJ projects on the command line

When trying to use the commandline version of Lint, the following problem presents itself:

[luke@KeksDose dev]$ lint BikeTrack/
Scanning BikeTrack: ....................
BikeTrack: Error: No .class files were found in project "BikeTrack", so none of the classfile based checks could be run. Does the project need to be built first? [LintError]

The problem here is, that Lint searches a particular folder for the classfiles (See issue “IDEA-88701”), which is bin/classes (the standard in Eclipse). The standard IntelliJ output folder is out/production/MyProject.

An easy workaround for this problem is to make a symlink from the IntelliJ output folder to the bin/-folder. Another option is to change the output-folder of IntelliJ under “File -> Project Structure -> Project -> Project Compiler Output”.

Checking for compatibility problems

Lint can check for many things. But to speed the whole thing up, you can tell it which particular checks it shall perform on your project. For us (and the sake of this writing), the “NewApi” check is of the biggest interest:

Summary: Finds API accesses to APIs that are not supported in all targeted API versions

[…]

This check scans through all the Android API calls in the application and warns about any calls that are not available on all versions targeted by this application (according to its minimum SDK attribute in the manifest).

To run this single check on your project, use the commandline tool:

[luke@KeksDose dev]$ lint --check NewApi BikeTrack/
Scanning BikeTrack: .......................................................................
No issues found.

This time, the Lint tool did not find anything, but if it finds something, it gives you plenty of information:

[luke@KeksDose dev]$ lint --check NewApi BikeTrack/
Scanning BikeTrack: .......................................................................
src/org/knuth/biketrack/Main.java:246: Error: Call requires API level 11 (current min is 9): android.widget.ArrayAdapter#addAll [NewApi]
            tour_adapter.addAll(tours);
                         ^
1 errors, 0 warnings

You can see the source file, the line, the called method and the reason why it is not supported (and when it was introduced) in the given error. This makes it easy to check for such things.

You’ll want to run this check before deploying your application or after every major change. When using a build system or script, you might want to include it after compiling your project.

Using features of newer APIs

So, if you unintentionally used an API call which is not supported in every targeted platform, you’ll receive an error from Lint. But what if you want to intentionally use newer APIs when they’re available on the device that is running the application?

Conditional execution

Consider the following situation:

There are two ways to implement a feature: The first is new and shiny but requires an API Level higher then the minSdkVersion. The other is old and… well, not so shiny. Now, on a platform which has the needed API Level, you want to use the new and shiny way, while on older devices, you want to fall back on the “not-so-shiny” variation. But how do you check if the needed APIs are available at runtime?

For that purpose, there is the android.os.Build.VERSION-class and it’s SDK_INT-field. To check if a device is running (for example) Honeycomb or later, you can use this code:

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
  // call something for API Level 11+
} else {
  // use something available before
}

The above code and the topic itself is further discussed in this SO question.

Declaring intentional usage of new APIs

So, now you can check if an API is available and if so, use it. But, when running Lint over that code, it will still complain, even thought you checked first and it’s impossible that there will ever by a problem with that. So, you’ll want to tell it that you’re using those APIs intentionally.

Again quoting from the description of the “NewApi”-check:

If your code is deliberately accessing newer APIs, and you have ensured (e.g. with conditional execution) that this code will only ever be called on a supported platform, then you can annotate your class or method with the @TargetApi annotation specifying the local minimum SDK to apply, such as @TargetApi(11), such that this check considers 11 rather than your manifest file’s minimum SDK as the required API level.

So now, you’ll want to “outsource” all your post-Honeycomb code into a method and annotate it:

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
    doHoneycombStuff();
}

// ... Further down

@TargetApi(11)
private void doHoneycombStuff(){
    // Do your stuff...
}

After that, Lint won’t complain about your code in the doHoneycombStuff()-method and the running Android device will execute the code, depending on it’s current platform.

An example on how I used this to get contextual menus to work with the native ActionBar (on post-Honeycomb) or the classic context-menu can be found in this commit: BikeTrack - 3b60c31d85

Conclusion

  • If you want your application to work with the newest Android platform, build against it.
  • Use minSdkVersion to declare the lowest API Level which is supported by your application.
  • As your targetSdkVersion, use the API Level against which you compiled the application.
  • Use Lint to check for (possibly) unsupported API calls.
  • Use conditional execution and the @TargetApi-annotation to use newer APIs when available.

Posted by Lukas Knuth

Comments

comments powered by Disqus