// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
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);
}
}
}