summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/util')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java21
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java130
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java58
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java72
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java1194
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java389
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java46
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IXPCOMEventTarget.java12
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageDecoder.java88
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java334
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputDeviceUtils.java20
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java116
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java168
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java149
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java145
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMError.jinja38
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMEventTarget.java170
17 files changed, 3150 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
new file mode 100644
index 0000000000..b8d7ea3107
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
@@ -0,0 +1,21 @@
+/* -*- 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.util;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+@RobocopTarget
+public interface BundleEventListener {
+ /**
+ * Handles a message sent from Gecko.
+ *
+ * @param event The name of the event being sent.
+ * @param message The message data.
+ * @param callback The callback interface for this message. A callback is provided only if the
+ * originating call included a callback argument; otherwise, callback will be null.
+ */
+ void handleMessage(String event, GeckoBundle message, EventCallback callback);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java
new file mode 100644
index 0000000000..7a445de90a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java
@@ -0,0 +1,130 @@
+/* -*- 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.util;
+
+import android.os.Bundle;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.TypeDescription;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.Constructor;
+import org.yaml.snakeyaml.error.YAMLException;
+
+// Raptor writes a *-config.yaml file to specify Gecko runtime settings (e.g.
+// the profile dir). This file gets deserialized into a DebugConfig object.
+// Yaml uses reflection to create this class so we have to tell PG to keep it.
+@ReflectionTarget
+public class DebugConfig {
+ private static final String LOGTAG = "GeckoDebugConfig";
+
+ protected Map<String, Object> prefs;
+ protected Map<String, String> env;
+ protected List<String> args;
+
+ public static class ConfigException extends RuntimeException {
+ public ConfigException(final String message) {
+ super(message);
+ }
+ }
+
+ public static @NonNull DebugConfig fromFile(final @NonNull File configFile)
+ throws FileNotFoundException {
+ final LoaderOptions options = new LoaderOptions();
+ final Constructor constructor = new Constructor(DebugConfig.class, options);
+ final TypeDescription description = new TypeDescription(DebugConfig.class);
+ description.putMapPropertyType("prefs", String.class, Object.class);
+ description.putMapPropertyType("env", String.class, String.class);
+ description.putListPropertyType("args", String.class);
+
+ final Yaml yaml = new Yaml(constructor);
+ yaml.addTypeDescription(description);
+
+ final FileInputStream fileInputStream = new FileInputStream(configFile);
+ try {
+ return yaml.load(fileInputStream);
+ } catch (final YAMLException e) {
+ throw new ConfigException(e.getMessage());
+ } finally {
+ try {
+ if (fileInputStream != null) {
+ ((Closeable) fileInputStream).close();
+ }
+ } catch (final IOException e) {
+ }
+ }
+ }
+
+ @Nullable
+ public Bundle mergeIntoExtras(final @Nullable Bundle extras) {
+ if (env == null) {
+ return extras;
+ }
+
+ Log.d(LOGTAG, "Adding environment variables from debug config: " + env);
+
+ final Bundle result = extras != null ? extras : new Bundle();
+
+ int c = 0;
+ while (result.getString("env" + c) != null) {
+ c += 1;
+ }
+
+ for (final Map.Entry<String, String> entry : env.entrySet()) {
+ result.putString("env" + c, entry.getKey() + "=" + entry.getValue());
+ c += 1;
+ }
+
+ return result;
+ }
+
+ @Nullable
+ public String[] mergeIntoArgs(final @Nullable String[] initArgs) {
+ if (args == null) {
+ return initArgs;
+ }
+
+ Log.d(LOGTAG, "Adding arguments from debug config: " + args);
+
+ final ArrayList<String> combinedArgs = new ArrayList<>();
+ if (initArgs != null) {
+ combinedArgs.addAll(Arrays.asList(initArgs));
+ }
+ combinedArgs.addAll(args);
+
+ return combinedArgs.toArray(new String[combinedArgs.size()]);
+ }
+
+ @Nullable
+ public Map<String, Object> mergeIntoPrefs(final @Nullable Map<String, Object> initPrefs) {
+ if (prefs == null) {
+ return initPrefs;
+ }
+
+ Log.d(LOGTAG, "Adding prefs from debug config: " + prefs);
+
+ final Map<String, Object> combinedPrefs = new HashMap<>();
+ if (initPrefs != null) {
+ combinedPrefs.putAll(initPrefs);
+ }
+ combinedPrefs.putAll(prefs);
+
+ return Collections.unmodifiableMap(combinedPrefs);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
new file mode 100644
index 0000000000..3ef469ac1b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
@@ -0,0 +1,58 @@
+/* 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.util;
+
+import androidx.annotation.Nullable;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.geckoview.GeckoResult;
+
+/**
+ * Callback interface for Gecko requests.
+ *
+ * <p>For each instance of EventCallback, exactly one of sendResponse, sendError, or sendCancel must
+ * be called to prevent observer leaks. If more than one send* method is called, or if a single send
+ * method is called multiple times, an {@link IllegalStateException} will be thrown.
+ */
+@RobocopTarget
+@WrapForJNI(calledFrom = "gecko")
+public interface EventCallback {
+ /**
+ * Sends a success response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ void sendSuccess(Object response);
+
+ /**
+ * Sends an error response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ void sendError(Object response);
+
+ /**
+ * Resolve this Event callback with the result from the {@link GeckoResult}.
+ *
+ * @param response the result that will be used for this callback.
+ */
+ default <T> void resolveTo(final @Nullable GeckoResult<T> response) {
+ if (response == null) {
+ sendSuccess(null);
+ return;
+ }
+ response.accept(
+ this::sendSuccess,
+ throwable -> {
+ // Don't propagate Errors, just crash
+ if (!(throwable instanceof Exception)) {
+ throw new GeckoResult.UncaughtException(throwable);
+ }
+ sendError(throwable.getMessage());
+ });
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
new file mode 100644
index 0000000000..01b177fe21
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
@@ -0,0 +1,72 @@
+/* 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.util;
+
+import android.os.Handler;
+import android.os.Looper;
+
+final class GeckoBackgroundThread extends Thread {
+ private static final String LOOPER_NAME = "GeckoBackgroundThread";
+
+ // Guarded by 'GeckoBackgroundThread.class'.
+ private static Handler handler;
+ private static Thread thread;
+
+ // The initial Runnable to run on the new thread. Its purpose
+ // is to avoid us having to wait for the new thread to start.
+ private Runnable mInitialRunnable;
+
+ // Singleton, so private constructor.
+ private GeckoBackgroundThread(final Runnable initialRunnable) {
+ mInitialRunnable = initialRunnable;
+ }
+
+ @Override
+ public void run() {
+ setName(LOOPER_NAME);
+ Looper.prepare();
+
+ synchronized (GeckoBackgroundThread.class) {
+ handler = new Handler();
+ GeckoBackgroundThread.class.notifyAll();
+ }
+
+ if (mInitialRunnable != null) {
+ mInitialRunnable.run();
+ mInitialRunnable = null;
+ }
+
+ Looper.loop();
+ }
+
+ private static void startThread(final Runnable initialRunnable) {
+ thread = new GeckoBackgroundThread(initialRunnable);
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ // Get a Handler for a looper thread, or create one if it doesn't yet exist.
+ /*package*/ static synchronized Handler getHandler() {
+ if (thread == null) {
+ startThread(null);
+ }
+
+ while (handler == null) {
+ try {
+ GeckoBackgroundThread.class.wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ return handler;
+ }
+
+ /*package*/ static synchronized void post(final Runnable runnable) {
+ if (thread == null) {
+ startThread(runnable);
+ return;
+ }
+ getHandler().post(runnable);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java
new file mode 100644
index 0000000000..315c4a89d7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java
@@ -0,0 +1,1194 @@
+/* -*- 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.util;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.collection.SimpleArrayMap;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * A lighter-weight version of Bundle that adds support for type coercion (e.g. int to double) in
+ * order to better cooperate with JS objects.
+ */
+@RobocopTarget
+public final class GeckoBundle implements Parcelable {
+ private static final String LOGTAG = "GeckoBundle";
+ private static final boolean DEBUG = false;
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
+
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+ private static final int[] EMPTY_INT_ARRAY = new int[0];
+ private static final long[] EMPTY_LONG_ARRAY = new long[0];
+ private static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ private static final GeckoBundle[] EMPTY_BUNDLE_ARRAY = new GeckoBundle[0];
+
+ private SimpleArrayMap<String, Object> mMap;
+
+ /** Construct an empty GeckoBundle. */
+ public GeckoBundle() {
+ mMap = new SimpleArrayMap<>();
+ }
+
+ /**
+ * Construct an empty GeckoBundle with specific capacity.
+ *
+ * @param capacity Initial capacity.
+ */
+ public GeckoBundle(final int capacity) {
+ mMap = new SimpleArrayMap<>(capacity);
+ }
+
+ /**
+ * Construct a copy of another GeckoBundle.
+ *
+ * @param bundle GeckoBundle to copy from.
+ */
+ public GeckoBundle(final GeckoBundle bundle) {
+ mMap = new SimpleArrayMap<>(bundle.mMap);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private GeckoBundle(final String[] keys, final Object[] values) {
+ final int len = keys.length;
+ mMap = new SimpleArrayMap<>(len);
+ for (int i = 0; i < len; i++) {
+ mMap.put(keys[i], values[i]);
+ }
+ }
+
+ /** Clear all mappings. */
+ public void clear() {
+ mMap.clear();
+ }
+
+ /**
+ * Returns whether a mapping exists. Null String, Bundle, or arrays are treated as nonexistent.
+ *
+ * @param key Key to look for.
+ * @return True if the specified key exists and the value is not null.
+ */
+ public boolean containsKey(final String key) {
+ return mMap.get(key) != null;
+ }
+
+ /**
+ * Returns the value associated with a mapping as an Object.
+ *
+ * @param key Key to look for.
+ * @return Mapping value or null if the mapping does not exist.
+ */
+ public Object get(final String key) {
+ return mMap.get(key);
+ }
+
+ /**
+ * Returns the value associated with a boolean mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Boolean value
+ */
+ public boolean getBoolean(final String key, final boolean defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : (Boolean) value;
+ }
+
+ /**
+ * Returns the value associated with a boolean mapping, or false if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Boolean value
+ */
+ public boolean getBoolean(final String key) {
+ return getBoolean(key, false);
+ }
+
+ /**
+ * Returns the value associated with a Boolean mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Boolean value
+ */
+ public Boolean getBooleanObject(final String key, final Boolean defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : (Boolean) value;
+ }
+
+ /**
+ * Returns the value associated with a Boolean mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Boolean value
+ */
+ public Boolean getBooleanObject(final String key) {
+ return getBooleanObject(key, null);
+ }
+
+ /**
+ * Returns the value associated with a boolean array mapping, or null if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return Boolean array value
+ */
+ public boolean[] getBooleanArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0 ? EMPTY_BOOLEAN_ARRAY : (boolean[]) value;
+ }
+
+ /**
+ * Returns the value associated with a double mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Double value
+ */
+ public double getDouble(final String key, final double defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : ((Number) value).doubleValue();
+ }
+
+ /**
+ * Returns the value associated with a double mapping, or 0.0 if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Double value
+ */
+ public double getDouble(final String key) {
+ return getDouble(key, 0.0);
+ }
+
+ private static double[] getDoubleArray(final int[] array) {
+ final int len = array.length;
+ final double[] ret = new double[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = (double) array[i];
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the value associated with a double array mapping, or null if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return Double array value
+ */
+ public double[] getDoubleArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0
+ ? EMPTY_DOUBLE_ARRAY
+ : value instanceof int[] ? getDoubleArray((int[]) value) : (double[]) value;
+ }
+
+ /**
+ * Returns the value associated with a Double mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Double value
+ */
+ public Double getDoubleObject(final String key) {
+ return getDoubleObject(key, null);
+ }
+
+ /**
+ * Returns the value associated with a Double mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return Double value
+ */
+ public Double getDoubleObject(final String key, final Double defaultValue) {
+ final Object value = mMap.get(key);
+ if (value == null) {
+ return defaultValue;
+ }
+ return ((Number) value).doubleValue();
+ }
+
+ /**
+ * Returns the value associated with an int mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Int value
+ */
+ public int getInt(final String key, final int defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : ((Number) value).intValue();
+ }
+
+ /**
+ * Returns the value associated with an int mapping, or 0 if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Int value
+ */
+ public int getInt(final String key) {
+ return getInt(key, 0);
+ }
+
+ /**
+ * Returns the value associated with an Integer mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Int value
+ */
+ public Integer getInteger(final String key, final Integer defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : ((Integer) value);
+ }
+
+ /**
+ * Returns the value associated with an Integer mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Int value
+ */
+ public Integer getInteger(final String key) {
+ return getInteger(key, null);
+ }
+
+ private static int[] getIntArray(final double[] array) {
+ final int len = array.length;
+ final int[] ret = new int[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = (int) array[i];
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the value associated with an int array mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Int array value
+ */
+ public int[] getIntArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0
+ ? EMPTY_INT_ARRAY
+ : value instanceof double[] ? getIntArray((double[]) value) : (int[]) value;
+ }
+
+ /**
+ * Returns the value associated with an byte array mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Byte array value
+ */
+ public byte[] getByteArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null ? null : Array.getLength(value) == 0 ? EMPTY_BYTE_ARRAY : (byte[]) value;
+ }
+
+ /**
+ * Returns the value associated with an int/double mapping as a long value, or defaultValue if the
+ * mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Long value
+ */
+ public long getLong(final String key, final long defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : ((Number) value).longValue();
+ }
+
+ /**
+ * Returns the value associated with an int/double mapping as a long value, or 0 if the mapping
+ * does not exist.
+ *
+ * @param key Key to look for.
+ * @return Long value
+ */
+ public long getLong(final String key) {
+ return getLong(key, 0L);
+ }
+
+ private static long[] getLongArray(final Object array) {
+ final int len = Array.getLength(array);
+ final long[] ret = new long[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = ((Number) Array.get(array, i)).longValue();
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the value associated with an int/double array mapping as a long array, or null if the
+ * mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Long array value
+ */
+ public long[] getLongArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0 ? EMPTY_LONG_ARRAY : getLongArray(value);
+ }
+
+ /**
+ * Returns the value associated with a String mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping value is null or mapping does not exist.
+ * @return String value
+ */
+ public String getString(final String key, final String defaultValue) {
+ // If the key maps to null, technically we should return null because the mapping
+ // exists and null is a valid string value. However, people expect the default
+ // value to be returned instead, so we make an exception to return the default value.
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : (String) value;
+ }
+
+ /**
+ * Returns the value associated with a String mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return String value
+ */
+ public String getString(final String key) {
+ return getString(key, null);
+ }
+
+ // The only case where we convert String[] to/from GeckoBundle[] is if every element
+ // is null.
+ private static int getNullArrayLength(final Object array) {
+ final int len = Array.getLength(array);
+ for (int i = 0; i < len; i++) {
+ if (Array.get(array, i) != null) {
+ throw new ClassCastException("Cannot cast array type");
+ }
+ }
+ return len;
+ }
+
+ /**
+ * Returns the value associated with a String array mapping, or null if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return String array value
+ */
+ public String[] getStringArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0
+ ? EMPTY_STRING_ARRAY
+ : !(value instanceof String[])
+ ? new String[getNullArrayLength(value)]
+ : (String[]) value;
+ }
+
+ /*
+ * Returns the value associated with a RectF mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return RectF value
+ */
+ public RectF getRectF(final String key) {
+ final GeckoBundle rectBundle = getBundle(key);
+ if (rectBundle == null) {
+ return null;
+ }
+
+ return new RectF(
+ (float) rectBundle.getDouble("left"),
+ (float) rectBundle.getDouble("top"),
+ (float) rectBundle.getDouble("right"),
+ (float) rectBundle.getDouble("bottom"));
+ }
+
+ /**
+ * Returns the value associated with a Point mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Point value
+ */
+ public Point getPoint(final String key) {
+ final GeckoBundle ptBundle = getBundle(key);
+ if (ptBundle == null) {
+ return null;
+ }
+
+ return new Point(ptBundle.getInt("x"), ptBundle.getInt("y"));
+ }
+
+ /**
+ * Returns the value associated with a PointF mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Point value
+ */
+ public PointF getPointF(final String key) {
+ final GeckoBundle ptBundle = getBundle(key);
+ if (ptBundle == null) {
+ return null;
+ }
+
+ return new PointF((float) ptBundle.getDouble("x"), (float) ptBundle.getDouble("y"));
+ }
+
+ /**
+ * Returns the value associated with a GeckoBundle mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return GeckoBundle value
+ */
+ public GeckoBundle getBundle(final String key) {
+ return (GeckoBundle) mMap.get(key);
+ }
+
+ /**
+ * Returns the value associated with a GeckoBundle array mapping, or null if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return GeckoBundle array value
+ */
+ public GeckoBundle[] getBundleArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0
+ ? EMPTY_BUNDLE_ARRAY
+ : !(value instanceof GeckoBundle[])
+ ? new GeckoBundle[getNullArrayLength(value)]
+ : (GeckoBundle[]) value;
+ }
+
+ /**
+ * Returns whether this GeckoBundle has no mappings.
+ *
+ * @return True if no mapping exists.
+ */
+ public boolean isEmpty() {
+ return mMap.isEmpty();
+ }
+
+ /**
+ * Returns an array of all mapped keys.
+ *
+ * @return String array containing all mapped keys.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public String[] keys() {
+ final int len = mMap.size();
+ final String[] ret = new String[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = mMap.keyAt(i);
+ }
+ return ret;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private Object[] values() {
+ final int len = mMap.size();
+ final Object[] ret = new Object[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = mMap.valueAt(i);
+ }
+ return ret;
+ }
+
+ private void put(final String key, final Object value) {
+ // We intentionally disallow a generic put() method for type safety and sanity. For
+ // example, we assume elsewhere in the code that a value belongs to a small list of
+ // predefined types, and cannot be any arbitrary object. If you want to put an
+ // Object in the bundle, check the type of the Object first and call the
+ // corresponding put methods. For example,
+ //
+ // if (obj instanceof Integer) {
+ // bundle.putInt(key, (Integer) key);
+ // } else if (obj instanceof String) {
+ // bundle.putString(key, (String) obj);
+ // } else {
+ // throw new IllegalArgumentException("unexpected type");
+ // }
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Map a key to a boolean value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBoolean(final String key, final boolean value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a boolean array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBooleanArray(final String key, final boolean[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a boolean array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBooleanArray(final String key, final Boolean[] value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final boolean[] array = new boolean[value.length];
+ for (int i = 0; i < value.length; i++) {
+ array[i] = value[i];
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a boolean array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBooleanArray(final String key, final Collection<Boolean> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final boolean[] array = new boolean[value.size()];
+ int i = 0;
+ for (final Boolean element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a double value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putDouble(final String key, final double value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putDoubleArray(final String key, final double[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putDoubleArray(final String key, final Double[] value) {
+ putDoubleArray(key, Arrays.asList(value));
+ }
+
+ /**
+ * Map a key to a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putDoubleArray(final String key, final Collection<Double> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final double[] array = new double[value.size()];
+ int i = 0;
+ for (final Double element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to an int value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putInt(final String key, final int value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to an int array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putIntArray(final String key, final int[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a int array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putIntArray(final String key, final Integer[] value) {
+ putIntArray(key, Arrays.asList(value));
+ }
+
+ /**
+ * Map a key to a int array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putIntArray(final String key, final Collection<Integer> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final int[] array = new int[value.size()];
+ int i = 0;
+ for (final Integer element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a long value stored as a double value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putLong(final String key, final long value) {
+ mMap.put(key, (double) value);
+ }
+
+ /**
+ * Map a key to a long array value stored as a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putLongArray(final String key, final long[] value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final double[] array = new double[value.length];
+ for (int i = 0; i < value.length; i++) {
+ array[i] = (double) value[i];
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a long array value stored as a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putLongArray(final String key, final Long[] value) {
+ putLongArray(key, Arrays.asList(value));
+ }
+
+ /**
+ * Map a key to a long array value stored as a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putLongArray(final String key, final Collection<Long> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final double[] array = new double[value.size()];
+ int i = 0;
+ for (final Long element : value) {
+ array[i++] = (double) element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a String value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putString(final String key, final String value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a String array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putStringArray(final String key, final String[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a String array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putStringArray(final String key, final Collection<String> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final String[] array = new String[value.size()];
+ int i = 0;
+ for (final String element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a GeckoBundle value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBundle(final String key, final GeckoBundle value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a GeckoBundle array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBundleArray(final String key, final GeckoBundle[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a GeckoBundle array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBundleArray(final String key, final Collection<GeckoBundle> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final GeckoBundle[] array = new GeckoBundle[value.size()];
+ int i = 0;
+ for (final GeckoBundle element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Remove a mapping.
+ *
+ * @param key Key to remove.
+ */
+ public void remove(final String key) {
+ mMap.remove(key);
+ }
+
+ /**
+ * Returns number of mappings in this GeckoBundle.
+ *
+ * @return Number of mappings.
+ */
+ public int size() {
+ return mMap.size();
+ }
+
+ private static Object normalizeValue(final Object value) {
+ if (value instanceof Integer) {
+ // We treat int and double as the same type.
+ return ((Integer) value).doubleValue();
+
+ } else if (value instanceof int[]) {
+ // We treat int[] and double[] as the same type.
+ final int[] array = (int[]) value;
+ return array.length == 0 ? EMPTY_STRING_ARRAY : getDoubleArray(array);
+
+ } else if (value != null && value.getClass().isArray()) {
+ // We treat arrays of all nulls as the same type, including empty arrays.
+ final int len = Array.getLength(value);
+ for (int i = 0; i < len; i++) {
+ if (Array.get(value, i) != null) {
+ return value;
+ }
+ }
+ return len == 0 ? EMPTY_STRING_ARRAY : new String[len];
+ }
+ return value;
+ }
+
+ @Override // Object
+ public boolean equals(final Object other) {
+ if (!(other instanceof GeckoBundle)) {
+ return false;
+ }
+
+ // Support library's SimpleArrayMap.equals is buggy, so roll our own version.
+ final SimpleArrayMap<String, Object> otherMap = ((GeckoBundle) other).mMap;
+ if (mMap == otherMap) {
+ return true;
+ }
+ if (mMap.size() != otherMap.size()) {
+ return false;
+ }
+
+ for (int i = 0; i < mMap.size(); i++) {
+ final String thisKey = mMap.keyAt(i);
+ final int otherKey = otherMap.indexOfKey(thisKey);
+ if (otherKey < 0) {
+ return false;
+ }
+ final Object thisValue = normalizeValue(mMap.valueAt(i));
+ final Object otherValue = normalizeValue(otherMap.valueAt(otherKey));
+ if (thisValue == otherValue) {
+ continue;
+ } else if (thisValue == null || otherValue == null) {
+ return false;
+ }
+
+ final Class<?> thisClass = thisValue.getClass();
+ final Class<?> otherClass = otherValue.getClass();
+ if (thisClass != otherClass && !thisClass.equals(otherClass)) {
+ return false;
+ } else if (!thisClass.isArray()) {
+ if (!thisValue.equals(otherValue)) {
+ return false;
+ }
+ continue;
+ }
+
+ // Work with both primitive arrays and Object arrays, unlike Arrays.equals().
+ final int thisLen = Array.getLength(thisValue);
+ final int otherLen = Array.getLength(otherValue);
+ if (thisLen != otherLen) {
+ return false;
+ }
+ for (int j = 0; j < thisLen; j++) {
+ final Object thisElem = Array.get(thisValue, j);
+ final Object otherElem = Array.get(otherValue, j);
+ if (thisElem != otherElem
+ && (thisElem == null || otherElem == null || !thisElem.equals(otherElem))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override // Object
+ public int hashCode() {
+ return mMap.hashCode();
+ }
+
+ @Override // Object
+ public String toString() {
+ return mMap.toString();
+ }
+
+ public JSONObject toJSONObject() throws JSONException {
+ final JSONObject out = new JSONObject();
+ for (int i = 0; i < mMap.size(); i++) {
+ final Object value = mMap.valueAt(i);
+ final Object jsonValue;
+
+ if (value instanceof GeckoBundle) {
+ jsonValue = ((GeckoBundle) value).toJSONObject();
+ } else if (value instanceof GeckoBundle[]) {
+ final GeckoBundle[] array = (GeckoBundle[]) value;
+ final JSONArray jsonArray = new JSONArray();
+ for (final GeckoBundle element : array) {
+ jsonArray.put(element == null ? JSONObject.NULL : element.toJSONObject());
+ }
+ jsonValue = jsonArray;
+ } else if (Build.VERSION.SDK_INT >= 19) {
+ // gradle task (testWithGeckoBinariesDebugUnitTest) won't use this since that unit test
+ // runs on build task.
+ final Object wrapped = JSONObject.wrap(value);
+ jsonValue = wrapped != null ? wrapped : value.toString();
+ } else if (value == null) {
+ // This is used by UnitTest only
+ jsonValue = JSONObject.NULL;
+ } else if (value.getClass().isArray()) {
+ // This is used by UnitTest only
+ final JSONArray jsonArray = new JSONArray();
+ for (int j = 0; j < Array.getLength(value); j++) {
+ jsonArray.put(Array.get(value, j));
+ }
+ jsonValue = jsonArray;
+ } else {
+ // This is used by UnitTest only
+ jsonValue = value;
+ }
+ out.put(mMap.keyAt(i), jsonValue);
+ }
+ return out;
+ }
+
+ public Bundle toBundle() {
+ final Bundle out = new Bundle(mMap.size());
+ for (int i = 0; i < mMap.size(); i++) {
+ final String key = mMap.keyAt(i);
+ final Object val = mMap.valueAt(i);
+
+ if (val == null) {
+ out.putString(key, null);
+ } else if (val instanceof GeckoBundle) {
+ out.putBundle(key, ((GeckoBundle) val).toBundle());
+ } else if (val instanceof GeckoBundle[]) {
+ final GeckoBundle[] array = (GeckoBundle[]) val;
+ final Parcelable[] parcelables = new Parcelable[array.length];
+ for (int j = 0; j < array.length; j++) {
+ if (array[j] != null) {
+ parcelables[j] = array[j].toBundle();
+ }
+ }
+ out.putParcelableArray(key, parcelables);
+ } else if (val instanceof Boolean) {
+ out.putBoolean(key, (Boolean) val);
+ } else if (val instanceof boolean[]) {
+ out.putBooleanArray(key, (boolean[]) val);
+ } else if (val instanceof Byte || val instanceof Short || val instanceof Integer) {
+ out.putInt(key, ((Number) val).intValue());
+ } else if (val instanceof int[]) {
+ out.putIntArray(key, (int[]) val);
+ } else if (val instanceof Float || val instanceof Double || val instanceof Long) {
+ out.putDouble(key, ((Number) val).doubleValue());
+ } else if (val instanceof double[]) {
+ out.putDoubleArray(key, (double[]) val);
+ } else if (val instanceof CharSequence || val instanceof Character) {
+ out.putString(key, val.toString());
+ } else if (val instanceof String[]) {
+ out.putStringArray(key, (String[]) val);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ return out;
+ }
+
+ public static GeckoBundle fromBundle(final Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+
+ final String[] keys = new String[bundle.size()];
+ final Object[] values = new Object[bundle.size()];
+ int i = 0;
+
+ for (final String key : bundle.keySet()) {
+ final Object value = bundle.get(key);
+ keys[i] = key;
+
+ if (value instanceof Bundle || value == null) {
+ values[i] = fromBundle((Bundle) value);
+ } else if (value instanceof Parcelable[]) {
+ final Parcelable[] array = (Parcelable[]) value;
+ final GeckoBundle[] out = new GeckoBundle[array.length];
+ for (int j = 0; j < array.length; j++) {
+ out[j] = fromBundle((Bundle) array[j]);
+ }
+ values[i] = out;
+ } else if (value instanceof Boolean
+ || value instanceof Integer
+ || value instanceof Double
+ || value instanceof String
+ || value instanceof boolean[]
+ || value instanceof int[]
+ || value instanceof double[]
+ || value instanceof String[]) {
+ values[i] = value;
+ } else if (value instanceof Byte || value instanceof Short) {
+ values[i] = ((Number) value).intValue();
+ } else if (value instanceof Float || value instanceof Long) {
+ values[i] = ((Number) value).doubleValue();
+ } else if (value instanceof CharSequence || value instanceof Character) {
+ values[i] = value.toString();
+ } else {
+ throw new UnsupportedOperationException();
+ }
+
+ i++;
+ }
+ return new GeckoBundle(keys, values);
+ }
+
+ private static Object fromJSONValue(final Object value) throws JSONException {
+ if (value == null || value == JSONObject.NULL) {
+ return null;
+ } else if (value instanceof JSONObject) {
+ return fromJSONObject((JSONObject) value);
+ }
+ if (value instanceof JSONArray) {
+ final JSONArray array = (JSONArray) value;
+ final int len = array.length();
+ if (len == 0) {
+ return EMPTY_BOOLEAN_ARRAY;
+ }
+ Object out = null;
+ for (int i = 0; i < len; i++) {
+ final Object element = fromJSONValue(array.opt(i));
+ if (element == null) {
+ continue;
+ }
+ if (out == null) {
+ Class<?> type = element.getClass();
+ if (type == Boolean.class) {
+ type = boolean.class;
+ } else if (type == Integer.class) {
+ type = int.class;
+ } else if (type == Double.class) {
+ type = double.class;
+ }
+ out = Array.newInstance(type, len);
+ }
+ Array.set(out, i, element);
+ }
+ if (out == null) {
+ // Treat all-null arrays as String arrays.
+ return new String[len];
+ }
+ return out;
+ }
+ if (value instanceof Boolean
+ || value instanceof Integer
+ || value instanceof Double
+ || value instanceof String) {
+ return value;
+ }
+ if (value instanceof Byte || value instanceof Short) {
+ return ((Number) value).intValue();
+ }
+ if (value instanceof Float || value instanceof Long) {
+ return ((Number) value).doubleValue();
+ }
+ return value.toString();
+ }
+
+ public static GeckoBundle fromJSONObject(final JSONObject obj) throws JSONException {
+ if (obj == null || obj == JSONObject.NULL) {
+ return null;
+ }
+
+ final String[] keys = new String[obj.length()];
+ final Object[] values = new Object[obj.length()];
+
+ final Iterator<String> iter = obj.keys();
+ for (int i = 0; iter.hasNext(); i++) {
+ final String key = iter.next();
+ keys[i] = key;
+ values[i] = fromJSONValue(obj.opt(key));
+ }
+ return new GeckoBundle(keys, values);
+ }
+
+ @Override // Parcelable
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override // Parcelable
+ public void writeToParcel(final Parcel dest, final int flags) {
+ final int len = mMap.size();
+ dest.writeInt(len);
+
+ for (int i = 0; i < len; i++) {
+ dest.writeString(mMap.keyAt(i));
+ dest.writeValue(mMap.valueAt(i));
+ }
+ }
+
+ // AIDL code may call readFromParcel even though it's not part of Parcelable.
+ public void readFromParcel(final Parcel source) {
+ final ClassLoader loader = getClass().getClassLoader();
+ final int len = source.readInt();
+ mMap.clear();
+ mMap.ensureCapacity(len);
+
+ for (int i = 0; i < len; i++) {
+ final String key = source.readString();
+ Object val = source.readValue(loader);
+
+ if (val instanceof Parcelable[]) {
+ final Parcelable[] array = (Parcelable[]) val;
+ val = Arrays.copyOf(array, array.length, GeckoBundle[].class);
+ }
+
+ mMap.put(key, val);
+ }
+ }
+
+ public static final Parcelable.Creator<GeckoBundle> CREATOR =
+ new Parcelable.Creator<GeckoBundle>() {
+ @Override
+ public GeckoBundle createFromParcel(final Parcel source) {
+ final GeckoBundle bundle = new GeckoBundle(0);
+ bundle.readFromParcel(source);
+ return bundle;
+ }
+
+ @Override
+ public GeckoBundle[] newArray(final int size) {
+ return new GeckoBundle[size];
+ }
+ };
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
new file mode 100644
index 0000000000..ccfce796bd
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
@@ -0,0 +1,389 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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.util;
+
+import android.annotation.SuppressLint;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.os.Build;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public final class HardwareCodecCapabilityUtils {
+ private static final String LOGTAG = "HardwareCodecCapability";
+
+ // List of supported HW VP8 encoders.
+ private static final String[] supportedVp8HwEncCodecPrefixes = {"OMX.qcom.", "OMX.Intel."};
+ // List of supported HW VP8 decoders.
+ private static final String[] supportedVp8HwDecCodecPrefixes = {
+ "OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "c2.exynos", "OMX.Intel."
+ };
+ private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8";
+ // List of supported HW VP9 codecs.
+ private static final String[] supportedVp9HwCodecPrefixes = {
+ "OMX.qcom.", "OMX.Exynos.", "c2.exynos"
+ };
+ private static final String VP9_MIME_TYPE = "video/x-vnd.on2.vp9";
+ // List of supported HW H.264 codecs.
+ private static final String[] supportedH264HwCodecPrefixes = {
+ "OMX.qcom.",
+ "OMX.Intel.",
+ "OMX.Exynos.",
+ "c2.exynos",
+ "OMX.Nvidia",
+ "OMX.SEC.",
+ "OMX.IMG.",
+ "OMX.k3.",
+ "OMX.hisi.",
+ "OMX.TI.",
+ "OMX.MTK."
+ };
+ private static final String H264_MIME_TYPE = "video/avc";
+ // NV12 color format supported by QCOM codec, but not declared in MediaCodec -
+ // see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h
+ private static final int COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04;
+ // Allowable color formats supported by codec - in order of preference.
+ private static final int[] supportedColorList = {
+ CodecCapabilities.COLOR_FormatYUV420Planar,
+ CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
+ CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
+ COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m
+ };
+ private static final int COLOR_FORMAT_NOT_SUPPORTED = -1;
+ private static final String[] adaptivePlaybackBlacklist = {
+ "GT-I9300", // S3 (I9300 / I9300I)
+ "SCH-I535", // S3
+ "SGH-T999", // S3 (T-Mobile)
+ "SAMSUNG-SGH-T999", // S3 (T-Mobile)
+ "SGH-M919", // S4
+ "GT-I9505", // S4
+ "GT-I9515", // S4
+ "SCH-R970", // S4
+ "SGH-I337", // S4
+ "SPH-L720", // S4 (Sprint)
+ "SAMSUNG-SGH-I337", // S4
+ "GT-I9195", // S4 Mini
+ "300E5EV/300E4EV/270E5EV/270E4EV/2470EV/2470EE",
+ "LG-D605" // LG Optimus L9 II
+ };
+
+ private static MediaCodecInfo[] getCodecListWithOldAPI() {
+ int numCodecs = 0;
+ try {
+ numCodecs = MediaCodecList.getCodecCount();
+ } catch (final RuntimeException e) {
+ Log.e(LOGTAG, "Failed to retrieve media codec count", e);
+ return new MediaCodecInfo[numCodecs];
+ }
+
+ final MediaCodecInfo[] codecList = new MediaCodecInfo[numCodecs];
+
+ for (int i = 0; i < numCodecs; ++i) {
+ final MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ codecList[i] = info;
+ }
+
+ return codecList;
+ }
+
+ // Return list of all codecs (decode + encode).
+ private static MediaCodecInfo[] getCodecList() {
+ final MediaCodecInfo[] codecList;
+ try {
+ final MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+ codecList = list.getCodecInfos();
+ } catch (final RuntimeException e) {
+ Log.e(LOGTAG, "Failed to retrieve media codec support list", e);
+ return new MediaCodecInfo[0];
+ }
+ return codecList;
+ }
+
+ // Return list of all decoders.
+ private static MediaCodecInfo[] getDecoderInfos() {
+ final ArrayList<MediaCodecInfo> decoderList = new ArrayList<MediaCodecInfo>();
+ for (final MediaCodecInfo info : getCodecList()) {
+ if (!info.isEncoder()) {
+ decoderList.add(info);
+ }
+ }
+ return decoderList.toArray(new MediaCodecInfo[0]);
+ }
+
+ // Return list of all encoders.
+ private static MediaCodecInfo[] getEncoderInfos() {
+ final ArrayList<MediaCodecInfo> encoderList = new ArrayList<MediaCodecInfo>();
+ for (final MediaCodecInfo info : getCodecList()) {
+ if (info.isEncoder()) {
+ encoderList.add(info);
+ }
+ }
+ return encoderList.toArray(new MediaCodecInfo[0]);
+ }
+
+ // Return list of all decoder-supported MIME types without distinguishing
+ // between SW/HW support.
+ @WrapForJNI
+ public static String[] getDecoderSupportedMimeTypes() {
+ final Set<String> mimeTypes = new HashSet<>();
+ for (final MediaCodecInfo info : getDecoderInfos()) {
+ mimeTypes.addAll(Arrays.asList(info.getSupportedTypes()));
+ }
+ return mimeTypes.toArray(new String[0]);
+ }
+
+ // Return list of all decoder-supported MIME types, each prefixed with
+ // either SW or HW indicating software or hardware support.
+ @WrapForJNI
+ public static String[] getDecoderSupportedMimeTypesWithAccelInfo() {
+ final Set<String> mimeTypes = new HashSet<>();
+ final String[] hwPrefixes = getAllSupportedHWCodecPrefixes(false);
+
+ for (final MediaCodecInfo info : getDecoderInfos()) {
+ final String[] supportedTypes = info.getSupportedTypes();
+ for (final String mimeType : info.getSupportedTypes()) {
+ boolean isHwPrefix = false;
+ for (final String prefix : hwPrefixes) {
+ if (info.getName().startsWith(prefix)) {
+ isHwPrefix = true;
+ break;
+ }
+ }
+ if (!isHwPrefix) {
+ mimeTypes.add("SW " + mimeType);
+ continue;
+ }
+ final CodecCapabilities caps = info.getCapabilitiesForType(mimeType);
+ if (getSupportsYUV420orNV12(caps) != COLOR_FORMAT_NOT_SUPPORTED) {
+ mimeTypes.add("HW " + mimeType);
+ }
+ }
+ }
+ for (final String typeit : mimeTypes) {
+ Log.d(LOGTAG, "MIME support: " + typeit);
+ }
+ return mimeTypes.toArray(new String[0]);
+ }
+
+ public static boolean checkSupportsAdaptivePlayback(
+ final MediaCodec aCodec, final String aMimeType) {
+ if (isAdaptivePlaybackBlacklisted(aMimeType)) {
+ return false;
+ }
+
+ try {
+ final MediaCodecInfo info = aCodec.getCodecInfo();
+ final MediaCodecInfo.CodecCapabilities capabilities = info.getCapabilitiesForType(aMimeType);
+ return capabilities != null
+ && capabilities.isFeatureSupported(
+ MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback);
+ } catch (final IllegalArgumentException e) {
+ Log.e(LOGTAG, "Retrieve codec information failed", e);
+ }
+ return false;
+ }
+
+ // See Bug1360626 and
+ // https://codereview.chromium.org/1869103002 for details.
+ private static boolean isAdaptivePlaybackBlacklisted(final String aMimeType) {
+ Log.d(LOGTAG, "The device ModelID is " + Build.MODEL);
+ if (!aMimeType.equals("video/avc") && !aMimeType.equals("video/avc1")) {
+ return false;
+ }
+
+ if (!Build.MANUFACTURER.toLowerCase(Locale.ROOT).equals("samsung")) {
+ return false;
+ }
+
+ for (final String model : adaptivePlaybackBlacklist) {
+ if (Build.MODEL.startsWith(model)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Check if a given MIME Type has HW decode or encode support.
+ public static boolean getHWCodecCapability(final String aMimeType, final boolean aIsEncoder) {
+ for (final MediaCodecInfo info : getCodecList()) {
+ if (info.isEncoder() != aIsEncoder) {
+ continue;
+ }
+ String name = null;
+ for (final String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(aMimeType)) {
+ name = info.getName();
+ break;
+ }
+ }
+ if (name == null) {
+ continue; // No HW support in this codec; try the next one.
+ }
+ Log.d(LOGTAG, "Found candidate" + (aIsEncoder ? " encoder " : " decoder ") + name);
+
+ // Check if this is supported codec.
+ final String[] hwList = getSupportedHWCodecPrefixes(aMimeType, aIsEncoder);
+ if (hwList == null) {
+ continue;
+ }
+ boolean supportedCodec = false;
+ for (final String codecPrefix : hwList) {
+ if (name.startsWith(codecPrefix)) {
+ supportedCodec = true;
+ break;
+ }
+ }
+ if (!supportedCodec) {
+ continue;
+ }
+
+ // Check if codec supports either yuv420 or nv12.
+ final CodecCapabilities capabilities = info.getCapabilitiesForType(aMimeType);
+ for (final int colorFormat : capabilities.colorFormats) {
+ Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
+ }
+ if (Build.VERSION.SDK_INT >= 24) {
+ for (final MediaCodecInfo.CodecProfileLevel pl : capabilities.profileLevels) {
+ Log.v(
+ LOGTAG,
+ " Profile: 0x"
+ + Integer.toHexString(pl.profile)
+ + "/Level=0x"
+ + Integer.toHexString(pl.level));
+ }
+ }
+ final int codecColorFormat = getSupportsYUV420orNV12(capabilities);
+ if (codecColorFormat != COLOR_FORMAT_NOT_SUPPORTED) {
+ Log.d(
+ LOGTAG,
+ "Found target"
+ + (aIsEncoder ? " encoder " : " decoder ")
+ + name
+ + ". Color: 0x"
+ + Integer.toHexString(codecColorFormat));
+ return true;
+ }
+ }
+ // No HW codec.
+ return false;
+ }
+
+ // Check if codec supports YUV420 or NV12
+ private static int getSupportsYUV420orNV12(final CodecCapabilities aCodecCaps) {
+ for (final int supportedColorFormat : supportedColorList) {
+ for (final int codecColorFormat : aCodecCaps.colorFormats) {
+ if (codecColorFormat == supportedColorFormat) {
+ return codecColorFormat;
+ }
+ }
+ }
+ return COLOR_FORMAT_NOT_SUPPORTED;
+ }
+
+ // Check if MIME type string has HW prefix (encode or decode, VP8, VP9, and H264)
+ private static String[] getSupportedHWCodecPrefixes(
+ final String aMimeType, final boolean aIsEncoder) {
+ if (aMimeType.equals(H264_MIME_TYPE)) {
+ return supportedH264HwCodecPrefixes;
+ }
+ if (aMimeType.equals(VP9_MIME_TYPE)) {
+ return supportedVp9HwCodecPrefixes;
+ }
+ if (aMimeType.equals(VP8_MIME_TYPE)) {
+ return aIsEncoder ? supportedVp8HwEncCodecPrefixes : supportedVp8HwDecCodecPrefixes;
+ }
+ return null;
+ }
+
+ // Return list of HW codec prefixes (encode or decode, VP8, VP9, and H264)
+ private static String[] getAllSupportedHWCodecPrefixes(final boolean aIsEncoder) {
+ final Set<String> prefixes = new HashSet<>();
+ final String[] mimeTypes = {H264_MIME_TYPE, VP8_MIME_TYPE, VP9_MIME_TYPE};
+ for (final String mt : mimeTypes) {
+ prefixes.addAll(Arrays.asList(getSupportedHWCodecPrefixes(mt, aIsEncoder)));
+ }
+ return prefixes.toArray(new String[0]);
+ }
+
+ @WrapForJNI
+ public static boolean hasHWVP8(final boolean aIsEncoder) {
+ return getHWCodecCapability(VP8_MIME_TYPE, aIsEncoder);
+ }
+
+ @WrapForJNI
+ public static boolean hasHWVP9(final boolean aIsEncoder) {
+ return getHWCodecCapability(VP9_MIME_TYPE, aIsEncoder);
+ }
+
+ @WrapForJNI
+ public static boolean hasHWH264(final boolean aIsEncoder) {
+ return getHWCodecCapability(H264_MIME_TYPE, aIsEncoder);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static boolean hasHWH264() {
+ return getHWCodecCapability(H264_MIME_TYPE, true)
+ && getHWCodecCapability(H264_MIME_TYPE, false);
+ }
+
+ @WrapForJNI
+ @SuppressLint("NewApi")
+ public static boolean decodes10Bit(final String aMimeType) {
+ if (Build.VERSION.SDK_INT < 24) {
+ // Be conservative when we cannot get supported profile.
+ return false;
+ }
+
+ final MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+ for (final MediaCodecInfo info : codecs.getCodecInfos()) {
+ if (info.isEncoder()) {
+ continue;
+ }
+ try {
+ for (final MediaCodecInfo.CodecProfileLevel pl :
+ info.getCapabilitiesForType(aMimeType).profileLevels) {
+ if ((aMimeType.equals(H264_MIME_TYPE)
+ && pl.profile == MediaCodecInfo.CodecProfileLevel.AVCProfileHigh10)
+ || (aMimeType.equals(VP9_MIME_TYPE) && is10BitVP9Profile(pl.profile))) {
+ return true;
+ }
+ }
+ } catch (final IllegalArgumentException e) {
+ // Type not supported.
+ continue;
+ }
+ }
+
+ return false;
+ }
+
+ @SuppressLint("NewApi")
+ private static boolean is10BitVP9Profile(final int profile) {
+ if (Build.VERSION.SDK_INT < 24) {
+ // Be conservative when we cannot get supported profile.
+ return false;
+ }
+
+ if ((profile == MediaCodecInfo.CodecProfileLevel.VP9Profile2)
+ || (profile == MediaCodecInfo.CodecProfileLevel.VP9Profile3)
+ || (profile == MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR)
+ || (profile == MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR)) {
+ return true;
+ }
+
+ return Build.VERSION.SDK_INT >= 29
+ && ((profile == MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR10Plus)
+ || (profile == MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR10Plus));
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
new file mode 100644
index 0000000000..bab64b92d4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
@@ -0,0 +1,46 @@
+/* -*- 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.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+
+public final class HardwareUtils {
+ private static final String LOGTAG = "GeckoHardwareUtils";
+
+ private static volatile boolean sInited;
+
+ // These are all set once, during init.
+ private static volatile boolean sIsLargeTablet;
+ private static volatile boolean sIsSmallTablet;
+
+ private HardwareUtils() {}
+
+ public static synchronized void init(final Context context) {
+ if (sInited) {
+ return;
+ }
+
+ // Pre-populate common flags from the context.
+ final int screenLayoutSize =
+ context.getResources().getConfiguration().screenLayout
+ & Configuration.SCREENLAYOUT_SIZE_MASK;
+ if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+ sIsLargeTablet = true;
+ } else if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_LARGE) {
+ sIsSmallTablet = true;
+ }
+
+ sInited = true;
+ }
+
+ public static boolean isTablet(final Context context) {
+ if (!sInited) {
+ init(context);
+ }
+ return sIsLargeTablet || sIsSmallTablet;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IXPCOMEventTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IXPCOMEventTarget.java
new file mode 100644
index 0000000000..9f42d9bd85
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IXPCOMEventTarget.java
@@ -0,0 +1,12 @@
+/* -*- 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.util;
+
+import java.util.concurrent.Executor;
+
+public interface IXPCOMEventTarget extends Executor {
+ boolean isOnCurrentThread();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageDecoder.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageDecoder.java
new file mode 100644
index 0000000000..4ab330f182
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageDecoder.java
@@ -0,0 +1,88 @@
+/* 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.util;
+
+import android.graphics.Bitmap;
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.geckoview.GeckoResult;
+
+/** Provides access to Gecko's Image processing library. */
+@AnyThread
+public class ImageDecoder {
+ private static ImageDecoder instance;
+
+ private ImageDecoder() {}
+
+ public static ImageDecoder instance() {
+ if (instance == null) {
+ instance = new ImageDecoder();
+ }
+
+ return instance;
+ }
+
+ @WrapForJNI(dispatchTo = "gecko", stubName = "Decode")
+ private static native void nativeDecode(
+ final String uri, final int desiredLength, GeckoResult<Bitmap> result);
+
+ /**
+ * Fetches and decodes an image at the specified location. This method supports SVG, PNG, Bitmap
+ * and other formats supported by Gecko.
+ *
+ * @param uri location of the image. Can be either a remote https:// location, file:/// if the
+ * file is local or a resource://android/ if the file is located inside the APK.
+ * <p>e.g. if the image file is locate at /assets/test.png inside the apk, set the uri to
+ * resource://android/assets/test.png.
+ * @return A {@link GeckoResult} to the decoded image.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> decode(final @NonNull String uri) {
+ return decode(uri, 0);
+ }
+
+ /**
+ * Fetches and decodes an image at the specified location and resizes it to the desired length.
+ * This method supports SVG, PNG, Bitmap and other formats supported by Gecko.
+ *
+ * <p>Note: The final size might differ slightly from the requested output.
+ *
+ * @param uri location of the image. Can be either a remote https:// location, file:/// if the
+ * file is local or a resource://android/ if the file is located inside the APK.
+ * <p>e.g. if the image file is locate at /assets/test.png inside the apk, set the uri to
+ * resource://android/assets/test.png.
+ * @param desiredLength Longest size for the image in device pixel units. The resulting image
+ * might be slightly different if the image cannot be resized efficiently. If desiredLength is
+ * 0 then the image will be decoded to its natural size.
+ * @return A {@link GeckoResult} to the decoded image.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> decode(final @NonNull String uri, final int desiredLength) {
+ if (uri == null) {
+ throw new IllegalArgumentException("Uri cannot be null");
+ }
+
+ final GeckoResult<Bitmap> result = new GeckoResult<>();
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeDecode(uri, desiredLength, result);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY,
+ this,
+ "nativeDecode",
+ String.class,
+ uri,
+ int.class,
+ desiredLength,
+ GeckoResult.class,
+ result);
+ }
+
+ return result;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java
new file mode 100644
index 0000000000..d155ea951e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java
@@ -0,0 +1,334 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * 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.util;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import org.mozilla.geckoview.GeckoResult;
+
+/**
+ * Represents an Web API image resource as used in web app manifests and media session metadata.
+ *
+ * @see <a href="https://www.w3.org/TR/image-resource">Image Resource</a>
+ */
+@AnyThread
+public class ImageResource {
+ private static final String LOGTAG = "ImageResource";
+ private static final boolean DEBUG = false;
+
+ /** Represents the size of an image resource option. */
+ public static class Size {
+ /** The width in pixels. */
+ public final int width;
+
+ /** The height in pixels. */
+ public final int height;
+
+ /**
+ * Size contructor.
+ *
+ * @param width The width in pixels.
+ * @param height The height in pixels.
+ */
+ public Size(final int width, final int height) {
+ this.width = width;
+ this.height = height;
+ }
+ }
+
+ /** The URI of the image resource. */
+ public final @NonNull String src;
+
+ /** The MIME type of the image resource. */
+ public final @Nullable String type;
+
+ /** A {@link Size} array of supported images sizes. */
+ public final @Nullable Size[] sizes;
+
+ /**
+ * ImageResource constructor.
+ *
+ * @param src The URI string of the image resource.
+ * @param type The MIME type of the image resource.
+ * @param sizes The supported images {@link Size} array.
+ */
+ public ImageResource(
+ final @NonNull String src, final @Nullable String type, final @Nullable Size[] sizes) {
+ this.src = src;
+ this.type = type != null ? type.toLowerCase(Locale.ROOT) : null;
+ this.sizes = sizes;
+ }
+
+ /**
+ * ImageResource constructor.
+ *
+ * @param src The URI string of the image resource.
+ * @param type The MIME type of the image resource.
+ * @param sizes The supported images sizes string.
+ * @see <a href="https://html.spec.whatwg.org/multipage/semantics.html#dom-link-sizes">Attribute
+ * spec for sizes</a>
+ */
+ public ImageResource(
+ final @NonNull String src, final @Nullable String type, final @Nullable String sizes) {
+ this(src, type, parseSizes(sizes));
+ }
+
+ private static @Nullable Size[] parseSizes(final @Nullable String sizesStr) {
+ if (sizesStr == null || sizesStr.isEmpty()) {
+ return null;
+ }
+
+ final String[] sizesStrs = sizesStr.toLowerCase(Locale.ROOT).split(" ");
+ final List<Size> sizes = new ArrayList<Size>();
+
+ for (final String sizeStr : sizesStrs) {
+ if (sizesStr.equals("any")) {
+ // 0-width size will always be favored.
+ sizes.add(new Size(0, 0));
+ continue;
+ }
+ final String[] widthHeight = sizeStr.split("x");
+ if (widthHeight.length != 2) {
+ // Not spec-compliant size.
+ continue;
+ }
+ try {
+ sizes.add(new Size(Integer.valueOf(widthHeight[0]), Integer.valueOf(widthHeight[1])));
+ } catch (final NumberFormatException e) {
+ Log.e(LOGTAG, "Invalid image resource size", e);
+ }
+ }
+ if (sizes.isEmpty()) {
+ return null;
+ }
+ return sizes.toArray(new Size[0]);
+ }
+
+ public static @NonNull ImageResource fromBundle(final GeckoBundle bundle) {
+ return new ImageResource(
+ bundle.getString("src"), bundle.getString("type"), bundle.getString("sizes"));
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("ImageResource {");
+ builder
+ .append("src=")
+ .append(src)
+ .append("type=")
+ .append(type)
+ .append("sizes=")
+ .append(sizes)
+ .append("}");
+ return builder.toString();
+ }
+
+ /**
+ * Get the best version of this image for size <code>size</code>. Embedders are encouraged to
+ * cache the result of this method keyed with this instance.
+ *
+ * @param size pixel size at which this image will be displayed at.
+ * @return A {@link GeckoResult} that resolves to the bitmap when ready.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> getBitmap(final int size) {
+ return ImageDecoder.instance().decode(src, size);
+ }
+
+ /**
+ * Represents a collection of {@link ImageResource} options. Image resources are often used in a
+ * collection to provide multiple image options for various sizes. This data structure can be used
+ * to retrieve the best image resource for any given target image size.
+ */
+ public static class Collection {
+ private static class SizeIndexPair {
+ public final int width;
+ public final int idx;
+
+ public SizeIndexPair(final int width, final int idx) {
+ this.width = width;
+ this.idx = idx;
+ }
+ }
+
+ // The individual image resources, usually each with a unique src.
+ private final List<ImageResource> mImages;
+
+ // A sorted size-index list. The list is sorted based on the supported
+ // sizes of the images in ascending order.
+ private final List<SizeIndexPair> mSizeIndex;
+
+ /* package */ Collection() {
+ mImages = new ArrayList<>();
+ mSizeIndex = new ArrayList<>();
+ }
+
+ /** Builder class for the construction of a {@link Collection}. */
+ public static class Builder {
+ final Collection mCollection;
+
+ public Builder() {
+ mCollection = new Collection();
+ }
+
+ /**
+ * Add an image resource to the collection.
+ *
+ * @param image The {@link ImageResource} to be added.
+ * @return This builder instance.
+ */
+ public @NonNull Builder add(final ImageResource image) {
+ final int index = mCollection.mImages.size();
+
+ if (image.sizes == null) {
+ // Null-sizes are handled the same as `any`.
+ mCollection.mSizeIndex.add(new SizeIndexPair(0, index));
+ } else {
+ for (final Size size : image.sizes) {
+ mCollection.mSizeIndex.add(new SizeIndexPair(size.width, index));
+ }
+ }
+ mCollection.mImages.add(image);
+ return this;
+ }
+
+ /**
+ * Finalize the collection.
+ *
+ * @return The final collection.
+ */
+ public @NonNull Collection build() {
+ Collections.sort(mCollection.mSizeIndex, (a, b) -> Integer.compare(a.width, b.width));
+ return mCollection;
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("ImageResource.Collection {");
+ builder.append("images=[");
+
+ for (final ImageResource image : mImages) {
+ builder.append(image).append(", ");
+ }
+ builder.append("]}");
+ return builder.toString();
+ }
+
+ /**
+ * Returns the best suited {@link ImageResource} for the given size. This is usually determined
+ * based on the minimal difference between the given size and one of the supported widths of an
+ * image resource.
+ *
+ * @param size The target size for the image in pixels.
+ * @return The best {@link ImageResource} for the given size from this collection.
+ */
+ public @Nullable ImageResource getBest(final int size) {
+ if (mSizeIndex.isEmpty()) {
+ return null;
+ }
+ int bestMatchIdx = mSizeIndex.get(0).idx;
+ int lastDiff = size;
+ for (final SizeIndexPair sizeIndex : mSizeIndex) {
+ final int diff = Math.abs(sizeIndex.width - size);
+ if (lastDiff <= diff) {
+ // With increasing widths, the difference can only grow now.
+ // 0-width means "any", so we're finished at the first
+ // entry.
+ break;
+ }
+ lastDiff = diff;
+ bestMatchIdx = sizeIndex.idx;
+ }
+ return mImages.get(bestMatchIdx);
+ }
+
+ /**
+ * Get the best version of this image for size <code>size</code>. Embedders are encouraged to
+ * cache the result of this method keyed with this instance.
+ *
+ * @param size pixel size at which this image will be displayed at.
+ * @return A {@link GeckoResult} that resolves to the bitmap when ready.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> getBitmap(final int size) {
+ final ImageResource image = getBest(size);
+ if (image == null) {
+ return GeckoResult.fromValue(null);
+ }
+ return image.getBitmap(size);
+ }
+
+ public static Collection fromSizeSrcBundle(final GeckoBundle bundle) {
+ final Builder builder = new Builder();
+
+ for (final String key : bundle.keys()) {
+ final Integer intKey = Integer.valueOf(key);
+ if (intKey == null) {
+ Log.e(LOGTAG, "Non-integer image key: " + intKey);
+
+ if (DEBUG) {
+ throw new RuntimeException("Non-integer image key: " + key);
+ }
+ continue;
+ }
+
+ final String src = getImageValue(bundle.get(key));
+ if (src != null) {
+ // Given the bundle structure, we don't have insight on
+ // individual image resources so we have to create an
+ // instance for each size entry.
+ final ImageResource image =
+ new ImageResource(src, null, new Size[] {new Size(intKey, intKey)});
+ builder.add(image);
+ }
+ }
+ return builder.build();
+ }
+
+ private static String getImageValue(final Object value) {
+ // The image value can either be an object containing images for
+ // each theme...
+ if (value instanceof GeckoBundle) {
+ // We don't support theme_images yet, so let's just return the
+ // default value.
+ final GeckoBundle themeImages = (GeckoBundle) value;
+ final Object defaultImages = themeImages.get("default");
+
+ if (!(defaultImages instanceof String)) {
+ if (DEBUG) {
+ throw new RuntimeException("Unexpected themed_icon value.");
+ }
+ Log.e(LOGTAG, "Unexpected themed_icon value.");
+ return null;
+ }
+
+ return (String) defaultImages;
+ }
+
+ // ... or just a URL.
+ if (value instanceof String) {
+ return (String) value;
+ }
+
+ // We never expect it to be something else, so let's error out here.
+ if (DEBUG) {
+ throw new RuntimeException("Unexpected image value: " + value);
+ }
+
+ Log.e(LOGTAG, "Unexpected image value.");
+ return null;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputDeviceUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputDeviceUtils.java
new file mode 100644
index 0000000000..e0a0d924a9
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputDeviceUtils.java
@@ -0,0 +1,20 @@
+/* -*- 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.util;
+
+import android.view.InputDevice;
+
+public class InputDeviceUtils {
+ public static boolean isPointerTypeDevice(final InputDevice inputDevice) {
+ final int sources = inputDevice.getSources();
+ return (sources
+ & (InputDevice.SOURCE_CLASS_JOYSTICK
+ | InputDevice.SOURCE_CLASS_POINTER
+ | InputDevice.SOURCE_CLASS_POSITION
+ | InputDevice.SOURCE_CLASS_TRACKBALL))
+ != 0;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
new file mode 100644
index 0000000000..36fde18a02
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
@@ -0,0 +1,116 @@
+/*
+ * 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.util;
+
+import android.content.Intent;
+import android.net.Uri;
+import java.net.URISyntaxException;
+import java.util.Locale;
+
+/** Utilities for Intents. */
+public class IntentUtils {
+ private IntentUtils() {}
+
+ /**
+ * Return a Uri instance which is equivalent to uri, but with a guaranteed-lowercase scheme as if
+ * the API level 16 method Uri.normalizeScheme had been called.
+ *
+ * @param uri The URI string to normalize.
+ * @return The corresponding normalized Uri.
+ */
+ private static Uri normalizeUriScheme(final Uri uri) {
+ final String scheme = uri.getScheme();
+ if (scheme == null) {
+ return uri;
+ }
+ final String lower = scheme.toLowerCase(Locale.ROOT);
+ if (lower.equals(scheme)) {
+ return uri;
+ }
+
+ // Otherwise, return a new URI with a normalized scheme.
+ return uri.buildUpon().scheme(lower).build();
+ }
+
+ /**
+ * Return a normalized Uri instance that corresponds to the given URI string with cross-API-level
+ * compatibility.
+ *
+ * @param aUri The URI string to normalize.
+ * @return The corresponding normalized Uri.
+ */
+ public static Uri normalizeUri(final String aUri) {
+ return normalizeUriScheme(
+ aUri.indexOf(':') >= 0 ? Uri.parse(aUri) : new Uri.Builder().scheme(aUri).build());
+ }
+
+ public static boolean isUriSafeForScheme(final String aUri) {
+ return isUriSafeForScheme(normalizeUri(aUri));
+ }
+
+ /**
+ * Verify whether the given URI is considered safe to load in respect to its scheme. Unsafe URIs
+ * should be blocked from further handling.
+ *
+ * @param aUri The URI instance to test.
+ * @return Whether the provided URI is considered safe in respect to its scheme.
+ */
+ public static boolean isUriSafeForScheme(final Uri aUri) {
+ final String scheme = aUri.getScheme();
+ if ("tel".equals(scheme) || "sms".equals(scheme)) {
+ // Bug 794034 - We don't want to pass MWI or USSD codes to the
+ // dialer, and ensure the Uri class doesn't parse a URI
+ // containing a fragment ('#')
+ final String number = aUri.getSchemeSpecificPart();
+ if (number.contains("#") || number.contains("*") || aUri.getFragment() != null) {
+ return false;
+ }
+ }
+
+ if (("intent".equals(scheme) || "android-app".equals(scheme))) {
+ // Bug 1356893 - Rject intents with file data schemes.
+ return getSafeIntent(aUri) != null;
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a safe intent for the given URI. Intents with file data schemes are considered unsafe.
+ *
+ * @param aUri The URI for the intent.
+ * @return A safe intent for the given URI or null if URI is considered unsafe.
+ */
+ public static Intent getSafeIntent(final Uri aUri) {
+ final Intent intent;
+ try {
+ intent = Intent.parseUri(aUri.toString(), 0);
+ } catch (final URISyntaxException e) {
+ return null;
+ }
+
+ final Uri data = intent.getData();
+ if (data != null && "file".equals(normalizeUriScheme(data).getScheme())) {
+ return null;
+ }
+
+ // Only open applications which can accept arbitrary data from a browser.
+ intent.addCategory(Intent.CATEGORY_BROWSABLE);
+
+ // Prevent site from explicitly opening our internal activities,
+ // which can leak data.
+ intent.setComponent(null);
+ nullIntentSelector(intent);
+
+ return intent;
+ }
+
+ // We create a separate method to better encapsulate the @TargetApi use.
+ private static void nullIntentSelector(final Intent intent) {
+ intent.setSelector(null);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java
new file mode 100644
index 0000000000..b8f15c04e3
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java
@@ -0,0 +1,168 @@
+/* -*- 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.util;
+
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+
+public class NetworkUtils {
+ /*
+ * Keep the below constants in sync with
+ * http://searchfox.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum ConnectionSubType {
+ CELL_2G("2g"),
+ CELL_3G("3g"),
+ CELL_4G("4g"),
+ ETHERNET("ethernet"),
+ WIFI("wifi"),
+ WIMAX("wimax"),
+ UNKNOWN("unknown");
+
+ public final String value;
+
+ ConnectionSubType(final String value) {
+ this.value = value;
+ }
+ }
+
+ /*
+ * Keep the below constants in sync with
+ * http://searchfox.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum NetworkStatus {
+ UP("up"),
+ DOWN("down"),
+ UNKNOWN("unknown");
+
+ public final String value;
+
+ NetworkStatus(final String value) {
+ this.value = value;
+ }
+ }
+
+ // Connection Type defined in Network Information API v3.
+ // See Bug 1270401 - current W3C Spec (Editor's Draft) is different, it also contains wimax,
+ // mixed, unknown.
+ // W3C spec: http://w3c.github.io/netinfo/#the-connectiontype-enum
+ public enum ConnectionType {
+ CELLULAR(0),
+ BLUETOOTH(1),
+ ETHERNET(2),
+ WIFI(3),
+ OTHER(4),
+ NONE(5);
+
+ public final int value;
+
+ ConnectionType(final int value) {
+ this.value = value;
+ }
+ }
+
+ public static boolean isConnected(final ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return false;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnected();
+ }
+
+ /** For mobile connections, maps particular connection subtype to a general 2G, 3G, 4G bucket. */
+ public static ConnectionSubType getConnectionSubType(
+ final ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+
+ if (networkInfo == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionSubType.ETHERNET;
+ case ConnectivityManager.TYPE_MOBILE:
+ return getGenericMobileSubtype(networkInfo.getSubtype());
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionSubType.WIMAX;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionSubType.WIFI;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+
+ public static ConnectionType getConnectionType(final ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionType.NONE;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return ConnectionType.NONE;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_BLUETOOTH:
+ return ConnectionType.BLUETOOTH;
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionType.ETHERNET;
+ // Fallthrough, MOBILE and WIMAX both map to CELLULAR.
+ case ConnectivityManager.TYPE_MOBILE:
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionType.CELLULAR;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionType.WIFI;
+ default:
+ return ConnectionType.OTHER;
+ }
+ }
+
+ public static NetworkStatus getNetworkStatus(final ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return NetworkStatus.UNKNOWN;
+ }
+
+ if (isConnected(connectivityManager)) {
+ return NetworkStatus.UP;
+ }
+ return NetworkStatus.DOWN;
+ }
+
+ private static ConnectionSubType getGenericMobileSubtype(final int subtype) {
+ switch (subtype) {
+ // 2G types: fallthrough 5x
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ return ConnectionSubType.CELL_2G;
+ // 3G types: fallthrough 9x
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return ConnectionSubType.CELL_3G;
+ // 4G - just one type!
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return ConnectionSubType.CELL_4G;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
new file mode 100644
index 0000000000..2fb4015f41
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
@@ -0,0 +1,149 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// This code is based on AOSP /libcore/luni/src/main/java/java/net/ProxySelectorImpl.java
+
+package org.mozilla.gecko.util;
+
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+
+public class ProxySelector {
+ public static URLConnection openConnectionWithProxy(final URI uri) throws IOException {
+ final java.net.ProxySelector ps = java.net.ProxySelector.getDefault();
+ Proxy proxy = Proxy.NO_PROXY;
+ if (ps != null) {
+ final List<Proxy> proxies = ps.select(uri);
+ if (proxies != null && !proxies.isEmpty()) {
+ proxy = proxies.get(0);
+ }
+ }
+
+ return uri.toURL().openConnection(proxy);
+ }
+
+ public ProxySelector() {}
+
+ public Proxy select(final String scheme, final String host) {
+ int port = -1;
+ Proxy proxy = null;
+ String nonProxyHostsKey = null;
+ boolean httpProxyOkay = true;
+ if ("http".equalsIgnoreCase(scheme)) {
+ port = 80;
+ nonProxyHostsKey = "http.nonProxyHosts";
+ proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ port = 443;
+ nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support this
+ proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("ftp".equalsIgnoreCase(scheme)) {
+ port = 80; // not 21 as you might guess
+ nonProxyHostsKey = "ftp.nonProxyHosts";
+ proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("socket".equalsIgnoreCase(scheme)) {
+ httpProxyOkay = false;
+ } else {
+ return Proxy.NO_PROXY;
+ }
+
+ if (nonProxyHostsKey != null && isNonProxyHost(host, System.getProperty(nonProxyHostsKey))) {
+ return Proxy.NO_PROXY;
+ }
+
+ if (proxy != null) {
+ return proxy;
+ }
+
+ if (httpProxyOkay) {
+ proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);
+ if (proxy != null) {
+ return proxy;
+ }
+ }
+
+ proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);
+ if (proxy != null) {
+ return proxy;
+ }
+
+ return Proxy.NO_PROXY;
+ }
+
+ /** Returns the proxy identified by the {@code hostKey} system property, or null. */
+ @Nullable
+ private Proxy lookupProxy(
+ final String hostKey, final String portKey, final Proxy.Type type, final int defaultPort) {
+ final String host = System.getProperty(hostKey);
+ if (TextUtils.isEmpty(host)) {
+ return null;
+ }
+
+ final int port = getSystemPropertyInt(portKey, defaultPort);
+ if (port == -1) {
+ // Port can be -1. See bug 1270529.
+ return null;
+ }
+
+ return new Proxy(type, InetSocketAddress.createUnresolved(host, port));
+ }
+
+ private int getSystemPropertyInt(final String key, final int defaultValue) {
+ final String string = System.getProperty(key);
+ if (string != null) {
+ try {
+ return Integer.parseInt(string);
+ } catch (final NumberFormatException ignored) {
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns true if the {@code nonProxyHosts} system property pattern exists and matches {@code
+ * host}.
+ */
+ private boolean isNonProxyHost(final String host, final String nonProxyHosts) {
+ if (host == null || nonProxyHosts == null) {
+ return false;
+ }
+
+ // construct pattern
+ final StringBuilder patternBuilder = new StringBuilder();
+ for (int i = 0; i < nonProxyHosts.length(); i++) {
+ final char c = nonProxyHosts.charAt(i);
+ switch (c) {
+ case '.':
+ patternBuilder.append("\\.");
+ break;
+ case '*':
+ patternBuilder.append(".*");
+ break;
+ default:
+ patternBuilder.append(c);
+ }
+ }
+ // check whether the host is the nonProxyHosts.
+ final String pattern = patternBuilder.toString();
+ return host.matches(pattern);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
new file mode 100644
index 0000000000..00625800c9
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
@@ -0,0 +1,145 @@
+/* -*- 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.util;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public final class ThreadUtils {
+ private static final String LOGTAG = "ThreadUtils";
+
+ /**
+ * Controls the action taken when a method like {@link
+ * ThreadUtils#assertOnUiThread(AssertBehavior)} detects a problem.
+ */
+ public enum AssertBehavior {
+ NONE,
+ THROW,
+ }
+
+ private static final Thread sUiThread = Looper.getMainLooper().getThread();
+ private static final Handler sUiHandler = new Handler(Looper.getMainLooper());
+
+ // Referenced directly from GeckoAppShell in highly performance-sensitive code (The extra
+ // function call of the getter was harming performance. (Bug 897123))
+ // Once Bug 709230 is resolved we should reconsider this as ProGuard should be able to optimise
+ // this out at compile time.
+ public static Handler sGeckoHandler;
+ public static volatile Thread sGeckoThread;
+
+ public static Thread getUiThread() {
+ return sUiThread;
+ }
+
+ public static Handler getUiHandler() {
+ return sUiHandler;
+ }
+
+ /**
+ * Runs the provided runnable on the UI thread. If this method is called on the UI thread the
+ * runnable will be executed synchronously.
+ *
+ * @param runnable the runnable to be executed.
+ */
+ public static void runOnUiThread(final Runnable runnable) {
+ // We're on the UI thread already, let's just run this
+ if (isOnUiThread()) {
+ runnable.run();
+ return;
+ }
+
+ postToUiThread(runnable);
+ }
+
+ public static void postToUiThread(final Runnable runnable) {
+ sUiHandler.post(runnable);
+ }
+
+ public static void postToUiThreadDelayed(final Runnable runnable, final long delayMillis) {
+ sUiHandler.postDelayed(runnable, delayMillis);
+ }
+
+ public static void removeUiThreadCallbacks(final Runnable runnable) {
+ sUiHandler.removeCallbacks(runnable);
+ }
+
+ public static Handler getBackgroundHandler() {
+ return GeckoBackgroundThread.getHandler();
+ }
+
+ public static void postToBackgroundThread(final Runnable runnable) {
+ GeckoBackgroundThread.post(runnable);
+ }
+
+ public static void assertOnUiThread(final AssertBehavior assertBehavior) {
+ assertOnThread(getUiThread(), assertBehavior);
+ }
+
+ public static void assertOnUiThread() {
+ assertOnThread(getUiThread(), AssertBehavior.THROW);
+ }
+
+ @RobocopTarget
+ public static void assertOnGeckoThread() {
+ assertOnThread(sGeckoThread, AssertBehavior.THROW);
+ }
+
+ public static void assertOnThread(final Thread expectedThread, final AssertBehavior behavior) {
+ assertOnThreadComparison(expectedThread, behavior, true);
+ }
+
+ private static void assertOnThreadComparison(
+ final Thread expectedThread, final AssertBehavior behavior, final boolean expected) {
+ final Thread currentThread = Thread.currentThread();
+ final long currentThreadId = currentThread.getId();
+ final long expectedThreadId = expectedThread.getId();
+
+ if ((currentThreadId == expectedThreadId) == expected) {
+ return;
+ }
+
+ final String message;
+ if (expected) {
+ message =
+ "Expected thread "
+ + expectedThreadId
+ + " (\""
+ + expectedThread.getName()
+ + "\"), but running on thread "
+ + currentThreadId
+ + " (\""
+ + currentThread.getName()
+ + "\")";
+ } else {
+ message =
+ "Expected anything but "
+ + expectedThreadId
+ + " (\""
+ + expectedThread.getName()
+ + "\"), but running there.";
+ }
+
+ final IllegalThreadStateException e = new IllegalThreadStateException(message);
+
+ switch (behavior) {
+ case THROW:
+ throw e;
+ default:
+ Log.e(LOGTAG, "Method called on wrong thread!", e);
+ }
+ }
+
+ public static boolean isOnUiThread() {
+ return isOnThread(getUiThread());
+ }
+
+ @RobocopTarget
+ public static boolean isOnThread(final Thread thread) {
+ return (Thread.currentThread().getId() == thread.getId());
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMError.jinja b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMError.jinja
new file mode 100644
index 0000000000..f704bbc775
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMError.jinja
@@ -0,0 +1,38 @@
+/* -*- Mode: Java; c-basic-offset: 2; 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.util;
+
+public final class XPCOMError {
+ /** Check if the error code corresponds to a failure */
+ public static boolean failed(long err) {
+ return (err & 0x80000000L) != 0;
+ }
+
+ /** Check if the error code corresponds to a failure */
+ public static boolean succeeded(long err) {
+ return !failed(err);
+ }
+
+ /** Extract the error code part of the error message */
+ public static int getErrorCode(long err) {
+ return (int)(err & 0xffffL);
+ }
+
+ /** Extract the error module part of the error message */
+ public static int getErrorModule(long err) {
+ return (int)(((err >> 16) - NS_ERROR_MODULE_BASE_OFFSET) & 0x1fffL);
+ }
+
+ public static final int NS_ERROR_MODULE_BASE_OFFSET = {{ MODULE_BASE_OFFSET }};
+
+{% for mod, val in modules %}
+ public static final int NS_ERROR_MODULE_{{ mod }} = {{ val }};
+{% endfor %}
+
+{% for error, val in errors %}
+ public static final long {{ error }} = 0x{{ "%X" % val }}L;
+{% endfor %}
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMEventTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMEventTarget.java
new file mode 100644
index 0000000000..f3e5248466
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMEventTarget.java
@@ -0,0 +1,170 @@
+/* -*- 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.util;
+
+import androidx.annotation.NonNull;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.geckoview.BuildConfig;
+
+/**
+ * Wrapper for nsIEventTarget, enabling seamless dispatch of java runnables to Gecko event queues.
+ */
+@WrapForJNI
+public final class XPCOMEventTarget extends JNIObject implements IXPCOMEventTarget {
+ @Override
+ public void execute(final Runnable runnable) {
+ dispatchNative(new JNIRunnable(runnable));
+ }
+
+ public static synchronized IXPCOMEventTarget mainThread() {
+ if (mMainThread == null) {
+ mMainThread = new AsyncProxy("main");
+ }
+ return mMainThread;
+ }
+
+ private static IXPCOMEventTarget mMainThread = null;
+
+ public static synchronized IXPCOMEventTarget launcherThread() {
+ if (mLauncherThread == null) {
+ mLauncherThread = new AsyncProxy("launcher");
+ }
+ return mLauncherThread;
+ }
+
+ private static IXPCOMEventTarget mLauncherThread = null;
+
+ /**
+ * Runs the provided runnable on the launcher thread. If this method is called from the launcher
+ * thread itself, the runnable will be executed immediately and synchronously.
+ */
+ public static void runOnLauncherThread(@NonNull final Runnable runnable) {
+ final IXPCOMEventTarget launcherThread = launcherThread();
+ if (launcherThread.isOnCurrentThread()) {
+ // We're already on the launcher thread, just execute the runnable
+ runnable.run();
+ return;
+ }
+
+ launcherThread.execute(runnable);
+ }
+
+ public static void assertOnLauncherThread() {
+ if (BuildConfig.DEBUG_BUILD && !launcherThread().isOnCurrentThread()) {
+ throw new AssertionError("Expected to be running on XPCOM launcher thread");
+ }
+ }
+
+ public static void assertNotOnLauncherThread() {
+ if (BuildConfig.DEBUG_BUILD && launcherThread().isOnCurrentThread()) {
+ throw new AssertionError("Expected to not be running on XPCOM launcher thread");
+ }
+ }
+
+ private static synchronized IXPCOMEventTarget getTarget(final String name) {
+ if (name.equals("launcher")) {
+ return mLauncherThread;
+ } else if (name.equals("main")) {
+ return mMainThread;
+ } else {
+ throw new RuntimeException("Attempt to assign to unknown thread named " + name);
+ }
+ }
+
+ @WrapForJNI
+ private static synchronized void setTarget(final String name, final XPCOMEventTarget target) {
+ if (name.equals("main")) {
+ mMainThread = target;
+ } else if (name.equals("launcher")) {
+ mLauncherThread = target;
+ } else {
+ throw new RuntimeException("Attempt to assign to unknown thread named " + name);
+ }
+
+ // Ensure that we see the right name in the Java debugger. We don't do this for mMainThread
+ // because its name was already set (in this context, "main" is the GeckoThread).
+ if (mMainThread != target) {
+ target.execute(
+ () -> {
+ Thread.currentThread().setName(name);
+ });
+ }
+ }
+
+ @Override
+ public native boolean isOnCurrentThread();
+
+ private native void dispatchNative(final JNIRunnable runnable);
+
+ @WrapForJNI
+ private static synchronized void resolveAndDispatch(final String name, final Runnable runnable) {
+ getTarget(name).execute(runnable);
+ }
+
+ private static native void resolveAndDispatchNative(final String name, final Runnable runnable);
+
+ @Override
+ protected native void disposeNative();
+
+ @WrapForJNI
+ private static final class JNIRunnable {
+ JNIRunnable(final Runnable inner) {
+ mInner = inner;
+ }
+
+ @WrapForJNI
+ void run() {
+ mInner.run();
+ }
+
+ private Runnable mInner;
+ }
+
+ private static final class AsyncProxy implements IXPCOMEventTarget {
+ private String mTargetName;
+
+ public AsyncProxy(final String targetName) {
+ mTargetName = targetName;
+ }
+
+ @Override
+ public void execute(final Runnable runnable) {
+ final IXPCOMEventTarget target = XPCOMEventTarget.getTarget(mTargetName);
+
+ if (target instanceof XPCOMEventTarget) {
+ target.execute(runnable);
+ return;
+ }
+
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.JNI_READY,
+ XPCOMEventTarget.class,
+ "resolveAndDispatchNative",
+ String.class,
+ mTargetName,
+ Runnable.class,
+ runnable);
+ }
+
+ @Override
+ public boolean isOnCurrentThread() {
+ final IXPCOMEventTarget target = XPCOMEventTarget.getTarget(mTargetName);
+
+ // If target is not yet a XPCOMEventTarget then JNI is not
+ // initialized yet. If JNI is not initialized yet, then we cannot
+ // possibly be running on a target with an XPCOMEventTarget.
+ if (!(target instanceof XPCOMEventTarget)) {
+ return false;
+ }
+
+ // Otherwise we have a real XPCOMEventTarget, so we can delegate
+ // this call to it.
+ return target.isOnCurrentThread();
+ }
+ }
+}