You can ask any mobile developer: the major disadvantage of developing on Android is the number of different devices you have to support. You have to handle different sizes of screen, different hardware, and a large range of Android versions evolving so fast that ensuring backward compatibility is close to impossible. And if that was not enough, you sometimes have to deal with bugs… in Android itself. The latest release, Ice Cream Sandwich (ICS), was not an exception to this rule, and did not fail to surprise us with a quite tricky and unpleasant bug.
Before going on to the bug in itself, let us give a little context on how it happened. We designed the Moodstocks SDK as a C library and we ported it on iOS then Android thanks to the Android Native Development Kit (NDK). For performance reasons, we decided to support only ARMv7 devices (non-ARMv7 devices being mainly older devices), and cross-compiled our library so that the non-ARMv7 version would only return errors to inform the user that his/her device was not compatible.
Bug
Everything went perfectly well until this terrible day of March 2012, when one of our developers informed us that his brand new Samsung Galaxy Nexus running ICS told him that it was not compatible with our SDK. Even worse: after a few tests, he determined that it worked like a charm if he did not bundle too many resources with his app (animation xml’s, graphical resources, etc…), and suddenly decided to become incompatible if he bundled more than 8 resources. Yes, you heard me right: apparently, one of the latest and most powerful smartphones available, running the latest version of Android on its dual-core 1.5GHz ARMv7 processor, chose the most absurd and unrelated reason to start thinking of itself as a 3-year-old ARMv6 device.
Let’s first give a little example of what theoretically happens when you cross-compile a native library for both these architectures and run it on an Android device. Using the NDK, let’s build a simple C library containing this function, cross-compiled for ARMv7 and non-ARMv7 architectures:
This will result in two files: /lib/armeabi-v7a/libfoo.so compiled for ARMv7 devices, and /lib/armeabi/libfoo.so for non-ARMv7 devices. Let’s write the corresponding Java class:
When you call this class in Java, the System.loadLibrary function will check your device, and decide which of the two libraries it will load and use at runtime. And, as expected, this function will return 1 on any ARMv7 device, and 0 otherwise.
Workaround
That was for theory. Because in practice, and as explained in this thread, ICS developers accidentally let this functionality go rogue on Android 4.0.1 – 4.0.3: when crawling the application’s apk file looking for the right version of the library to use, ICS “forgets” that it found an ARMv7 version and choses the non-ARMv7 version instead! Luckily for us, they provide this quite ugly but useful tip:
“ensure that the armeabi-v7a [i.e. ARMv7] binaries are packaged _after_ the armeabi [i.e. non-ARMv7] ones in the final .apk. This is not trivial, but one way to do it is remove the armeabi-v7a files from the package, then add them back, manually.”
OK, it looks quite annoying, but at least it shows some coherency with the fact that adding resources could mess up with our SDK, as adding files in an archive does not necessarily preserve the files order. We thus started testing this workaround: after all, the Android SDK contains a small tool called aapt made especially to manipulate apk files. Let’s try what is suggested:
See the problem here? The file was added back, but not within the right folder. Let’s have a look at the interesting lines in aapt’s documentation:
As you can see, we did not use the -k option. Believe me or not, but the tool provided by Android is bugged too! Never mind… after all, an apk file is nothing more than a zip file, so why not use the usual zip tool? I’ll spare you the details, but applying the same method using zip and checking the files order using the following small python script, we realized that the suggested workaround simply did not work.
Working around the workaround
At this point, we had spent hours trying to work around a bug in Android, using a bugged Android tool, with a method that did not work. But all hope was not lost. After hiding under my desk to cry for a good 10 minutes a good coffee, we decided to try a more brutal method – and i don’t mean smashing the phone with a sledgehammer, however tempting it seemed at the moment.
So, as the problem comes from the fact that this System.loadLibrary function cannot be trusted to choose between the ARMv7 and non-ARMv7 libraries, we’ll simply do it ourselves. The problem divided in two main parts:
- System.loadLibrary can’t be trusted to choose between two libraries with the same name, even if they are placed in explicitly named directories. By renaming them to /lib/armeabi/libfoo-core.so and /lib/armeabi-v7a/libfoo-core-v7a.so, we would simply call System.loadLibrary(“foo-core-v7a”) if we were on an ARMv7 architecture, and System.loadLibrary(“foo-core”) otherwise.
- There is no direct way in Java to know if the device you’re using is ARMv7 or not: we had to create another native library that would be in charge of this choice, cross compiled for ARMv7 and non-ARMv7 architectures, and simply named libfoo.so. A good piece of code being worth a thousand words, here is an example of source code for this libfoo.so:
And the corresponding Java class simply becomes:
This way, the first loaded library makes a native isARMv7() function available, and this function is used to decide which core library must be loaded immediately after.
In the end, this workaround happens to be far more reliable and viable in the long term. Even though Google promises that the bug will be fixed in the next release of ICS, experience shows that many users don’t update their device, or update them months after the release, which will lead to thousands and thousands of “corrupted” devices that, despite this bug, we want to support. This trick has the advantage of being far more practical and sure than tinkling with our apk files hoping that it magically fixes a bug, and we’ll be able to keep it in place for a long time without concern.

