From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../mozilla/gecko/util/BundleEventListener.java | 21 + .../java/org/mozilla/gecko/util/DebugConfig.java | 136 +++ .../java/org/mozilla/gecko/util/EventCallback.java | 58 + .../mozilla/gecko/util/GeckoBackgroundThread.java | 72 ++ .../java/org/mozilla/gecko/util/GeckoBundle.java | 1164 ++++++++++++++++++++ .../gecko/util/HardwareCodecCapabilityUtils.java | 397 +++++++ .../java/org/mozilla/gecko/util/HardwareUtils.java | 46 + .../org/mozilla/gecko/util/IXPCOMEventTarget.java | 12 + .../java/org/mozilla/gecko/util/ImageDecoder.java | 88 ++ .../java/org/mozilla/gecko/util/ImageResource.java | 334 ++++++ .../org/mozilla/gecko/util/InputDeviceUtils.java | 20 + .../java/org/mozilla/gecko/util/IntentUtils.java | 120 ++ .../java/org/mozilla/gecko/util/NetworkUtils.java | 168 +++ .../java/org/mozilla/gecko/util/ProxySelector.java | 149 +++ .../java/org/mozilla/gecko/util/ThreadUtils.java | 145 +++ .../java/org/mozilla/gecko/util/XPCOMError.jinja | 38 + .../org/mozilla/gecko/util/XPCOMEventTarget.java | 170 +++ 17 files changed, 3138 insertions(+) create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IXPCOMEventTarget.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageDecoder.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputDeviceUtils.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMError.jinja create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMEventTarget.java (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/util') 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..b030c8e67f --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java @@ -0,0 +1,136 @@ +/* -*- 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.Build; +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 prefs; + protected Map env; + protected List 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 { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // There are a lot of problems with SnakeYaml on older version let's just bail. + throw new ConfigException("Config version is only supported for SDK_INT >= 21."); + } + + 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 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 combinedArgs = new ArrayList<>(); + if (initArgs != null) { + combinedArgs.addAll(Arrays.asList(initArgs)); + } + combinedArgs.addAll(args); + + return combinedArgs.toArray(new String[combinedArgs.size()]); + } + + @Nullable + public Map mergeIntoPrefs(final @Nullable Map initPrefs) { + if (prefs == null) { + return initPrefs; + } + + Log.d(LOGTAG, "Adding prefs from debug config: " + prefs); + + final Map 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. + * + *

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 void resolveTo(final @Nullable GeckoResult 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..4ed37872f2 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java @@ -0,0 +1,1164 @@ +/* -*- 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 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 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 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 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 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 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 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 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 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) { + final Object wrapped = JSONObject.wrap(value); + jsonValue = wrapped != null ? wrapped : value.toString(); + } else if (value == null) { + jsonValue = JSONObject.NULL; + } else if (value.getClass().isArray()) { + final JSONArray jsonArray = new JSONArray(); + for (int j = 0; j < Array.getLength(value); j++) { + jsonArray.put(Array.get(value, j)); + } + jsonValue = jsonArray; + } else { + 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 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 CREATOR = + new Parcelable.Creator() { + @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..7e302a7c3d --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java @@ -0,0 +1,397 @@ +/* -*- 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; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + codecList = getCodecListWithOldAPI(); + } else { + final MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + codecList = list.getCodecInfos(); + } + return codecList; + } + + // Return list of all decoders. + private static MediaCodecInfo[] getDecoderInfos() { + final ArrayList decoderList = new ArrayList(); + 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 encoderList = new ArrayList(); + 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 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 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) { + // isFeatureSupported supported on API level >= 19. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT + || 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) { + if (Build.VERSION.SDK_INT >= 20) { + for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) { + final MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); + 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 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; + } + + if (Build.VERSION.SDK_INT >= 29 + && ((profile == MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR10Plus) + || (profile == MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR10Plus))) { + return true; + } + + return false; + } +} 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..96e5c7b311 --- /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 { + public 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 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. + *

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 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. + * + *

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. + *

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 decode(final @NonNull String uri, final int desiredLength) { + if (uri == null) { + throw new IllegalArgumentException("Uri cannot be null"); + } + + final GeckoResult 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..d57147f363 --- /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 Image Resource + */ +@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.toLowerCase(Locale.ROOT); + 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 Attribute + * spec for sizes + */ + 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 sizes = new ArrayList(); + + 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 size. 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 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 mImages; + + // A sorted size-index list. The list is sorted based on the supported + // sizes of the images in ascending order. + private final List 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 size. 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 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..20a7b95f4d --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java @@ -0,0 +1,120 @@ +/* + * 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.TargetApi; +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) { + final Uri normUri = + normalizeUriScheme( + aUri.indexOf(':') >= 0 ? Uri.parse(aUri) : new Uri.Builder().scheme(aUri).build()); + return normUri; + } + + 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. + @TargetApi(15) + 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 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..31eac71a66 --- /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 != null && 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 == null || !(target instanceof XPCOMEventTarget)) { + return false; + } + + // Otherwise we have a real XPCOMEventTarget, so we can delegate + // this call to it. + return target.isOnCurrentThread(); + } + } +} -- cgit v1.2.3