Index: base/android/java/src/org/chromium/base/library_loader/LibraryLoaderHelper.java |
diff --git a/base/android/java/src/org/chromium/base/library_loader/LibraryLoaderHelper.java b/base/android/java/src/org/chromium/base/library_loader/LibraryLoaderHelper.java |
index 58198aed8c4874352e343cad7c6effa93d3e1681..c185fadbe5f4b00a558ed863ead1abfc56d09948 100644 |
--- a/base/android/java/src/org/chromium/base/library_loader/LibraryLoaderHelper.java |
+++ b/base/android/java/src/org/chromium/base/library_loader/LibraryLoaderHelper.java |
@@ -6,18 +6,42 @@ |
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.BufferedOutputStream; |
+import java.io.Closeable; |
import java.io.File; |
+import java.io.FileNotFoundException; |
import java.io.FileOutputStream; |
import java.io.IOException; |
import java.io.InputStream; |
+import java.io.OutputStream; |
+import java.util.Collection; |
+import java.util.Collections; |
+import java.util.HashMap; |
+import java.util.HashSet; |
+import java.util.Map; |
+import java.util.Map.Entry; |
+import java.util.Set; |
import java.util.zip.ZipEntry; |
+import java.util.zip.ZipException; |
import java.util.zip.ZipFile; |
/** |
+ * Class representing an exception which occured during the unpacking process. |
+ */ |
+class UnpackingException extends Exception { |
+ public UnpackingException(String message, Throwable cause) { |
+ super(message, cause); |
+ } |
+ |
+ public UnpackingException(String message) { |
+ super(message); |
+ } |
+} |
+ |
+/** |
* The class provides helper functions to extract native libraries from APK, |
* and load libraries from there. |
* |
@@ -27,7 +51,12 @@ import java.util.zip.ZipFile; |
public class LibraryLoaderHelper { |
private static final String TAG = "LibraryLoaderHelper"; |
- private static final String LIB_DIR = "lib"; |
+ // Workaround and fallback directories. |
+ // TODO(petrcermak): Merge the directories once refactored. |
+ public static final String PACKAGE_MANAGER_WORKAROUND_DIR = "lib"; |
+ public static final String LOAD_FROM_APK_FALLBACK_DIR = "fallback"; |
+ |
+ private static final int BUFFER_SIZE = 16384; |
/** |
* One-way switch becomes true if native libraries were unpacked |
@@ -71,7 +100,7 @@ public class LibraryLoaderHelper { |
* 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; |
@@ -89,8 +118,9 @@ public class LibraryLoaderHelper { |
*/ |
static boolean tryLoadLibraryUsingWorkaround(Context context, String library) { |
assert context != null; |
- File libFile = getWorkaroundLibFile(context, library); |
- if (!libFile.exists() && !unpackLibrariesOnce(context)) { |
+ String libName = System.mapLibraryName(library); |
+ File libFile = new File(getLibDir(context, PACKAGE_MANAGER_WORKAROUND_DIR), libName); |
+ if (!libFile.exists() && !unpackWorkaroundLibrariesOnce(context)) { |
return false; |
} |
try { |
@@ -105,16 +135,18 @@ public class LibraryLoaderHelper { |
* 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 |
+ * @param context The context the code is running. |
+ * @param dirName The name of the directory containing the libraries. |
+ * @return The directory file object. |
*/ |
- public static File getWorkaroundLibDir(Context context) { |
- return context.getDir(LIB_DIR, Context.MODE_PRIVATE); |
+ public static File getLibDir(Context context, String dirName) { |
+ return context.getDir(dirName, Context.MODE_PRIVATE); |
} |
- private static File getWorkaroundLibFile(Context context, String library) { |
- String libName = System.mapLibraryName(library); |
- return new File(getWorkaroundLibDir(context), libName); |
+ @SuppressWarnings("deprecation") |
+ private static String getJniNameInApk(String libName) { |
+ // TODO(aurimas): Build.CPU_ABI has been deprecated. Replace it when final L SDK is public. |
+ return "lib/" + Build.CPU_ABI + "/" + libName; |
} |
/** |
@@ -126,128 +158,306 @@ public class LibraryLoaderHelper { |
* @return true when unpacking was successful, false when failed or called |
* more than once. |
*/ |
- private static boolean unpackLibrariesOnce(Context context) { |
+ private static boolean unpackWorkaroundLibrariesOnce(Context context) { |
if (sLibrariesWereUnpacked) { |
return false; |
} |
sLibrariesWereUnpacked = true; |
- File libDir = getWorkaroundLibDir(context); |
- deleteDirectorySync(libDir); |
+ deleteLibrariesSynchronously(context, PACKAGE_MANAGER_WORKAROUND_DIR); |
+ File libDir = getLibDir(context, PACKAGE_MANAGER_WORKAROUND_DIR); |
try { |
- ApplicationInfo appInfo = context.getApplicationInfo(); |
- ZipFile file = new ZipFile(new File(appInfo.sourceDir), ZipFile.OPEN_READ); |
- for (String libName : NativeLibraries.LIBRARIES) { |
- String jniNameInApk = getJniNameInApk(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; |
- } |
+ Map<String, File> dstFiles = new HashMap<String, File>(); |
+ for (String library : NativeLibraries.LIBRARIES) { |
+ String libName = System.mapLibraryName(library); |
+ String pathInZipFile = getJniNameInApk(libName); |
+ dstFiles.put(pathInZipFile, new File(libDir, libName)); |
} |
- file.close(); |
+ unpackLibraries(context, dstFiles); |
return true; |
- } catch (IOException e) { |
+ } catch (UnpackingException e) { |
Log.e(TAG, "Failed to unpack native libraries", e); |
- deleteDirectorySync(libDir); |
+ deleteLibrariesSynchronously(context, PACKAGE_MANAGER_WORKAROUND_DIR); |
return false; |
} |
} |
/** |
- * Delete old library files in the backup directory. |
- * The actual deletion is done in a background thread. |
+ * Delete libraries and their directory synchronously. |
+ * For testing purpose only. |
+ * |
+ * @param context |
+ */ |
+ public static void deleteLibrariesSynchronously(Context context, String dirName) { |
+ File libDir = getLibDir(context, dirName); |
+ deleteObsoleteLibraries(libDir, Collections.<File>emptyList()); |
+ } |
+ |
+ /** |
+ * Delete libraries and their directory asynchronously. |
+ * The actual deletion is done in a background thread. |
* |
* @param context |
*/ |
- static void deleteWorkaroundLibrariesAsynchronously(final Context context) { |
+ static void deleteLibrariesAsynchronously( |
+ final Context context, final String dirName) { |
// Child process should not reach here. |
new Thread() { |
@Override |
public void run() { |
- deleteWorkaroundLibrariesSynchronously(context); |
+ deleteLibrariesSynchronously(context, dirName); |
} |
}.start(); |
} |
/** |
- * Delete the workaround libraries and directory synchronously. |
- * For testing purpose only. |
- * @param context |
+ * Copy a library from a zip file to the application's private directory. |
+ * This is used as a fallback when we are unable to load the library |
+ * directly from the APK file (crbug.com/390618). |
+ * |
+ * @param context The context the code is running in. |
+ * @param library Library name. |
+ * @return name of the fallback copy of the library. |
*/ |
- public static void deleteWorkaroundLibrariesSynchronously(Context context) { |
- File libDir = getWorkaroundLibDir(context); |
- deleteDirectorySync(libDir); |
- } |
+ public static String buildFallbackLibrary(Context context, String library) { |
+ try { |
+ String libName = System.mapLibraryName(library); |
+ File fallbackLibDir = getLibDir(context, LOAD_FROM_APK_FALLBACK_DIR); |
+ File fallbackLibFile = new File(fallbackLibDir, libName); |
+ String pathInZipFile = Linker.getLibraryFilePathInZipFile(libName); |
+ Map<String, File> dstFiles = Collections.singletonMap(pathInZipFile, fallbackLibFile); |
- @SuppressWarnings("deprecation") |
- private static String getJniNameInApk(String libName) { |
- // TODO(aurimas): Build.CPU_ABI has been deprecated. Replace it when final L SDK is public. |
- return "lib/" + Build.CPU_ABI + "/" + System.mapLibraryName(libName); |
+ deleteObsoleteLibraries(fallbackLibDir, dstFiles.values()); |
+ unpackLibraries(context, dstFiles); |
+ |
+ return fallbackLibFile.getAbsolutePath(); |
+ } catch (Exception e) { |
+ String errorMessage = "Unable to load fallback for library " + library |
+ + " (" + (e.getMessage() == null ? e.toString() : e.getMessage()) + ")"; |
+ Log.e(TAG, errorMessage, e); |
+ throw new UnsatisfiedLinkError(errorMessage); |
+ } |
} |
- private static void deleteDirectorySync(File dir) { |
+ // Delete obsolete libraries from a library folder. |
+ private static void deleteObsoleteLibraries(File libDir, Collection<File> keptFiles) { |
try { |
- File[] files = dir.listFiles(); |
+ // Build a list of libraries that should NOT be deleted. |
+ Set<String> keptFileNames = new HashSet<String>(); |
+ for (File k : keptFiles) { |
+ keptFileNames.add(k.getName()); |
+ } |
+ |
+ // Delete the obsolete libraries. |
+ Log.i(TAG, "Deleting obsolete libraries in " + libDir.getPath()); |
+ File[] files = libDir.listFiles(); |
if (files != null) { |
- for (File file : files) { |
- if (!file.delete()) { |
- Log.e(TAG, "Failed to remove " + file.getAbsolutePath()); |
+ for (File f : files) { |
+ if (!keptFileNames.contains(f.getName())) { |
+ delete(f); |
} |
} |
+ } else { |
+ Log.e(TAG, "Failed to list files in " + libDir.getPath()); |
} |
- if (!dir.delete()) { |
- Log.w(TAG, "Failed to remove " + dir.getAbsolutePath()); |
+ |
+ // Delete the folder if no libraries were kept. |
+ if (keptFileNames.isEmpty()) { |
+ delete(libDir); |
} |
- return; |
} catch (Exception e) { |
- Log.e(TAG, "Failed to remove old libs, ", e); |
+ Log.e(TAG, "Failed to remove obsolete libraries from " + libDir.getPath()); |
+ } |
+ } |
+ |
+ // Unpack libraries from a zip file to the file system. |
+ private static void unpackLibraries(Context context, |
+ Map<String, File> dstFiles) throws UnpackingException { |
+ String zipFilePath = context.getApplicationInfo().sourceDir; |
+ Log.i(TAG, "Opening zip file " + zipFilePath); |
+ File zipFile = new File(zipFilePath); |
+ ZipFile zipArchive = openZipFile(zipFile); |
+ |
+ try { |
+ for (Entry<String, File> d : dstFiles.entrySet()) { |
+ String pathInZipFile = d.getKey(); |
+ File dstFile = d.getValue(); |
+ Log.i(TAG, "Unpacking " + pathInZipFile |
+ + " to " + dstFile.getAbsolutePath()); |
+ ZipEntry packedLib = zipArchive.getEntry(pathInZipFile); |
+ |
+ if (needToUnpackLibrary(zipFile, packedLib, dstFile)) { |
+ unpackLibraryFromZipFile(zipArchive, packedLib, dstFile); |
+ setLibraryFilePermissions(dstFile); |
+ } |
+ } |
+ } finally { |
+ closeZipFile(zipArchive); |
+ } |
+ } |
+ |
+ // Open a zip file. |
+ private static ZipFile openZipFile(File zipFile) throws UnpackingException { |
+ try { |
+ return new ZipFile(zipFile); |
+ } catch (ZipException e) { |
+ throw new UnpackingException("Failed to open zip file " + zipFile.getPath()); |
+ } catch (IOException e) { |
+ throw new UnpackingException("Failed to open zip file " + zipFile.getPath()); |
+ } |
+ } |
+ |
+ // Determine whether it is necessary to unpack a library from a zip file. |
+ private static boolean needToUnpackLibrary( |
+ File zipFile, ZipEntry packedLib, File dstFile) { |
+ // Check if the fallback library already exists. |
+ if (!dstFile.exists()) { |
+ Log.i(TAG, "File " + dstFile.getPath() + " does not exist yet"); |
+ return true; |
+ } |
+ |
+ // Check last modification dates. |
+ long zipTime = zipFile.lastModified(); |
+ long fallbackLibTime = dstFile.lastModified(); |
+ if (zipTime > fallbackLibTime) { |
+ Log.i(TAG, "Not using existing fallback file because " |
+ + "the APK file " + zipFile.getPath() |
+ + " (timestamp=" + zipTime + ") is newer than " |
+ + "the fallback library " + dstFile.getPath() |
+ + "(timestamp=" + fallbackLibTime + ")"); |
+ return true; |
+ } |
+ |
+ // Check file sizes. |
+ long packedLibSize = packedLib.getSize(); |
+ long fallbackLibSize = dstFile.length(); |
+ if (fallbackLibSize != packedLibSize) { |
+ Log.i(TAG, "Not using existing fallback file because " |
+ + "the library in the APK " + zipFile.getPath() |
+ + " (" + packedLibSize + "B) has a different size than " |
+ + "the fallback library " + dstFile.getPath() |
+ + "(" + fallbackLibSize + "B)"); |
+ return true; |
+ } |
+ |
+ Log.i(TAG, "Reusing existing file " + dstFile.getPath()); |
+ return false; |
+ } |
+ |
+ // Unpack a library from a zip file to the filesystem. |
+ private static void unpackLibraryFromZipFile(ZipFile zipArchive, ZipEntry packedLib, |
+ File dstFile) throws UnpackingException { |
+ // Open input stream for the library file inside the zip file. |
+ InputStream in; |
+ try { |
+ in = zipArchive.getInputStream(packedLib); |
+ } catch (IOException e) { |
+ throw new UnpackingException( |
+ "IO exception when locating library in the zip file", e); |
+ } |
+ |
+ // Ensure that the input stream is closed at the end. |
+ try { |
+ // Delete existing file if it exists. |
+ if (dstFile.exists()) { |
+ Log.i(TAG, "Deleting existing unpacked library file " + dstFile.getPath()); |
+ if (!dstFile.delete()) { |
+ throw new UnpackingException( |
+ "Failed to delete existing unpacked library file " + dstFile.getPath()); |
+ } |
+ } |
+ |
+ // Ensure that the library folder exists. Since this is added |
+ // for increased robustness, we log errors and carry on. |
+ try { |
+ dstFile.getParentFile().mkdirs(); |
+ } catch (Exception e) { |
+ Log.e(TAG, "Failed to make library folder", e); |
+ } |
+ |
+ // Create the destination file. |
+ try { |
+ if (!dstFile.createNewFile()) { |
+ throw new UnpackingException("existing unpacked library file was not deleted"); |
+ } |
+ } catch (IOException e) { |
+ throw new UnpackingException("failed to create unpacked library file", e); |
+ } |
+ |
+ // Open the output stream for the destination file. |
+ OutputStream out; |
+ try { |
+ out = new BufferedOutputStream(new FileOutputStream(dstFile)); |
+ } catch (FileNotFoundException e) { |
+ throw new UnpackingException( |
+ "failed to open output stream for unpacked library file", e); |
+ } |
+ |
+ // Ensure that the output stream is closed at the end. |
+ try { |
+ // Copy the library from the zip file to the destination file. |
+ Log.i(TAG, "Copying " + packedLib.getName() + " from " + zipArchive.getName() |
+ + " to " + dstFile.getPath()); |
+ byte[] buffer = new byte[BUFFER_SIZE]; |
+ int len; |
+ while ((len = in.read(buffer)) != -1) { |
+ out.write(buffer, 0, len); |
+ } |
+ } catch (IOException e) { |
+ throw new UnpackingException( |
+ "failed to copy the library from the zip file", e); |
+ } finally { |
+ close(out, "output stream"); |
+ } |
+ } finally { |
+ close(in, "input stream"); |
+ } |
+ } |
+ |
+ // Set up library file permissions. |
+ private static void setLibraryFilePermissions(File libFile) { |
+ // Change permission to rwxr-xr-x |
+ Log.i(TAG, "Setting file permissions for " + libFile.getPath()); |
+ if (!libFile.setReadable(/* readable */ true, /* ownerOnly */ false)) { |
+ Log.e(TAG, "failed to chmod a+r the temporary file"); |
+ } |
+ if (!libFile.setExecutable(/* executable */ true, /* ownerOnly */ false)) { |
+ Log.e(TAG, "failed to chmod a+x the temporary file"); |
+ } |
+ if (!libFile.setWritable(/* writable */ true)) { |
+ Log.e(TAG, "failed to chmod +w the temporary file"); |
+ } |
+ } |
+ |
+ // Close a closable and log a warning if it fails. |
+ private static void close(Closeable closeable, String name) { |
+ try { |
+ closeable.close(); |
+ } catch (IOException e) { |
+ // Warn and ignore. |
+ Log.w(TAG, "IO exception when closing " + name, e); |
+ } |
+ } |
+ |
+ // Close a zip file and log a warning if it fails. |
+ // This needs to be a separate method because ZipFile is not Closeable in |
+ // Java 6 (used on some older devices). |
+ private static void closeZipFile(ZipFile file) { |
+ try { |
+ file.close(); |
+ } catch (IOException e) { |
+ // Warn and ignore. |
+ Log.w(TAG, "IO exception when closing zip file", e); |
+ } |
+ } |
+ |
+ // Delete a file and log it. |
+ private static void delete(File file) { |
+ if (file.delete()) { |
+ Log.i(TAG, "Deleted " + file.getPath()); |
+ } else { |
+ Log.w(TAG, "Failed to delete " + file.getPath()); |
} |
} |
} |