Posted on 20,Mar |
Posted by Maxime Brenon 






Comments
By In the News: 2012-03-20 | Klaus' Korner  
456 days ago
[...] Programming News: Ice Cream Sandwich: why native code support sucksYou can ask any mobile developer: the major disadvantage of developing on Android is the number of different devices you have to support. You have to handle different sizes of screen, different hardware, and a large range of Android versions evolving so fast that ensuring backward compatibility is close to impossible. Read full story => Moodstocks [...]
By Fabian  
433 days ago
Your JNI code returns “false” on my Xoom, even though Xoom supports ARMv7
By Maxime Brenon  
433 days ago
You’re right, this code does a little more than checking if the device supports ARMv7: it also checks that it features the NEON instruction set (see “ANDROID_CPU_ARM_FEATURE_NEON” flag). It happens that the Xoom uses an Nvidia Tegra 2 processor, which supports ARMv7 but not the NEON instruction set, which explains the result.
You can just disable the test on this flag to check only if the device supports ARMv7 or not.
By Fabian  
433 days ago
Yup, thats what we do now. The function name was a bit misleading at first. Thanks anyway for your article, it helped us a lot!
By Alex Cohn  
429 days ago
The nasty effect of the bug is that the /data/data//lib directory is now twice as large. Well, we have separate so’s for neon and Tegra, which results in three times as large.
We did not bundle yet another native library to call cpufeateres, though. We perform the test in java, following the example at http://www.roman10.net/how-to-get-cpu-information-on-android.
It’s a pity Google did not include cpu-features into android SDK, where they have so much system information anyway.
By Jay Freeman (saurik)  
404 days ago
Hello. I’m the guy who did the writeup on Stack Overflow you linked to about why not passing -k to aapt was futile, due to a bug in that tool.
If you are at all curious, I looked into this issue today (having randomly found your blog while searching for details on an unrelated issue I’ve been experiencing), and wanted to look into why the people on the mailing list were wrong in their belief that reordering the entries of the zip file would fix this bug.
The code in com_android_internal_content_NativeLibraryHelper.cpp does, in fact, iterate over zip file entries, and takes the first one (it is not, for example, just looking through twice, once for each platform):
ZipFileRO zipFile;
…
for (int i = 0; i < N; i++) {
const ZipEntryRO entry = zipFile.findEntryByIndex(i);
…
if (cpuAbi.size() == cpuAbiRegionSize && …) {
…
} else if (cpuAbi2.size() == cpuAbiRegionSize && …) {
…
}
However, when you look at the code for ZipFileRO, you find that the array that when it parsed the zip file, it added all of the entries to a hashtable by name; in order to not keep a second array of all of the entries in their original order, and under the assumption that the order does not matter, they then iterate the sparse hash table (which requires them to skip the empty cells).
ZipEntryRO ZipFileRO::findEntryByIndex(int idx) const {
…
for (int ent = 0; ent < mHashTableSize; ent++) {
if (mHashTable[ent].name != NULL) {
if (idx– == 0)
return (ZipEntryRO) (ent + kZipEntryAdj);
So, when you reorder the two entries in the zip file, it is actually quite likely that they will just end up in the same spots they would have previously ended up in the hashtable anyway (as the hashtable is over-allocated, and only with conflicts will it fall into any kind of altered behavior while finding replacement slots, and even that behavior is going to be localized as their conflict resolution algorithm is "find the next slot").
I hope this provides you at least a little closure, knowing that despite the recommendations from the people on the mailing list the workaround that was provided simply had no hope of reliably working, despite the fact that it very well might have worked for some other people testing it, entirely by chance, and that when they go to add a couple more files to their .zip file and the hashtable's size changes, they will be just as screwed as you were before finding this much more sane and reliable solution to your problems.
By Jay Freeman (saurik)  
404 days ago
“I looked into this issue today” -> “I looked into the JNI selection issue you describe”
“you find that the array that when it parsed the” -> “you find that when it parsed the”
By Ben  
183 days ago
What about parsing /proc/cpuinfo ?. I think it’s readable if you have . And that will let you choose the correct library file purely in java.
By Maxime Brenon  
181 days ago
Hi Ben,
Yes, parsing /proc/cpuinfo would work too, and can indeed be done in pure Java. Nevertheless, it requires to write your own parser, while cpufeatures in the NDK provides this information directly. As we already had much native code, we decided to do that in native code too!