Last active
August 11, 2021 08:10
-
-
Save NLMartian/2181119eee015c16a2857449a81d60f4 to your computer and use it in GitHub Desktop.
The class provides helper functions to extract native libraries from APK, and load libraries from there.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.chromium.base.library_loader; | |
import android.content.Context; | |
import android.content.pm.ApplicationInfo; | |
import android.os.Build; | |
import android.util.Log; | |
import java.io.File; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.util.zip.ZipEntry; | |
import java.util.zip.ZipFile; | |
/** | |
* The class provides helper functions to extract native libraries from APK, | |
* and load libraries from there. | |
* | |
* The class should be package-visible only, but made public for testing | |
* purpose. | |
*/ | |
public class LibraryLoaderHelper { | |
private static final String TAG = "LibraryLoaderHelper"; | |
private static final String LIB_DIR = "lib"; | |
/** | |
* One-way switch becomes true if native libraries were unpacked | |
* from APK. | |
*/ | |
private static boolean sLibrariesWereUnpacked = false; | |
/** | |
* Loads native libraries using workaround only, skip the library in system | |
* lib path. The method exists only for testing purpose. | |
* Caller must ensure thread safety of this method. | |
* @param context | |
*/ | |
public static boolean loadNativeLibrariesUsingWorkaroundForTesting(Context context) { | |
// Although tryLoadLibraryUsingWorkaround might be called multiple times, | |
// libraries should only be unpacked once, this is guaranteed by | |
// sLibrariesWereUnpacked. | |
for (String library : NativeLibraries.LIBRARIES) { | |
if (!tryLoadLibraryUsingWorkaround(context, library)) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/** | |
* Try to load a native library using a workaround of | |
* http://b/13216167. | |
* | |
* Workaround for b/13216167 was adapted from code in | |
* https://googleplex-android-review.git.corp.google.com/#/c/433061 | |
* | |
* More details about http://b/13216167: | |
* PackageManager may fail to update shared library. | |
* | |
* Native library directory in an updated package is a symbolic link | |
* to a directory in /data/app-lib/<package name>, for example: | |
* /data/data/com.android.chrome/lib -> /data/app-lib/com.android.chrome[-1]. | |
* When updating the application, the PackageManager create a new directory, | |
* e.g., /data/app-lib/com.android.chrome-2, and remove the old symlink and | |
* recreate one to the new directory. However, on some devices (e.g. Sony Xperia), | |
* the symlink was updated, but fails to extract new native libraries from | |
* the new apk. | |
* We make the following changes to alleviate the issue: | |
* 1) name the native library with apk version code, e.g., | |
* libchrome.1750.136.so, 1750.136 is Chrome version number; | |
* 2) first try to load the library using System.loadLibrary, | |
* if that failed due to the library file was not found, | |
* search the named library in a /data/data/com.android.chrome/app_lib | |
* directory. Because of change 1), each version has a different native | |
* library name, so avoid mistakenly using the old native library. | |
* | |
* If named library is not in /data/data/com.android.chrome/app_lib directory, | |
* extract native libraries from apk and cache in the directory. | |
* | |
* This function doesn't throw UnsatisfiedLinkError, the caller needs to | |
* check the return value. | |
*/ | |
static boolean tryLoadLibraryUsingWorkaround(Context context, String library) { | |
assert context != null; | |
File libFile = getWorkaroundLibFile(context, library); | |
if (!libFile.exists() && !unpackLibrariesOnce(context)) { | |
return false; | |
} | |
try { | |
System.load(libFile.getAbsolutePath()); | |
return true; | |
} catch (UnsatisfiedLinkError e) { | |
return false; | |
} | |
} | |
/** | |
* Returns the directory for holding extracted native libraries. | |
* It may create the directory if it doesn't exist. | |
* | |
* @param context | |
* @return the directory file object | |
*/ | |
public static File getWorkaroundLibDir(Context context) { | |
return context.getDir(LIB_DIR, Context.MODE_PRIVATE); | |
} | |
private static File getWorkaroundLibFile(Context context, String library) { | |
String libName = System.mapLibraryName(library); | |
return new File(getWorkaroundLibDir(context), libName); | |
} | |
/** | |
* Unpack native libraries from the APK file. The method is supposed to | |
* be called only once. It deletes existing files in unpacked directory | |
* before unpacking. | |
* | |
* @param context | |
* @return true when unpacking was successful, false when failed or called | |
* more than once. | |
*/ | |
private static boolean unpackLibrariesOnce(Context context) { | |
if (sLibrariesWereUnpacked) { | |
return false; | |
} | |
sLibrariesWereUnpacked = true; | |
File libDir = getWorkaroundLibDir(context); | |
if (libDir.exists()) { | |
assert libDir.isDirectory(); | |
deleteDirectorySync(libDir); | |
} | |
try { | |
ApplicationInfo appInfo = context.getApplicationInfo(); | |
ZipFile file = new ZipFile(new File(appInfo.sourceDir), ZipFile.OPEN_READ); | |
for (String libName : NativeLibraries.LIBRARIES) { | |
String jniNameInApk = "lib/" + Build.CPU_ABI + "/" + | |
System.mapLibraryName(libName); | |
final ZipEntry entry = file.getEntry(jniNameInApk); | |
if (entry == null) { | |
Log.e(TAG, appInfo.sourceDir + " doesn't have file " + jniNameInApk); | |
file.close(); | |
deleteDirectorySync(libDir); | |
return false; | |
} | |
File outputFile = getWorkaroundLibFile(context, libName); | |
Log.i(TAG, "Extracting native libraries into " + outputFile.getAbsolutePath()); | |
assert !outputFile.exists(); | |
try { | |
if (!outputFile.createNewFile()) { | |
throw new IOException(); | |
} | |
InputStream is = null; | |
FileOutputStream os = null; | |
try { | |
is = file.getInputStream(entry); | |
os = new FileOutputStream(outputFile); | |
int count = 0; | |
byte[] buffer = new byte[16 * 1024]; | |
while ((count = is.read(buffer)) > 0) { | |
os.write(buffer, 0, count); | |
} | |
} finally { | |
try { | |
if (is != null) is.close(); | |
} finally { | |
if (os != null) os.close(); | |
} | |
} | |
// Change permission to rwxr-xr-x | |
outputFile.setReadable(true, false); | |
outputFile.setExecutable(true, false); | |
outputFile.setWritable(true); | |
} catch (IOException e) { | |
if (outputFile.exists()) { | |
if (!outputFile.delete()) { | |
Log.e(TAG, "Failed to delete " + outputFile.getAbsolutePath()); | |
} | |
} | |
file.close(); | |
throw e; | |
} | |
} | |
file.close(); | |
return true; | |
} catch (IOException e) { | |
Log.e(TAG, "Failed to unpack native libraries", e); | |
deleteDirectorySync(libDir); | |
return false; | |
} | |
} | |
/** | |
* Delete old library files in the backup directory. | |
* The actual deletion is done in a background thread. | |
* | |
* @param context | |
*/ | |
static void deleteWorkaroundLibrariesAsynchronously(Context context) { | |
// Child process should not reach here. | |
final File libDir = getWorkaroundLibDir(context); | |
if (libDir.exists()) { | |
assert libDir.isDirectory(); | |
// Async deletion | |
new Thread() { | |
@Override | |
public void run() { | |
deleteDirectorySync(libDir); | |
} | |
}.start(); | |
} | |
} | |
/** | |
* Delete the workaround libraries and directory synchronously. | |
* For testing purpose only. | |
* @param context | |
*/ | |
public static void deleteWorkaroundLibrariesSynchronously(Context context) { | |
File libDir = getWorkaroundLibDir(context); | |
if (libDir.exists()) { | |
deleteDirectorySync(libDir); | |
} | |
} | |
private static void deleteDirectorySync(File dir) { | |
try { | |
File[] files = dir.listFiles(); | |
if (files != null) { | |
for (File file : files) { | |
String fileName = file.getName(); | |
if (!file.delete()) { | |
Log.e(TAG, "Failed to remove " + file.getAbsolutePath()); | |
} | |
} | |
} | |
if (!dir.delete()) { | |
Log.w(TAG, "Failed to remove " + dir.getAbsolutePath()); | |
} | |
return; | |
} catch (Exception e) { | |
Log.e(TAG, "Failed to remove old libs, ", e); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment