I have the same trouble, and the UnsatisfiedLinkErrors comes on all versions of Android - over the past 6 months, for an app that currently has over 90000 active installs, I had:
Android 4.2 36 57.1%
Android 4.1 11 17.5%
Android 4.3 8 12.7%
Android 2.3.x 6 9.5%
Android 4.4 1 1.6%
Android 4.0.x 1 1.6%
and the users report that it usually happens just after the app update. This is for an app that gets around 200 - 500 new users per day.
I think I came up with a simpler work-around. I can find out where is the original apk of my app with this simple call:
String apkFileName = context.getApplicationInfo().sourceDir;
this returns something like "/data/app/com.example.pkgname-3.apk", the exact file name of my app's APK file. This file is a regular ZIP file and it is readable without root. Therefore, if I catch the java.lang.UnsatisfiedLinkError, I can extract and copy my native library, from the inside of .apk (zip) lib/armeabi-v7a folder (or whatever architecture I'm on), to any directory where I can read/write/execute, and load it with System.load(full_path).
Edit: It seems to work
Update July 1, 2014 since releasing a version of my product with the code similar to the listed below, on June 23, 2014, did not have any Unsatisfied Link Errors from my native library.
Here is the code I used:
public static void initNativeLib(Context context) {
try {
// Try loading our native lib, see if it works...
System.loadLibrary("MyNativeLibName");
} catch (UnsatisfiedLinkError er) {
ApplicationInfo appInfo = context.getApplicationInfo();
String libName = "libMyNativeLibName.so";
String destPath = context.getFilesDir().toString();
try {
String soName = destPath + File.separator + libName;
new File(soName).delete();
UnzipUtil.extractFile(appInfo.sourceDir, "lib/" + Build.CPU_ABI + "/" + libName, destPath);
System.load(soName);
} catch (IOException e) {
// extractFile to app files dir did not work. Not enough space? Try elsewhere...
destPath = context.getExternalCacheDir().toString();
// Note: location on external memory is not secure, everyone can read/write it...
// However we extract from a "secure" place (our apk) and instantly load it,
// on each start of the app, this should make it safer.
String soName = destPath + File.separator + libName;
new File(soName).delete(); // this copy could be old, or altered by an attack
try {
UnzipUtil.extractFile(appInfo.sourceDir, "lib/" + Build.CPU_ABI + "/" + libName, destPath);
System.load(soName);
} catch (IOException e2) {
Log.e(TAG "Exception in InstallInfo.init(): " + e);
e.printStackTrace();
}
}
}
}
Unfortunately, if a bad app update leaves an old version of the native library, or a copy somehow damaged, which we loaded with System.loadLibrary("MyNativeLibName"), there is no way to unload it. Upon finding out about such remnant defunct library lingering in the standard app native lib folder, e.g. by calling one of our native methods and finding out it's not there (UnsatisfiedLinkError again), we could store a preference to avoid calling the standard System.loadLibrary() altogether and relying on our own extraction and loading code upon next app startups.
For completeness, here is UnzipUtil class, that I copied and modified from this CodeJava UnzipUtility article:
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class UnzipUtil {
/**
* Size of the buffer to read/write data
*/
private static final int BUFFER_SIZE = 4096;
/**
* Extracts a zip file specified by the zipFilePath to a directory specified by
* destDirectory (will be created if does not exists)
* @param zipFilePath
* @param destDirectory
* @throws java.io.IOException
*/
public static void unzip(String zipFilePath, String destDirectory) throws IOException {
File destDir = new File(destDirectory);
if (!destDir.exists()) {
destDir.mkdir();
}
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
String filePath = destDirectory + File.separator + entry.getName();
if (!entry.isDirectory()) {
// if the entry is a file, extracts it
extractFile(zipIn, filePath);
} else {
// if the entry is a directory, make the directory
File dir = new File(filePath);
dir.mkdir();
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.close();
}
/**
* Extracts a file from a zip to specified destination directory.
* The path of the file inside the zip is discarded, the file is
* copied directly to the destDirectory.
* @param zipFilePath - path and file name of a zip file
* @param inZipFilePath - path and file name inside the zip
* @param destDirectory - directory to which the file from zip should be extracted, the path part is discarded.
* @throws java.io.IOException
*/
public static void extractFile(String zipFilePath, String inZipFilePath, String destDirectory) throws IOException {
ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
ZipEntry entry = zipIn.getNextEntry();
// iterates over entries in the zip file
while (entry != null) {
if (!entry.isDirectory() && inZipFilePath.equals(entry.getName())) {
String filePath = entry.getName();
int separatorIndex = filePath.lastIndexOf(File.separator);
if (separatorIndex > -1)
filePath = filePath.substring(separatorIndex + 1, filePath.length());
filePath = destDirectory + File.separator + filePath;
extractFile(zipIn, filePath);
break;
}
zipIn.closeEntry();
entry = zipIn.getNextEntry();
}
zipIn.close();
}
/**
* Extracts a zip entry (file entry)
* @param zipIn
* @param filePath
* @throws java.io.IOException
*/
private static void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
byte[] bytesIn = new byte[BUFFER_SIZE];
int read = 0;
while ((read = zipIn.read(bytesIn)) != -1) {
bos.write(bytesIn, 0, read);
}
bos.close();
}
}
Greg