diff options
author | feng@chromium.org <feng@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-03-21 12:39:56 +0000 |
---|---|---|
committer | feng@chromium.org <feng@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-03-21 12:39:56 +0000 |
commit | 7cd58be7b084710c7785cd856fd18e04c17c390a (patch) | |
tree | 54e7b9646e8be047ab93e649141b1d624fc2c887 /base | |
parent | 4bf7283e0b24061f5c254d0c443198b2346bfe4a (diff) | |
download | chromium_src-7cd58be7b084710c7785cd856fd18e04c17c390a.zip chromium_src-7cd58be7b084710c7785cd856fd18e04c17c390a.tar.gz chromium_src-7cd58be7b084710c7785cd856fd18e04c17c390a.tar.bz2 |
[Android] Workaround of an android platform bug.
On some Android devices (e.g., Sony Xperia), package manager may
fail to extract native libraries when updating Chrome.
The change tries alleviate the situation by:
1) name libchrome with version number;
2) when failed to load library, try to extract native libraies
and load them.
BUG=311644
Review URL: https://codereview.chromium.org/200753002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@258546 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'base')
4 files changed, 367 insertions, 6 deletions
diff --git a/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java b/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java index 4e15955..c3b3b35f 100644 --- a/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java +++ b/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java @@ -4,6 +4,7 @@ package org.chromium.base.library_loader; +import android.content.Context; import android.os.SystemClock; import android.util.Log; @@ -41,16 +42,42 @@ public class LibraryLoader { // library_loader_hooks.cc). private static boolean sInitialized = false; + // One-way switch becomes true if the system library loading failed, + // and the right native library was found and loaded by the hack. + // The flag is used to report UMA stats later. + private static boolean sNativeLibraryHackWasUsed = false; + + /** - * This method blocks until the library is fully loaded and initialized. + * TODO: http://crbug.com/354655 + * remove this method once WebViewChromiumFactoryProvider.java + * changes the call to ensureInitialized(null). */ public static void ensureInitialized() throws ProcessInitException { + ensureInitialized(null); + } + + /** + * This method blocks until the library is fully loaded and initialized. + * + * @param context The context in which the method is called, the caller + * may pass in a null context if it doesn't know in which context it + * is running, or it doesn't need to work around the issue + * http://b/13216167. + * + * When the context is not null and native library was not extracted + * by Android package manager, the LibraryLoader class + * will extract the native libraries from APK. This is a hack used to + * work around some Sony devices with the following platform bug: + * http://b/13216167. + */ + public static void ensureInitialized(Context context) throws ProcessInitException { synchronized (sLock) { if (sInitialized) { // Already initialized, nothing to do. return; } - loadAlreadyLocked(); + loadAlreadyLocked(context); initializeAlreadyLocked(CommandLine.getJavaSwitchesOrNull()); } } @@ -73,9 +100,9 @@ public class LibraryLoader { * * @throws ProcessInitException if the native library failed to load. */ - public static void loadNow() throws ProcessInitException { + public static void loadNow(Context context) throws ProcessInitException { synchronized (sLock) { - loadAlreadyLocked(); + loadAlreadyLocked(context); } } @@ -93,7 +120,7 @@ public class LibraryLoader { } // Invoke System.loadLibrary(...), triggering JNI_OnLoad in native code - private static void loadAlreadyLocked() throws ProcessInitException { + private static void loadAlreadyLocked(Context context) throws ProcessInitException { try { if (!sLoaded) { assert !sInitialized; @@ -108,7 +135,21 @@ public class LibraryLoader { if (useChromiumLinker) { Linker.loadLibrary(library); } else { - System.loadLibrary(library); + try { + System.loadLibrary(library); + if (context != null) { + LibraryLoaderHelper.deleteWorkaroundLibrariesAsynchronously( + context); + } + } catch (UnsatisfiedLinkError e) { + if (context != null + && LibraryLoaderHelper.tryLoadLibraryUsingWorkaround(context, + library)) { + sNativeLibraryHackWasUsed = true; + } else { + throw e; + } + } } } if (useChromiumLinker) Linker.finishLibraryLoad(); @@ -154,6 +195,8 @@ public class LibraryLoader { nativeRecordChromiumAndroidLinkerHistogram(Linker.loadAtFixedAddressFailed(), SysUtils.isLowEndDevice()); } + + nativeRecordNativeLibraryHack(sNativeLibraryHackWasUsed); } // Only methods needed before or during normal JNI registration are during System.OnLoad. @@ -174,4 +217,6 @@ public class LibraryLoader { // Get the version of the native library. This is needed so that we can check we // have the right version before initializing the (rest of the) JNI. private static native String nativeGetVersionNumber(); + + private static native void nativeRecordNativeLibraryHack(boolean usedHack); } 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 new file mode 100644 index 0000000..7eeb2c8 --- /dev/null +++ b/base/android/java/src/org/chromium/base/library_loader/LibraryLoaderHelper.java @@ -0,0 +1,259 @@ +// 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); + } + } +} diff --git a/base/android/javatests/src/org/chromium/base/LibraryLoaderHelperTest.java b/base/android/javatests/src/org/chromium/base/LibraryLoaderHelperTest.java new file mode 100644 index 0000000..85f4c77 --- /dev/null +++ b/base/android/javatests/src/org/chromium/base/LibraryLoaderHelperTest.java @@ -0,0 +1,53 @@ +// 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; + +import android.content.Context; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.MediumTest; + +import org.chromium.base.library_loader.LibraryLoaderHelper; + +import java.io.File; + +/** + * Test class for the native library hack. + */ +public class LibraryLoaderHelperTest extends InstrumentationTestCase { + private static final String TAG = "LibraryLoaderHelperTest"; + + @Override + public void setUp() throws Exception { + Context context = getInstrumentation().getTargetContext(); + LibraryLoaderHelper.deleteWorkaroundLibrariesSynchronously(context); + } + + @MediumTest + public void testLoadNativeLibraries() { + getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + Context context = getInstrumentation().getTargetContext(); + File libDir = LibraryLoaderHelper.getWorkaroundLibDir(context); + assertTrue(libDir.exists()); + assertTrue(libDir.isDirectory()); + assertEquals(libDir.list().length, 0); + + assertTrue( + LibraryLoaderHelper.loadNativeLibrariesUsingWorkaroundForTesting( + context)); + + assertTrue(libDir.list().length > 0); + } + }); + } + + @Override + public void tearDown() throws Exception { + Context context = getInstrumentation().getTargetContext(); + LibraryLoaderHelper.deleteWorkaroundLibrariesSynchronously(context); + super.tearDown(); + } +} diff --git a/base/android/library_loader/library_loader_hooks.cc b/base/android/library_loader/library_loader_hooks.cc index 8fa16df..0145eab 100644 --- a/base/android/library_loader/library_loader_hooks.cc +++ b/base/android/library_loader/library_loader_hooks.cc @@ -64,5 +64,9 @@ jstring GetVersionNumber(JNIEnv* env, jclass clazz) { return env->NewStringUTF(g_library_version_number); } +static void RecordNativeLibraryHack(JNIEnv*, jclass, jboolean usedHack) { + UMA_HISTOGRAM_BOOLEAN("LibraryLoader.NativeLibraryHack", usedHack); +} + } // namespace android } // namespace base |