diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
4 files changed, 664 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 0000000000..701780171e --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java @@ -0,0 +1,440 @@ +/* -*- 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 android.content.Context; +import android.os.Build; +import android.os.Environment; +import android.util.Log; +import dalvik.system.BaseDexClassLoader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.annotation.JNITarget; +import org.mozilla.gecko.annotation.RobocopTarget; + +public final class GeckoLoader { + private static final String LOGTAG = "GeckoLoader"; + + 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 getGREDir(final Context context) { + if (sGREDir == null) { + sGREDir = new File(context.getApplicationInfo().dataDir); + } + return sGREDir; + } + + 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 (final Exception e) { + Log.w(LOGTAG, "No download directory found.", e); + } + } + + private static void delTree(final File file) { + if (file.isDirectory()) { + final File[] children = file.listFiles(); + for (final File child : children) { + delTree(child); + } + } + file.delete(); + } + + private static File getTmpDir(final Context context) { + // It's important that this folder is in the cache directory so users can actually + // clear it when it gets too big. + return new File(context.getCacheDir(), "gecko_temp"); + } + + private static String escapeDoubleQuotes(final String str) { + return str.replaceAll("\"", "\\\""); + } + + private static void setupInitialPrefs(final Map<String, Object> prefs) { + if (prefs != null) { + final StringBuilder prefsEnv = new StringBuilder("MOZ_DEFAULT_PREFS="); + for (final String key : prefs.keySet()) { + final Object value = prefs.get(key); + if (value == null) { + continue; + } + prefsEnv.append(String.format("pref(\"%s\",", escapeDoubleQuotes(key))); + if (value instanceof String) { + prefsEnv.append(String.format("\"%s\"", escapeDoubleQuotes(value.toString()))); + } else if (value instanceof Boolean) { + prefsEnv.append((Boolean) value ? "true" : "false"); + } else { + prefsEnv.append(value.toString()); + } + + prefsEnv.append(");\n"); + } + + putenv(prefsEnv.toString()); + } + } + + @SuppressWarnings("deprecation") // for Build.CPU_ABI + public static synchronized void setupGeckoEnvironment( + final Context context, + final boolean isChildProcess, + final String profilePath, + final Collection<String> env, + final Map<String, Object> prefs, + final boolean xpcshell) { + for (final String e : env) { + putenv(e); + } + + putenv("MOZ_ANDROID_PACKAGE_NAME=" + context.getPackageName()); + + if (!isChildProcess) { + setupDownloadEnvironment(context); + + // profile home path + putenv("HOME=" + profilePath); + + // setup the downloads path + File f = Environment.getDownloadCacheDirectory(); + putenv("EXTERNAL_STORAGE=" + f.getPath()); + + // setup the app-specific cache path + f = context.getCacheDir(); + putenv("CACHE_DIRECTORY=" + f.getPath()); + + f = context.getExternalFilesDir(null); + if (f != null) { + putenv("PUBLIC_STORAGE=" + f.getPath()); + } + + if (Build.VERSION.SDK_INT >= 17) { + final 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); + } + } + + setupInitialPrefs(prefs); + } + + // Xpcshell tests set up their own temp directory + if (!xpcshell) { + // setup the tmp path + final File f = getTmpDir(context); + if (!f.exists()) { + f.mkdirs(); + } + putenv("TMPDIR=" + f.getPath()); + } + + putenv("LANG=" + Locale.getDefault().toString()); + + final Class<?> crashHandler = GeckoAppShell.getCrashHandlerService(); + if (crashHandler != null) { + putenv( + "MOZ_ANDROID_CRASH_HANDLER=" + context.getPackageName() + "/" + crashHandler.getName()); + } + + putenv("MOZ_ANDROID_DEVICE_SDK_VERSION=" + Build.VERSION.SDK_INT); + putenv("MOZ_ANDROID_CPU_ABI=" + Build.CPU_ABI); + + // env from extras could have reset out linker flags; set them again. + loadLibsSetupLocked(context); + } + + // Adapted from + // https://source.chromium.org/chromium/chromium/src/+/main:base/android/java/src/org/chromium/base/BundleUtils.java;l=196;drc=c0fedddd4a1444653235912cfae3d44b544ded01 + private static String getLibraryPath(final String libraryName) { + // Due to b/171269960 isolated split class loaders have an empty library path, so check + // the base module class loader first which loaded GeckoAppShell. If the library is not + // found there, attempt to construct the correct library path from the split. + String path = + ((BaseDexClassLoader) GeckoAppShell.class.getClassLoader()).findLibrary(libraryName); + if (path != null) { + return path; + } + + // SplitCompat is installed on the application context, so check there for library paths + // which were added to that ClassLoader. + final ClassLoader classLoader = GeckoAppShell.getApplicationContext().getClassLoader(); + if (classLoader instanceof BaseDexClassLoader) { + path = ((BaseDexClassLoader) classLoader).findLibrary(libraryName); + if (path != null) { + return path; + } + } + + throw new RuntimeException("Could not find mozglue path."); + } + + private static String getLibraryBase() { + final String mozglue = getLibraryPath("mozglue"); + final int lastSlash = mozglue.lastIndexOf('/'); + if (lastSlash < 0) { + throw new IllegalStateException("Invalid library path for libmozglue.so: " + mozglue); + } + final String base = mozglue.substring(0, lastSlash); + Log.i(LOGTAG, "Library base=" + base); + return base; + } + + private static void loadLibsSetupLocked(final Context context) { + putenv("GRE_HOME=" + getGREDir(context).getPath()); + putenv("MOZ_ANDROID_LIBDIR=" + getLibraryBase()); + } + + @RobocopTarget + public static synchronized void loadSQLiteLibs(final Context context) { + if (sSQLiteLibsLoaded) { + return; + } + + loadMozGlue(context); + loadLibsSetupLocked(context); + loadSQLiteLibsNative(); + sSQLiteLibsLoaded = true; + } + + public static synchronized void loadNSSLibs(final Context context) { + if (sNSSLibsLoaded) { + return; + } + + loadMozGlue(context); + loadLibsSetupLocked(context); + loadNSSLibsNative(); + sNSSLibsLoaded = true; + } + + @SuppressWarnings("deprecation") + private static 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. + final File outDirFile = new File(outDir); + if (!outDirFile.isDirectory()) { + if (!outDirFile.mkdirs()) { + Log.e(LOGTAG, "Couldn't create " + outDir); + return false; + } + } + + if (Build.VERSION.SDK_INT >= 21) { + final String[] abis = Build.SUPPORTED_ABIS; + for (final 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( + final String lib, final String outDir, final String apkPath, final 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 (final 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 (final Exception e) { + Log.e(LOGTAG, "Failed to extract lib from APK.", e); + return false; + } + } + + private static boolean attemptLoad(final String path) { + try { + System.load(path); + return true; + } catch (final 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. + * + * <p>Returns null or the cause exception. + */ + public static Throwable doLoadLibrary(final Context context, final String lib) { + try { + // Attempt 1: the way that should work. + System.loadLibrary(lib); + return null; + } catch (final Throwable e) { + final String libPath = getLibraryPath(lib); + // Does it even exist? + if (new File(libPath).exists()) { + if (attemptLoad(libPath)) { + // Success! + return null; + } + throw new RuntimeException( + "Library exists but couldn't load." + "Path: " + libPath + " lib: " + lib, e); + } + throw new RuntimeException( + "Library doesn't exist when it should." + "Path: " + libPath + " lib: " + lib, e); + } + } + + public static synchronized void loadMozGlue(final Context context) { + if (sMozGlueLoaded) { + return; + } + + doLoadLibrary(context, "mozglue"); + sMozGlueLoaded = true; + } + + public static synchronized void loadGeckoLibs(final Context context) { + loadLibsSetupLocked(context); + loadGeckoLibsNative(); + } + + @SuppressWarnings("serial") + public static class AbortException extends Exception { + public AbortException(final 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, + int prefsFd, + int prefMapFd, + int ipcFd, + int crashFd, + int crashAnnotationFd, + boolean xpcshell, + String outFilePath); + + private static native void loadGeckoLibsNative(); + + private static native void loadSQLiteLibsNative(); + + private static native void loadNSSLibsNative(); + + public static native void suppressCrashDialog(); +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java new file mode 100644 index 0000000000..3b0f8cc96b --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java @@ -0,0 +1,20 @@ +/* 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; + +// Class that all classes with native methods extend from. +public abstract class JNIObject { + // Pointer that references the native object. This is volatile because it may be accessed + // by multiple threads simultaneously. + private volatile long mHandle; + + // Dispose of any reference to a native object. + // + // If the native instance is destroyed from the native side, this should never be + // called, so you should throw an UnsupportedOperationException. If instead you + // want to destroy the native side from the Java end, make override this with + // a native call, and the right thing will be done in the native code. + protected abstract void disposeNative(); +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java new file mode 100644 index 0000000000..028cfd6590 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java @@ -0,0 +1,12 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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; + +public interface NativeReference { + public void release(); + + public boolean isReleased(); +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SharedMemory.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SharedMemory.java new file mode 100644 index 0000000000..af8b62c382 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SharedMemory.java @@ -0,0 +1,192 @@ +/* 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 android.annotation.SuppressLint; +import android.os.MemoryFile; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.util.Log; +import java.io.FileDescriptor; +import java.io.IOException; +import java.lang.reflect.Method; + +@SuppressLint("DiscouragedPrivateApi") +public class SharedMemory implements Parcelable { + private static final String LOGTAG = "GeckoShmem"; + private static final Method sGetFDMethod; + private ParcelFileDescriptor mDescriptor; + private int mSize; + private int mId; + private long mHandle; // The native pointer. + private boolean mIsMapped; + private MemoryFile mBackedFile; + + // MemoryFile.getFileDescriptor() is hidden. :( + static { + Method method = null; + try { + method = MemoryFile.class.getDeclaredMethod("getFileDescriptor"); + } catch (final NoSuchMethodException e) { + e.printStackTrace(); + } + sGetFDMethod = method; + } + + private SharedMemory(final Parcel in) { + mDescriptor = in.readFileDescriptor(); + mSize = in.readInt(); + mId = in.readInt(); + } + + public static final Creator<SharedMemory> CREATOR = + new Creator<SharedMemory>() { + @Override + public SharedMemory createFromParcel(final Parcel in) { + return new SharedMemory(in); + } + + @Override + public SharedMemory[] newArray(final int size) { + return new SharedMemory[size]; + } + }; + + @Override + public int describeContents() { + return CONTENTS_FILE_DESCRIPTOR; + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + // We don't want ParcelFileDescriptor.writeToParcel() to close the fd. + dest.writeFileDescriptor(mDescriptor.getFileDescriptor()); + dest.writeInt(mSize); + dest.writeInt(mId); + } + + public SharedMemory(final int id, final int size) throws NoSuchMethodException, IOException { + if (sGetFDMethod == null) { + throw new NoSuchMethodException("MemoryFile.getFileDescriptor() doesn't exist."); + } + mBackedFile = new MemoryFile(null, size); + try { + final FileDescriptor fd = (FileDescriptor) sGetFDMethod.invoke(mBackedFile); + mDescriptor = ParcelFileDescriptor.dup(fd); + mSize = size; + mId = id; + mBackedFile.allowPurging(false); + } catch (final Exception e) { + e.printStackTrace(); + close(); + throw new IOException(e.getMessage()); + } + } + + public void flush() { + if (!mIsMapped) { + return; + } + + unmap(mHandle, mSize); + mHandle = 0; + mIsMapped = false; + } + + public void close() { + flush(); + + if (mDescriptor != null) { + try { + mDescriptor.close(); + } catch (final IOException e) { + e.printStackTrace(); + } + mDescriptor = null; + } + } + + // Should only be called by process that allocates shared memory. + public void dispose() { + if (!isValid()) { + return; + } + + close(); + + if (mBackedFile != null) { + mBackedFile.close(); + mBackedFile = null; + } + } + + private native void unmap(long address, int size); + + public boolean isValid() { + return mDescriptor != null; + } + + public int getSize() { + return mSize; + } + + private int getFD() { + return isValid() ? mDescriptor.getFd() : -1; + } + + public long getPointer() { + if (!isValid()) { + return 0; + } + + if (!mIsMapped) { + try { + mHandle = map(getFD(), mSize); + } catch (final NullPointerException e) { + Log.e(LOGTAG, "SharedMemory#" + mId + " error.", e); + throw e; + } + if (mHandle != 0) { + mIsMapped = true; + } + } + return mHandle; + } + + private native long map(int fd, int size); + + @Override + protected void finalize() throws Throwable { + if (mBackedFile != null) { + Log.w(LOGTAG, "dispose() not called before finalizing"); + } + dispose(); + + super.finalize(); + } + + @Override + public String toString() { + return "SHM(" + + getSize() + + " bytes): id=" + + mId + + ", backing=" + + mBackedFile + + ",fd=" + + mDescriptor; + } + + @Override + public boolean equals(final Object that) { + return (this == that) || ((that instanceof SharedMemory) && (hashCode() == that.hashCode())); + } + + @Override + public int hashCode() { + return mId; + } +} |