Android targeting system

Estimated read time: 6 min

Originally published on August 2nd, 2012 (Last updated on July 3rd, 2020)

I recent­ly start­ed sup­port­ing API Lev­el 9 in my cur­rent project, which brought a prob­lem with the Action­Bar along. I had used it exten­sive­ly in devel­op­ment (which was tar­get­ing post-Hon­ey­comb devices), but it’s only avail­able on API Lev­el 11 and later.

Long sto­ry short, I start­ed using Action­BarSh­er­lock to get the Action­Bar work­ing on all devices and dis­cov­ered an inter­est­ing fact about the Android build­ing sys­tem and it’s tar­get­ing mechanism.

Tar­get­ing in Android #

When com­pil­ing a Java appli­ca­tion with a Java 7 com­pil­er, you can’t use it with a Java 6 inter­preter. The inter­preter will tell you, that it can’t inter­pret the pro­duced byte-code, even if you’re not using any Java 7 lan­guage fea­tures. If you want to com­pile with the lat­est com­pil­er but make your byte-code exe­cutable on old­er JVM ver­sions, you’ll need to tell the com­pil­er to do so (using the -target-flag).

In Android, you can declare what plat­form-ver­sions you sup­port in your man­i­fest-file, using the <uses-sdk>-ele­ment and it’s android:minSdkVersion and android:targetSdkVersion-attrib­ut­es. The dif­fer­ence between those tar­get­ing mech­a­nisms” is, that Android does not care against which plat­form ver­sion the appli­ca­tion was compiled.

If you declare your appli­ca­tion to be com­pat­i­ble with API Lev­el 4, Android will hap­pi­ly install it, even if you com­piled it against Android 4.1 (API Lev­el 16).

Pro #

This allows your appli­ca­tion to use new API calls on new­er plat­form, but still ful­ly sup­port old­er Android ver­sions. It is pos­si­ble to check the Android ver­sion and decide what fea­tures to use at run­time (as described fur­ther below).

This way, you can make use of new­er func­tion­al­i­ty on Android devices with high­er API Lev­els and use avail­able fall­back func­tion­al­i­ty on devices with low­er API Levels.

Con­tra #

But the big prob­lem with this sys­tem is, that you loose a huge amount of com­pile-time security.

When declar­ing your minSdkVersion, you effec­tive­ly promise, that your App will run on this ver­sion or high­er. The thing with promis­es is, they’re easy to break.

If you declare your min­i­mum SDK ver­sion to be API Lev­el 6 and you use methods/​classes which where added in API Lev­el 11, the com­pil­er will not com­plain at com­pile-time. But your App will crash at exe­cu­tion-time.

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

Android Lint #

The tool web­site gives the fol­low­ing sum­ma­ry about it’s functionality:

Android Lint is a new tool intro­duced in ADT 16 (and Tools 16) which scans Android project sources for poten­tial bugs (which can not be found at com­pile-time). It is avail­able both as a com­mand line tool, as well as inte­grat­ed with Eclipse, and IntelliJ. […]

As men­tioned above, when build­ing against a new­er ver­sion of the plat­form, there is no way for the com­pil­er to know on what ver­sions the appli­ca­tion needs to run. This is some­thing the Lin­ter can check for you.

Intel­liJ problems #

This sec­tion is heav­i­ly out­dat­ed! It’s retained only for cohesion.

In the quote above, the Intel­liJ inte­gra­tion is men­tioned, although in it’s cur­rent state (Intel­liJ 11.1.3), it’s not real­ly worth mentioning.

The Intel­liJ set­tings include just a few of the many many checks that the Lin­ter can per­form. My par­tic­u­lar prob­lem is, that the NewApi-check (which we’ll fur­ther dis­cuss in just a minute) is not includ­ed at all.

Also, when man­u­al­ly per­form­ing any Lint-check­ing on an Intel­liJ project, there is a sec­ond problem.

Check­ing Intel­liJ projects on the com­mand line #

When try­ing to use the CLI ver­sion of Lint, this happens:

$ 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 prob­lem here is, that the Lin­ter search­es a par­tic­u­lar fold­er for the class­files (See issue IDEA-88701), which is bin/classes (the stan­dard in Eclipse). The stan­dard Intel­liJ out­put fold­er is out/production/MyProject.

