diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java | 549 |
1 files changed, 549 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java new file mode 100644 index 000000000..0bef2435b --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java @@ -0,0 +1,549 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.mozglue; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Locale; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.util.Log; + +import org.mozilla.gecko.annotation.JNITarget; +import org.mozilla.gecko.annotation.RobocopTarget; +import org.mozilla.gecko.AppConstants; + +public final class GeckoLoader { + private static final String LOGTAG = "GeckoLoader"; + + private static volatile SafeIntent sIntent; + private static File sCacheFile; + private static File sGREDir; + + /* Synchronized on GeckoLoader.class. */ + private static boolean sSQLiteLibsLoaded; + private static boolean sNSSLibsLoaded; + private static boolean sMozGlueLoaded; + + private GeckoLoader() { + // prevent instantiation + } + + public static File getCacheDir(Context context) { + if (sCacheFile == null) { + sCacheFile = context.getCacheDir(); + } + return sCacheFile; + } + + public static File getGREDir(Context context) { + if (sGREDir == null) { + sGREDir = new File(context.getApplicationInfo().dataDir); + } + return sGREDir; + } + + private static void setupPluginEnvironment(Context context, String[] pluginDirs) { + // setup plugin path directories + try { + // Check to see if plugins were blocked. + if (pluginDirs == null) { + putenv("MOZ_PLUGINS_BLOCKED=1"); + putenv("MOZ_PLUGIN_PATH="); + return; + } + + StringBuilder pluginSearchPath = new StringBuilder(); + for (int i = 0; i < pluginDirs.length; i++) { + pluginSearchPath.append(pluginDirs[i]); + pluginSearchPath.append(":"); + } + putenv("MOZ_PLUGIN_PATH=" + pluginSearchPath); + + File pluginDataDir = context.getDir("plugins", 0); + putenv("ANDROID_PLUGIN_DATADIR=" + pluginDataDir.getPath()); + + File pluginPrivateDataDir = context.getDir("plugins_private", 0); + putenv("ANDROID_PLUGIN_DATADIR_PRIVATE=" + pluginPrivateDataDir.getPath()); + + } catch (Exception ex) { + Log.w(LOGTAG, "Caught exception getting plugin dirs.", ex); + } + } + + private static void setupDownloadEnvironment(final Context context) { + try { + File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File updatesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); + if (downloadDir == null) { + downloadDir = new File(Environment.getExternalStorageDirectory().getPath(), "download"); + } + if (updatesDir == null) { + updatesDir = downloadDir; + } + putenv("DOWNLOADS_DIRECTORY=" + downloadDir.getPath()); + putenv("UPDATES_DIRECTORY=" + updatesDir.getPath()); + } catch (Exception e) { + Log.w(LOGTAG, "No download directory found.", e); + } + } + + private static void delTree(File file) { + if (file.isDirectory()) { + File children[] = file.listFiles(); + for (File child : children) { + delTree(child); + } + } + file.delete(); + } + + private static File getTmpDir(Context context) { + File tmpDir = context.getDir("tmpdir", Context.MODE_PRIVATE); + // check if the old tmp dir is there + File oldDir = new File(tmpDir.getParentFile(), "app_tmp"); + if (oldDir.exists()) { + delTree(oldDir); + } + return tmpDir; + } + + public static void setLastIntent(SafeIntent intent) { + sIntent = intent; + } + + public static void setupGeckoEnvironment(Context context, String[] pluginDirs, String profilePath) { + // if we have an intent (we're being launched by an activity) + // read in any environmental variables from it here + final SafeIntent intent = sIntent; + if (intent != null) { + String env = intent.getStringExtra("env0"); + Log.d(LOGTAG, "Gecko environment env0: " + env); + for (int c = 1; env != null; c++) { + putenv(env); + env = intent.getStringExtra("env" + c); + Log.d(LOGTAG, "env" + c + ": " + env); + } + } + + putenv("MOZ_ANDROID_PACKAGE_NAME=" + context.getPackageName()); + + setupPluginEnvironment(context, pluginDirs); + setupDownloadEnvironment(context); + + // profile home path + putenv("HOME=" + profilePath); + + // setup the tmp path + File f = getTmpDir(context); + if (!f.exists()) { + f.mkdirs(); + } + putenv("TMPDIR=" + f.getPath()); + + // setup the downloads path + f = Environment.getDownloadCacheDirectory(); + putenv("EXTERNAL_STORAGE=" + f.getPath()); + + // setup the app-specific cache path + f = context.getCacheDir(); + putenv("CACHE_DIRECTORY=" + f.getPath()); + + if (AppConstants.Versions.feature17Plus) { + android.os.UserManager um = (android.os.UserManager)context.getSystemService(Context.USER_SERVICE); + if (um != null) { + putenv("MOZ_ANDROID_USER_SERIAL_NUMBER=" + um.getSerialNumberForUser(android.os.Process.myUserHandle())); + } else { + Log.d(LOGTAG, "Unable to obtain user manager service on a device with SDK version " + Build.VERSION.SDK_INT); + } + } + setupLocaleEnvironment(); + + // We don't need this any more. + sIntent = null; + } + + private static void loadLibsSetupLocked(Context context) { + // The package data lib directory isn't placed in ld.so's + // search path, so we have to manually load libraries that + // libxul will depend on. Not ideal. + + File cacheFile = getCacheDir(context); + putenv("GRE_HOME=" + getGREDir(context).getPath()); + + // setup the libs cache + String linkerCache = System.getenv("MOZ_LINKER_CACHE"); + if (linkerCache == null) { + linkerCache = cacheFile.getPath(); + putenv("MOZ_LINKER_CACHE=" + linkerCache); + } + + // Disable on-demand decompression of the linker on devices where it + // is known to cause crashes. + String forced_ondemand = System.getenv("MOZ_LINKER_ONDEMAND"); + if (forced_ondemand == null) { + if ("HTC".equals(android.os.Build.MANUFACTURER) && + "HTC Vision".equals(android.os.Build.MODEL)) { + putenv("MOZ_LINKER_ONDEMAND=0"); + } + } + + putenv("MOZ_LINKER_EXTRACT=1"); + } + + @RobocopTarget + public synchronized static void loadSQLiteLibs(final Context context, final String apkName) { + if (sSQLiteLibsLoaded) { + return; + } + + loadMozGlue(context); + loadLibsSetupLocked(context); + loadSQLiteLibsNative(apkName); + sSQLiteLibsLoaded = true; + } + + public synchronized static void loadNSSLibs(final Context context, final String apkName) { + if (sNSSLibsLoaded) { + return; + } + + loadMozGlue(context); + loadLibsSetupLocked(context); + loadNSSLibsNative(apkName); + sNSSLibsLoaded = true; + } + + @SuppressWarnings("deprecation") + private static final String getCPUABI() { + return android.os.Build.CPU_ABI; + } + + /** + * Copy a library out of our APK. + * + * @param context a Context. + * @param lib the name of the library; e.g., "mozglue". + * @param outDir the output directory for the .so. No trailing slash. + * @return true on success, false on failure. + */ + private static boolean extractLibrary(final Context context, final String lib, final String outDir) { + final String apkPath = context.getApplicationInfo().sourceDir; + + // Sanity check. + if (!apkPath.endsWith(".apk")) { + Log.w(LOGTAG, "sourceDir is not an APK."); + return false; + } + + // Try to extract the named library from the APK. + File outDirFile = new File(outDir); + if (!outDirFile.isDirectory()) { + if (!outDirFile.mkdirs()) { + Log.e(LOGTAG, "Couldn't create " + outDir); + return false; + } + } + + if (AppConstants.Versions.feature21Plus) { + String[] abis = Build.SUPPORTED_ABIS; + for (String abi : abis) { + if (tryLoadWithABI(lib, outDir, apkPath, abi)) { + return true; + } + } + return false; + } else { + final String abi = getCPUABI(); + return tryLoadWithABI(lib, outDir, apkPath, abi); + } + } + + private static boolean tryLoadWithABI(String lib, String outDir, String apkPath, String abi) { + try { + final ZipFile zipFile = new ZipFile(new File(apkPath)); + try { + final String libPath = "lib/" + abi + "/lib" + lib + ".so"; + final ZipEntry entry = zipFile.getEntry(libPath); + if (entry == null) { + Log.w(LOGTAG, libPath + " not found in APK " + apkPath); + return false; + } + + final InputStream in = zipFile.getInputStream(entry); + try { + final String outPath = outDir + "/lib" + lib + ".so"; + final FileOutputStream out = new FileOutputStream(outPath); + final byte[] bytes = new byte[1024]; + int read; + + Log.d(LOGTAG, "Copying " + libPath + " to " + outPath); + boolean failed = false; + try { + while ((read = in.read(bytes, 0, 1024)) != -1) { + out.write(bytes, 0, read); + } + } catch (Exception e) { + Log.w(LOGTAG, "Failing library copy.", e); + failed = true; + } finally { + out.close(); + } + + if (failed) { + // Delete the partial copy so we don't fail to load it. + // Don't bother to check the return value -- there's nothing + // we can do about a failure. + new File(outPath).delete(); + } else { + // Mark the file as executable. This doesn't seem to be + // necessary for the loader, but it's the normal state of + // affairs. + Log.d(LOGTAG, "Marking " + outPath + " as executable."); + new File(outPath).setExecutable(true); + } + + return !failed; + } finally { + in.close(); + } + } finally { + zipFile.close(); + } + } catch (Exception e) { + Log.e(LOGTAG, "Failed to extract lib from APK.", e); + return false; + } + } + + private static String getLoadDiagnostics(final Context context, final String lib) { + final String androidPackageName = context.getPackageName(); + + final StringBuilder message = new StringBuilder("LOAD "); + message.append(lib); + + // These might differ. If so, we know why the library won't load! + message.append(": ABI: " + AppConstants.MOZ_APP_ABI + ", " + getCPUABI()); + message.append(": Data: " + context.getApplicationInfo().dataDir); + try { + final boolean appLibExists = new File("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so").exists(); + final boolean dataDataExists = new File("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so").exists(); + message.append(", ax=" + appLibExists); + message.append(", ddx=" + dataDataExists); + } catch (Throwable e) { + message.append(": ax/ddx fail, "); + } + + try { + final String dashOne = "/data/data/" + androidPackageName + "-1"; + final String dashTwo = "/data/data/" + androidPackageName + "-2"; + final boolean dashOneExists = new File(dashOne).exists(); + final boolean dashTwoExists = new File(dashTwo).exists(); + message.append(", -1x=" + dashOneExists); + message.append(", -2x=" + dashTwoExists); + } catch (Throwable e) { + message.append(", dash fail, "); + } + + try { + if (Build.VERSION.SDK_INT >= 9) { + final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir; + final boolean nativeLibDirExists = new File(nativeLibPath).exists(); + final boolean nativeLibLibExists = new File(nativeLibPath + "/lib" + lib + ".so").exists(); + + message.append(", nativeLib: " + nativeLibPath); + message.append(", dirx=" + nativeLibDirExists); + message.append(", libx=" + nativeLibLibExists); + } else { + message.append(", <pre-9>"); + } + } catch (Throwable e) { + message.append(", nativeLib fail."); + } + + return message.toString(); + } + + private static final boolean attemptLoad(final String path) { + try { + System.load(path); + return true; + } catch (Throwable e) { + Log.wtf(LOGTAG, "Couldn't load " + path + ": " + e); + } + + return false; + } + + /** + * The first two attempts at loading a library: directly, and + * then using the app library path. + * + * Returns null or the cause exception. + */ + private static final Throwable doLoadLibraryExpected(final Context context, final String lib) { + try { + // Attempt 1: the way that should work. + System.loadLibrary(lib); + return null; + } catch (Throwable e) { + Log.wtf(LOGTAG, "Couldn't load " + lib + ". Trying native library dir."); + + if (Build.VERSION.SDK_INT < 9) { + // We can't use nativeLibraryDir. + return e; + } + + // Attempt 2: use nativeLibraryDir, which should also work. + final String libDir = context.getApplicationInfo().nativeLibraryDir; + final String libPath = libDir + "/lib" + lib + ".so"; + + // Does it even exist? + if (new File(libPath).exists()) { + if (attemptLoad(libPath)) { + // Success! + return null; + } + Log.wtf(LOGTAG, "Library exists but couldn't load!"); + } else { + Log.wtf(LOGTAG, "Library doesn't exist when it should."); + } + + // We failed. Return the original cause. + return e; + } + } + + public static void doLoadLibrary(final Context context, final String lib) { + final Throwable e = doLoadLibraryExpected(context, lib); + if (e == null) { + // Success. + return; + } + + // If we're in a mismatched UID state (Bug 1042935 Comment 16) there's really + // nothing we can do. + if (Build.VERSION.SDK_INT >= 9) { + final String nativeLibPath = context.getApplicationInfo().nativeLibraryDir; + if (nativeLibPath.contains("mismatched_uid")) { + throw new RuntimeException("Fatal: mismatched UID: cannot load."); + } + } + + // Attempt 3: try finding the path the pseudo-supported way using .dataDir. + final String dataLibPath = context.getApplicationInfo().dataDir + "/lib/lib" + lib + ".so"; + if (attemptLoad(dataLibPath)) { + return; + } + + // Attempt 4: use /data/app-lib directly. This is a last-ditch effort. + final String androidPackageName = context.getPackageName(); + if (attemptLoad("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so")) { + return; + } + + // Attempt 5: even more optimistic. + if (attemptLoad("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so")) { + return; + } + + // Look in our files directory, copying from the APK first if necessary. + final String filesLibDir = context.getFilesDir() + "/lib"; + final String filesLibPath = filesLibDir + "/lib" + lib + ".so"; + if (new File(filesLibPath).exists()) { + if (attemptLoad(filesLibPath)) { + return; + } + } else { + // Try copying. + if (extractLibrary(context, lib, filesLibDir)) { + // Let's try it! + if (attemptLoad(filesLibPath)) { + return; + } + } + } + + // Give up loudly, leaking information to debug the failure. + final String message = getLoadDiagnostics(context, lib); + Log.e(LOGTAG, "Load diagnostics: " + message); + + // Throw the descriptive message, using the original library load + // failure as the cause. + throw new RuntimeException(message, e); + } + + public synchronized static void loadMozGlue(final Context context) { + if (sMozGlueLoaded) { + return; + } + + doLoadLibrary(context, "mozglue"); + sMozGlueLoaded = true; + } + + public synchronized static void loadGeckoLibs(final Context context, final String apkName) { + loadLibsSetupLocked(context); + loadGeckoLibsNative(apkName); + } + + public synchronized static void extractGeckoLibs(final Context context, final String apkName) { + loadLibsSetupLocked(context); + try { + extractGeckoLibsNative(apkName); + } catch (Exception e) { + Log.e(LOGTAG, "Failing library extraction.", e); + } + } + + private static void setupLocaleEnvironment() { + putenv("LANG=" + Locale.getDefault().toString()); + NumberFormat nf = NumberFormat.getInstance(); + if (nf instanceof DecimalFormat) { + DecimalFormat df = (DecimalFormat)nf; + DecimalFormatSymbols dfs = df.getDecimalFormatSymbols(); + + putenv("LOCALE_DECIMAL_POINT=" + dfs.getDecimalSeparator()); + putenv("LOCALE_THOUSANDS_SEP=" + dfs.getGroupingSeparator()); + putenv("LOCALE_GROUPING=" + (char)df.getGroupingSize()); + } + } + + @SuppressWarnings("serial") + public static class AbortException extends Exception { + public AbortException(String msg) { + super(msg); + } + } + + @JNITarget + public static void abort(final String msg) { + final Thread thread = Thread.currentThread(); + final Thread.UncaughtExceptionHandler uncaughtHandler = + thread.getUncaughtExceptionHandler(); + if (uncaughtHandler != null) { + uncaughtHandler.uncaughtException(thread, new AbortException(msg)); + } + } + + // These methods are implemented in mozglue/android/nsGeckoUtils.cpp + private static native void putenv(String map); + + // These methods are implemented in mozglue/android/APKOpen.cpp + public static native void nativeRun(String args); + private static native void loadGeckoLibsNative(String apkName); + private static native void loadSQLiteLibsNative(String apkName); + private static native void loadNSSLibsNative(String apkName); + private static native void extractGeckoLibsNative(String apkName); +} |