#filter substitution /* -*- 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.content.Intent; import android.os.Build; import android.os.Environment; import android.util.Log; public final class GeckoLoader { private static final String LOGTAG = "GeckoLoader"; // These match AppConstants, but we're built earlier. private static final String ANDROID_PACKAGE_NAME = "@ANDROID_PACKAGE_NAME@"; private static final String MOZ_APP_ABI = "@MOZ_APP_ABI@"; private static volatile Intent sIntent; private static File sCacheFile; private static File sGREDir; private static final Object sLibLoadingLock = new Object(); // Must hold sLibLoadingLock while accessing the following boolean variables. private static boolean sSQLiteLibsLoaded; private static boolean sNSSLibsLoaded; private static boolean sMozGlueLoaded; private static boolean sLibsSetup; 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(Intent 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 Intent 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); } } 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()); /* We really want to use this code, but it requires bumping up the SDK to 17 so for now we will use reflection. See https://bugzilla.mozilla.org/show_bug.cgi?id=811763#c11 if (Build.VERSION.SDK_INT >= 17) { 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); } } */ try { Object userManager = context.getSystemService("user"); if (userManager != null) { // if userManager is non-null that means we're running on 4.2+ and so the rest of this // should just work Object userHandle = android.os.Process.class.getMethod("myUserHandle", (Class[])null).invoke(null); Object userSerial = userManager.getClass().getMethod("getSerialNumberForUser", userHandle.getClass()).invoke(userManager, userHandle); putenv("MOZ_ANDROID_USER_SERIAL_NUMBER=" + userSerial.toString()); } } catch (Exception e) { // Guard against any unexpected failures Log.d(LOGTAG, "Unable to set the user serial number", e); } setupLocaleEnvironment(); // We don't need this any more. sIntent = null; } private static void loadLibsSetup(Context context) { synchronized (sLibLoadingLock) { if (sLibsSetup) { return; } sLibsSetup = true; } // 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"); } } #ifdef MOZ_LINKER_EXTRACT putenv("MOZ_LINKER_EXTRACT=1"); // Ensure that the cache dir is world-writable File cacheDir = new File(linkerCache); if (cacheDir.isDirectory()) { cacheDir.setWritable(true, false); cacheDir.setExecutable(true, false); cacheDir.setReadable(true, false); } #endif } @RobocopTarget public static void loadSQLiteLibs(final Context context, final String apkName) { synchronized (sLibLoadingLock) { if (sSQLiteLibsLoaded) { return; } sSQLiteLibsLoaded = true; } loadMozGlue(context); loadLibsSetup(context); loadSQLiteLibsNative(apkName); } public static void loadNSSLibs(final Context context, final String apkName) { synchronized (sLibLoadingLock) { if (sNSSLibsLoaded) { return; } sNSSLibsLoaded = true; } loadMozGlue(context); loadLibsSetup(context); loadNSSLibsNative(apkName); } /** * 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; } } final String abi = android.os.Build.CPU_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 StringBuilder message = new StringBuilder("LOAD "); message.append(lib); // These might differ. If so, we know why the library won't load! message.append(": ABI: " + MOZ_APP_ABI + ", " + android.os.Build.CPU_ABI); message.append(": Data: " + context.getApplicationInfo().dataDir); try { final boolean appLibExists = new File("/data/app-lib/" + ANDROID_PACKAGE_NAME + "/lib" + lib + ".so").exists(); final boolean dataDataExists = new File("/data/data/" + ANDROID_PACKAGE_NAME + "/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/" + ANDROID_PACKAGE_NAME + "-1"; final String dashTwo = "/data/data/" + ANDROID_PACKAGE_NAME + "-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(", "); } } 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. if (attemptLoad("/data/app-lib/" + ANDROID_PACKAGE_NAME + "/lib" + lib + ".so")) { return; } // Attempt 5: even more optimistic. if (attemptLoad("/data/data/" + ANDROID_PACKAGE_NAME + "/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 static void loadMozGlue(final Context context) { synchronized (sLibLoadingLock) { if (sMozGlueLoaded) { return; } sMozGlueLoaded = true; } doLoadLibrary(context, "mozglue"); } public static void loadGeckoLibs(final Context context, final String apkName) { loadLibsSetup(context); loadGeckoLibsNative(apkName); } 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); }