An easy workaround for this prob­lem is to make a sym­link from the Intel­liJ out­put fold­er to the bin/-fold­er. Anoth­er option is to change the out­put-fold­er of Intel­liJ under File -> Project Struc­ture -> Project -> Project Com­pil­er Output”.

Check­ing for com­pat­i­bil­i­ty problems #

The Lin­ter can check for many things. To speed the whole thing up, you can tell it which par­tic­u­lar checks it shall per­form on your project. For the sake of this arti­cle, the NewApi” check is of the biggest interest:

Sum­ma­ry: Finds API access­es to APIs that are not sup­port­ed in all tar­get­ed API versions

[…]

This check scans through all the Android API calls in the appli­ca­tion and warns about any calls that are not avail­able on all ver­sions tar­get­ed by this appli­ca­tion (accord­ing to its min­i­mum SDK attribute in the manifest).

To run this sin­gle check on your project, use the com­man­d­line tool:

$ lint --check NewApi BikeTrack/
Scanning BikeTrack: .......................................................................
No issues found.

This time, the Lin­ter did not find any­thing, but if it finds some­thing, it gives you plen­ty of information:

$ 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 rea­son why it is not sup­port­ed (and when it was intro­duced) in the giv­en error. 

You’ll want to run this check before deploy­ing your appli­ca­tion or after every major change. When using a CI, this should always be part of your build process.

Using fea­tures of new­er APIs #

So, if you unin­ten­tion­al­ly used an API call which is not sup­port­ed in every tar­get­ed plat­form, you’ll receive an error from the Lin­ter. But what if you want to inten­tion­al­ly use new­er APIs when they’re avail­able on the device that is run­ning the application?

Con­di­tion­al execution #

Con­sid­er the fol­low­ing situation:

There are two ways to imple­ment a fea­ture: The first is new and shiny but requires an API Lev­el high­er then the minSdkVersion. The oth­er is old and… well, not so shiny. Now, on a plat­form which has the need­ed API Lev­el, you want to use the new and shiny way, while on old­er devices, you want to fall back on the not-so-shiny” vari­a­tion. But how do you check if the need­ed APIs are avail­able at runtime?

For that pur­pose, there is the android.os.Build.VERSION-class and it’s SDK_INT-field. To check if a device is run­ning (for exam­ple) Hon­ey­comb or lat­er, 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 top­ic itself is fur­ther dis­cussed in this SO ques­tion.

Declar­ing inten­tion­al usage of new APIs #

So, now you can check if an API is avail­able and if so, use it. How­ev­er, the Lin­ter does not under­stand this check and will com­plain. To fix this, we’ll need to ensure the Lin­ter that we under­stand the pos­si­ble error sce­nario but have tak­en the nec­es­sary precautions:

If your code is delib­er­ate­ly access­ing new­er APIs, and you have ensured (e.g. with con­di­tion­al exe­cu­tion) that this code will only ever be called on a sup­port­ed plat­form, then you can anno­tate your class or method with the @TargetApi anno­ta­tion spec­i­fy­ing the local min­i­mum SDK to apply, such as @TargetApi(11), such that this check con­sid­ers 11 rather than your man­i­fest file’s min­i­mum SDK as the required API level.

So now, you’ll want to move all your post-Hon­ey­comb code into a method and anno­tate it:

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

// ... Further down

@TargetApi(11)
private void doHoneycombStuff(){
    // Use API Level 11 functionality here...
}

After that, the Lin­ter won’t com­plain about your code in the doHoneycombStuff()-method and the run­ning Android device will exe­cute the code, depend­ing on it’s cur­rent platform.

An exam­ple on how I used this to get con­tex­tu­al menus to work with the native Action­Bar (on post-Hon­ey­comb) or the clas­sic con­text-menu can be found in this com­mit: Bike­Track — 3b60c31d85

Con­clu­sion #

  • If you want your appli­ca­tion to work with the newest Android plat­form, build against it.
  • Use minSdkVersion to declare the low­est API Lev­el which is sup­port­ed by your application.
  • As your targetSdkVersion, use the API Lev­el against which you com­piled the application.
  • Use Lint to check for (pos­si­bly) unsup­port­ed API calls.
  • Use con­di­tion­al exe­cu­tion and the @TargetApi-anno­ta­tion to use new­er APIs when available.

Posted by Lukas Knuth

Comments

No com­ment sec­tion here 😄

You can reach me over at @knuth_dev or send me an Email.