summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java415
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java284
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/EnterpriseRoots.java96
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java588
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java1614
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java200
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoDragAndDrop.java253
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java456
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java807
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java413
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenChangeListener.java73
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java273
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSystemStateListener.java195
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java967
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java104
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/MagnifiableSurfaceView.java137
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/MultiMap.java186
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/NativeQueue.java225
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/ScreenManagerHelper.java24
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java227
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceViewWrapper.java198
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/TelemetryUtils.java102
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/BuildFlag.java25
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java18
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java14
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java56
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/AndroidVsync.java72
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/CompositorSurfaceManager.java26
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurface.java151
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurfaceTexture.java314
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java71
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java77
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java139
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceControlManager.java111
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java38
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SyncConfig.java59
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/AsyncCodec.java61
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/AsyncCodecFactory.java19
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/BaseHlsPlayer.java104
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Codec.java713
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/CodecProxy.java503
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/FormatParam.java199
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoAudioInfo.java36
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSDemuxerWrapper.java164
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSResourceWrapper.java119
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSSample.java93
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsAudioRenderer.java167
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsPlayer.java1107
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsRendererBase.java340
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsVideoRenderer.java502
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrm.java40
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java766
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java50
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoPlayerFactory.java43
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoVideoInfo.java45
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java481
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/LollipopAsyncCodec.java248
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/MediaDrmProxy.java297
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/MediaManager.java79
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteManager.java248
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java163
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java248
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Sample.java291
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java101
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SamplePool.java154
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SessionKeyInfo.java50
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Utils.java39
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java432
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java20
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java12
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SharedMemory.java192
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja19
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java924
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java40
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java223
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java63
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java74
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java613
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java141
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java21
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java130
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java58
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java72
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java1194
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java389
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java46
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IXPCOMEventTarget.java12
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageDecoder.java88
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java334
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputDeviceUtils.java20
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java116
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java168
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java149
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java145
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMError.jinja38
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMEventTarget.java170
98 files changed, 22409 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
new file mode 100644
index 0000000000..99be57fc12
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
@@ -0,0 +1,415 @@
+/* -*- 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;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class AndroidGamepadManager {
+ // This is completely arbitrary.
+ private static final float TRIGGER_PRESSED_THRESHOLD = 0.25f;
+ private static final long POLL_TIMER_PERIOD = 1000; // milliseconds
+
+ private enum Axis {
+ X(MotionEvent.AXIS_X),
+ Y(MotionEvent.AXIS_Y),
+ Z(MotionEvent.AXIS_Z),
+ RZ(MotionEvent.AXIS_RZ);
+
+ public final int axis;
+
+ Axis(final int axis) {
+ this.axis = axis;
+ }
+ }
+
+ // A list of gamepad button mappings. Axes are determined at
+ // runtime, as they vary by Android version.
+ private enum Trigger {
+ Left(6),
+ Right(7);
+
+ public final int button;
+
+ Trigger(final int button) {
+ this.button = button;
+ }
+ }
+
+ private static final int FIRST_DPAD_BUTTON = 12;
+
+ // A list of axis number, gamepad button mappings for negative, positive.
+ // Button mappings are added to FIRST_DPAD_BUTTON.
+ private enum DpadAxis {
+ UpDown(MotionEvent.AXIS_HAT_Y, 0, 1),
+ LeftRight(MotionEvent.AXIS_HAT_X, 2, 3);
+
+ public final int axis;
+ public final int negativeButton;
+ public final int positiveButton;
+
+ DpadAxis(final int axis, final int negativeButton, final int positiveButton) {
+ this.axis = axis;
+ this.negativeButton = negativeButton;
+ this.positiveButton = positiveButton;
+ }
+ }
+
+ private enum Button {
+ A(KeyEvent.KEYCODE_BUTTON_A),
+ B(KeyEvent.KEYCODE_BUTTON_B),
+ X(KeyEvent.KEYCODE_BUTTON_X),
+ Y(KeyEvent.KEYCODE_BUTTON_Y),
+ L1(KeyEvent.KEYCODE_BUTTON_L1),
+ R1(KeyEvent.KEYCODE_BUTTON_R1),
+ L2(KeyEvent.KEYCODE_BUTTON_L2),
+ R2(KeyEvent.KEYCODE_BUTTON_R2),
+ SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
+ START(KeyEvent.KEYCODE_BUTTON_START),
+ THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
+ THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
+ DPAD_UP(KeyEvent.KEYCODE_DPAD_UP),
+ DPAD_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
+ DPAD_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
+ DPAD_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT);
+
+ public final int button;
+
+ Button(final int button) {
+ this.button = button;
+ }
+ }
+
+ private static class Gamepad {
+ // ID from GamepadService
+ public byte[] handle;
+ // Retain axis state so we can determine changes.
+ public float axes[];
+ public boolean dpad[];
+ public int triggerAxes[];
+ public float triggers[];
+
+ public Gamepad(final byte[] handle, final int deviceId) {
+ this.handle = handle;
+ axes = new float[Axis.values().length];
+ dpad = new boolean[4];
+ triggers = new float[2];
+
+ final InputDevice device = InputDevice.getDevice(deviceId);
+ if (device != null) {
+ // LTRIGGER/RTRIGGER don't seem to be exposed on older
+ // versions of Android.
+ if (device.getMotionRange(MotionEvent.AXIS_LTRIGGER) != null
+ && device.getMotionRange(MotionEvent.AXIS_RTRIGGER) != null) {
+ triggerAxes = new int[] {MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_RTRIGGER};
+ } else if (device.getMotionRange(MotionEvent.AXIS_BRAKE) != null
+ && device.getMotionRange(MotionEvent.AXIS_GAS) != null) {
+ triggerAxes = new int[] {MotionEvent.AXIS_BRAKE, MotionEvent.AXIS_GAS};
+ } else {
+ triggerAxes = null;
+ }
+ }
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui")
+ private static native byte[] nativeAddGamepad();
+
+ @WrapForJNI(calledFrom = "ui")
+ private static native void nativeRemoveGamepad(byte[] aGamepadHandle);
+
+ @WrapForJNI(calledFrom = "ui")
+ private static native void onButtonChange(
+ byte[] aGamepadHandle, int aButton, boolean aPressed, float aValue);
+
+ @WrapForJNI(calledFrom = "ui")
+ private static native void onAxisChange(byte[] aGamepadHandle, boolean[] aValid, float[] aValues);
+
+ private static boolean sStarted;
+ private static final SparseArray<Gamepad> sGamepads = new SparseArray<>();
+ private static final SparseArray<List<KeyEvent>> sPendingGamepads = new SparseArray<>();
+ private static InputManager.InputDeviceListener sListener;
+ private static Timer sPollTimer;
+
+ private AndroidGamepadManager() {}
+
+ @WrapForJNI
+ private static void start(final Context context) {
+ ThreadUtils.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ doStart(context);
+ }
+ });
+ }
+
+ /* package */ static void doStart(final Context context) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ scanForGamepads();
+ addDeviceListener(context);
+ sStarted = true;
+ }
+ }
+
+ @WrapForJNI
+ private static void stop(final Context context) {
+ ThreadUtils.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ doStop(context);
+ }
+ });
+ }
+
+ /* package */ static void doStop(final Context context) {
+ ThreadUtils.assertOnUiThread();
+ if (sStarted) {
+ removeDeviceListener(context);
+ sPendingGamepads.clear();
+ sGamepads.clear();
+ sStarted = false;
+ }
+ }
+
+ /* package */ static void handleGamepadAdded(final int deviceId, final byte[] gamepadHandle) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return;
+ }
+
+ final List<KeyEvent> pending = sPendingGamepads.get(deviceId);
+ if (pending == null) {
+ removeGamepad(deviceId);
+ return;
+ }
+
+ sPendingGamepads.remove(deviceId);
+ sGamepads.put(deviceId, new Gamepad(gamepadHandle, deviceId));
+ // Handle queued KeyEvents
+ for (final KeyEvent ev : pending) {
+ handleKeyEvent(ev);
+ }
+ }
+
+ private static float sDeadZoneThresholdOverride = 1e-2f;
+
+ private static boolean isValueInDeadZone(final MotionEvent event, final int axis) {
+ final float threshold;
+ if (sDeadZoneThresholdOverride >= 0) {
+ threshold = sDeadZoneThresholdOverride;
+ } else {
+ final InputDevice.MotionRange range = event.getDevice().getMotionRange(axis);
+ threshold = range.getFlat() + range.getFuzz();
+ }
+ final float value = event.getAxisValue(axis);
+ return (Math.abs(value) < threshold);
+ }
+
+ private static float deadZone(final MotionEvent ev, final int axis) {
+ if (isValueInDeadZone(ev, axis)) {
+ return 0.0f;
+ }
+ return ev.getAxisValue(axis);
+ }
+
+ private static void mapDpadAxis(
+ final Gamepad gamepad, final boolean pressed, final float value, final int which) {
+ if (pressed != gamepad.dpad[which]) {
+ gamepad.dpad[which] = pressed;
+ onButtonChange(gamepad.handle, FIRST_DPAD_BUTTON + which, pressed, Math.abs(value));
+ }
+ }
+
+ public static boolean handleMotionEvent(final MotionEvent ev) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return false;
+ }
+
+ final Gamepad gamepad = sGamepads.get(ev.getDeviceId());
+ if (gamepad == null) {
+ // Not a device we care about.
+ return false;
+ }
+
+ // First check the analog stick axes
+ final boolean[] valid = new boolean[Axis.values().length];
+ final float[] axes = new float[Axis.values().length];
+ boolean anyValidAxes = false;
+ for (final Axis axis : Axis.values()) {
+ final float value = deadZone(ev, axis.axis);
+ final int i = axis.ordinal();
+ if (value != gamepad.axes[i]) {
+ axes[i] = value;
+ gamepad.axes[i] = value;
+ valid[i] = true;
+ anyValidAxes = true;
+ }
+ }
+ if (anyValidAxes) {
+ // Send an axismove event.
+ onAxisChange(gamepad.handle, valid, axes);
+ }
+
+ // Map triggers to buttons.
+ if (gamepad.triggerAxes != null) {
+ for (final Trigger trigger : Trigger.values()) {
+ final int i = trigger.ordinal();
+ final int axis = gamepad.triggerAxes[i];
+ final float value = deadZone(ev, axis);
+ if (value != gamepad.triggers[i]) {
+ gamepad.triggers[i] = value;
+ final boolean pressed = value > TRIGGER_PRESSED_THRESHOLD;
+ onButtonChange(gamepad.handle, trigger.button, pressed, value);
+ }
+ }
+ }
+ // Map d-pad to buttons.
+ for (final DpadAxis dpadaxis : DpadAxis.values()) {
+ final float value = deadZone(ev, dpadaxis.axis);
+ mapDpadAxis(gamepad, value < 0.0f, value, dpadaxis.negativeButton);
+ mapDpadAxis(gamepad, value > 0.0f, value, dpadaxis.positiveButton);
+ }
+ return true;
+ }
+
+ public static boolean handleKeyEvent(final KeyEvent ev) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return false;
+ }
+
+ final int deviceId = ev.getDeviceId();
+ final List<KeyEvent> pendingGamepad = sPendingGamepads.get(deviceId);
+ if (pendingGamepad != null) {
+ // Queue up key events for pending devices.
+ pendingGamepad.add(ev);
+ return true;
+ }
+
+ if (sGamepads.get(deviceId) == null) {
+ final InputDevice device = ev.getDevice();
+ if (device != null
+ && (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ // This is a gamepad we haven't seen yet.
+ addGamepad(device);
+ sPendingGamepads.get(deviceId).add(ev);
+ return true;
+ }
+ // Not a device we care about.
+ return false;
+ }
+
+ int key = -1;
+ for (final Button button : Button.values()) {
+ if (button.button == ev.getKeyCode()) {
+ key = button.ordinal();
+ break;
+ }
+ }
+ if (key == -1) {
+ // Not a key we know how to handle.
+ return false;
+ }
+ if (ev.getRepeatCount() > 0) {
+ // We would handle this key, but we're not interested in
+ // repeats. Eat it.
+ return true;
+ }
+
+ final Gamepad gamepad = sGamepads.get(deviceId);
+ final boolean pressed = ev.getAction() == KeyEvent.ACTION_DOWN;
+ onButtonChange(gamepad.handle, key, pressed, pressed ? 1.0f : 0.0f);
+ return true;
+ }
+
+ private static void scanForGamepads() {
+ final int[] deviceIds = InputDevice.getDeviceIds();
+ if (deviceIds == null) {
+ return;
+ }
+ for (int i = 0; i < deviceIds.length; i++) {
+ final InputDevice device = InputDevice.getDevice(deviceIds[i]);
+ if (device == null) {
+ continue;
+ }
+ if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD) {
+ continue;
+ }
+ addGamepad(device);
+ }
+ }
+
+ private static void addGamepad(final InputDevice device) {
+ sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
+ final byte[] gamepadId = nativeAddGamepad();
+ ThreadUtils.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ handleGamepadAdded(device.getId(), gamepadId);
+ }
+ });
+ }
+
+ private static void removeGamepad(final int deviceId) {
+ final Gamepad gamepad = sGamepads.get(deviceId);
+ nativeRemoveGamepad(gamepad.handle);
+ sGamepads.remove(deviceId);
+ }
+
+ private static void addDeviceListener(final Context context) {
+ sListener =
+ new InputManager.InputDeviceListener() {
+ @Override
+ public void onInputDeviceAdded(final int deviceId) {
+ final InputDevice device = InputDevice.getDevice(deviceId);
+ if (device == null) {
+ return;
+ }
+ if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ addGamepad(device);
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(final int deviceId) {
+ if (sPendingGamepads.get(deviceId) != null) {
+ // Got removed before Gecko's ack reached us.
+ // gamepadAdded will deal with it.
+ sPendingGamepads.remove(deviceId);
+ return;
+ }
+ if (sGamepads.get(deviceId) != null) {
+ removeGamepad(deviceId);
+ }
+ }
+
+ @Override
+ public void onInputDeviceChanged(final int deviceId) {}
+ };
+ final InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
+ im.registerInputDeviceListener(sListener, ThreadUtils.getUiHandler());
+ }
+
+ private static void removeDeviceListener(final Context context) {
+ final InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
+ im.unregisterInputDeviceListener(sListener);
+ sListener = null;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java
new file mode 100644
index 0000000000..6ef2dd3073
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/Clipboard.java
@@ -0,0 +1,284 @@
+/* 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;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.content.ClipboardManager.OnPrimaryClipChangedListener;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.atomic.AtomicLong;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public final class Clipboard {
+ private static final String HTML_MIME = "text/html";
+ private static final String PLAINTEXT_MIME = "text/plain";
+ private static final String LOGTAG = "GeckoClipboard";
+ private static final int DEFAULT_BUFFER_SIZE = 8192;
+
+ private static OnPrimaryClipChangedListener sClipboardChangedListener = null;
+ private static final AtomicLong sClipboardSequenceNumber = new AtomicLong();
+
+ private Clipboard() {}
+
+ /**
+ * Get the text on the primary clip on Android clipboard
+ *
+ * @param context application context.
+ * @return a plain text string of clipboard data.
+ */
+ public static String getText(final Context context) {
+ return getTextData(context, PLAINTEXT_MIME);
+ }
+
+ /**
+ * Get the text data on the primary clip on clipboard
+ *
+ * @param context application context
+ * @param mimeType the mime type we want. This supports text/html and text/plain only. If other
+ * type, we do nothing.
+ * @return a string into clipboard.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static String getTextData(final Context context, final String mimeType) {
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm.hasPrimaryClip()) {
+ final ClipData clip = cm.getPrimaryClip();
+ if (clip == null || clip.getItemCount() == 0) {
+ return null;
+ }
+
+ final ClipDescription description = clip.getDescription();
+ if (HTML_MIME.equals(mimeType)
+ && description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
+ final CharSequence data = clip.getItemAt(0).getHtmlText();
+ if (data == null) {
+ return null;
+ }
+ return data.toString();
+ }
+ if (PLAINTEXT_MIME.equals(mimeType)) {
+ try {
+ return clip.getItemAt(0).coerceToText(context).toString();
+ } catch (final SecurityException e) {
+ Log.e(LOGTAG, "Couldn't get clip data from clipboard", e);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the blob data on the primary clip on clipboard
+ *
+ * @param mimeType the mime type we want.
+ * @return a byte array into clipboard.
+ */
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "nsresult")
+ private static byte[] getRawData(final String mimeType) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (cm.hasPrimaryClip()) {
+ final ClipData clip = cm.getPrimaryClip();
+ if (clip == null || clip.getItemCount() == 0) {
+ return null;
+ }
+
+ final ClipDescription description = clip.getDescription();
+ if (description.hasMimeType(mimeType)) {
+ return getRawDataFromClipData(context, clip);
+ }
+ }
+ return null;
+ }
+
+ private static byte[] getRawDataFromClipData(final Context context, final ClipData clipData) {
+ try (final AssetFileDescriptor descriptor =
+ context
+ .getContentResolver()
+ .openAssetFileDescriptor(clipData.getItemAt(0).getUri(), "r");
+ final InputStream inputStream = new FileInputStream(descriptor.getFileDescriptor());
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ final byte[] data = new byte[DEFAULT_BUFFER_SIZE];
+ int readed;
+ while ((readed = inputStream.read(data)) != -1) {
+ outputStream.write(data, 0, readed);
+ }
+ return outputStream.toByteArray();
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Couldn't get clip data from clipboard due to I/O error", e);
+ } catch (final OutOfMemoryError e) {
+ Log.e(LOGTAG, "Couldn't get clip data from clipboard due to OOM", e);
+ }
+ return null;
+ }
+
+ /**
+ * Set plain text to clipboard
+ *
+ * @param context application context
+ * @param text a plain text to set to clipboard
+ * @return true if copy is successful.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public static boolean setText(final Context context, final CharSequence text) {
+ return setData(context, ClipData.newPlainText("text", text));
+ }
+
+ /**
+ * Store HTML to clipboard
+ *
+ * @param context application context
+ * @param text a plain text to set to clipboard
+ * @param html a html text to set to clipboard
+ * @return true if copy is successful.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean setHTML(
+ final Context context, final CharSequence text, final String htmlText) {
+ return setData(context, ClipData.newHtmlText("html", text, htmlText));
+ }
+
+ /**
+ * Store {@link android.content.ClipData} to clipboard
+ *
+ * @param context application context
+ * @param clipData a {@link android.content.ClipData} to set to clipboard
+ * @return true if copy is successful.
+ */
+ private static boolean setData(final Context context, final ClipData clipData) {
+ // In API Level 11 and above, CLIPBOARD_SERVICE returns android.content.ClipboardManager,
+ // which is a subclass of android.text.ClipboardManager.
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ try {
+ cm.setPrimaryClip(clipData);
+ } catch (final NullPointerException e) {
+ // Bug 776223: This is a Samsung clipboard bug. setPrimaryClip() can throw
+ // a NullPointerException if Samsung's /data/clipboard directory is full.
+ // Fortunately, the text is still successfully copied to the clipboard.
+ } catch (final RuntimeException e) {
+ // If clipData is too large, TransactionTooLargeException occurs.
+ Log.e(LOGTAG, "Couldn't set clip data to clipboard", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check whether primary clipboard has given MIME type.
+ *
+ * @param context application context
+ * @param mimeType MIME type
+ * @return true if the clipboard is nonempty, false otherwise.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean hasData(final Context context, final String mimeType) {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+ if (HTML_MIME.equals(mimeType) || PLAINTEXT_MIME.equals(mimeType)) {
+ return !TextUtils.isEmpty(getTextData(context, mimeType));
+ }
+ }
+
+ // Calling getPrimaryClip causes a toast message from Android 12.
+ // https://developer.android.com/about/versions/12/behavior-changes-all#clipboard-access-notifications
+
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+
+ if (!cm.hasPrimaryClip()) {
+ return false;
+ }
+
+ final ClipDescription description = cm.getPrimaryClipDescription();
+ if (description == null) {
+ return false;
+ }
+
+ if (HTML_MIME.equals(mimeType)) {
+ return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
+ }
+
+ if (PLAINTEXT_MIME.equals(mimeType)) {
+ // We cannot check content in data at this time to avoid toast message.
+ return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)
+ || description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
+ }
+
+ return description.hasMimeType(mimeType);
+ }
+
+ /**
+ * Deletes all data from the clipboard.
+ *
+ * @param context application context
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static void clear(final Context context) {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+ setText(context, null);
+ return;
+ }
+ // Although we don't know more details of https://crbug.com/1203377, Blink doesn't use
+ // clearPrimaryClip on Android P since this may throw an exception, even if it is supported
+ // on Android P.
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.clearPrimaryClip();
+ }
+
+ /**
+ * Start monitor clipboard sequence number.
+ *
+ * @param context application context
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static void startTrackingClipboardData(final Context context) {
+ if (sClipboardChangedListener != null) {
+ return;
+ }
+
+ sClipboardChangedListener =
+ new OnPrimaryClipChangedListener() {
+ @Override
+ public void onPrimaryClipChanged() {
+ Clipboard.sClipboardSequenceNumber.incrementAndGet();
+ }
+ };
+
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.addPrimaryClipChangedListener(sClipboardChangedListener);
+ }
+
+ /** Stop monitor clipboard sequence number. */
+ @WrapForJNI(calledFrom = "gecko")
+ private static void stopTrackingClipboardData(final Context context) {
+ if (sClipboardChangedListener == null) {
+ return;
+ }
+
+ final ClipboardManager cm =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.removePrimaryClipChangedListener(sClipboardChangedListener);
+ sClipboardChangedListener = null;
+ }
+
+ /** Get clipboard sequence number. */
+ @WrapForJNI(calledFrom = "gecko")
+ private static long getSequenceNumber(final Context context) {
+ return sClipboardSequenceNumber.get();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EnterpriseRoots.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EnterpriseRoots.java
new file mode 100644
index 0000000000..0aacef39a4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EnterpriseRoots.java
@@ -0,0 +1,96 @@
+/* -*- 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;
+
+import android.util.Log;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+// This class implements the functionality needed to find third-party root
+// certificates that have been added to the android CA store.
+public class EnterpriseRoots {
+ private static final String LOGTAG = "EnterpriseRoots";
+
+ // Gecko calls this function from C++ to find third-party root certificates
+ // it can use as trust anchors for TLS connections.
+ @WrapForJNI
+ private static byte[][] gatherEnterpriseRoots() {
+
+ // The KeyStore "AndroidCAStore" contains the certificates we're
+ // interested in.
+ final KeyStore ks;
+ try {
+ ks = KeyStore.getInstance("AndroidCAStore");
+ } catch (final KeyStoreException kse) {
+ Log.e(LOGTAG, "getInstance() failed", kse);
+ return new byte[0][0];
+ }
+ try {
+ ks.load(null);
+ } catch (final CertificateException ce) {
+ Log.e(LOGTAG, "load() failed", ce);
+ return new byte[0][0];
+ } catch (final IOException ioe) {
+ Log.e(LOGTAG, "load() failed", ioe);
+ return new byte[0][0];
+ } catch (final NoSuchAlgorithmException nsae) {
+ Log.e(LOGTAG, "load() failed", nsae);
+ return new byte[0][0];
+ }
+ // Given the KeyStore, we get an identifier for each object in it. For
+ // each one that is a Certificate, we try to distinguish between
+ // entries that shipped with the OS and entries that were added by the
+ // user or an administrator. The former we ignore and the latter we
+ // collect in an array of byte arrays and return.
+ final Enumeration<String> aliases;
+ try {
+ aliases = ks.aliases();
+ } catch (final KeyStoreException kse) {
+ Log.e(LOGTAG, "aliases() failed", kse);
+ return new byte[0][0];
+ }
+ final ArrayList<byte[]> roots = new ArrayList<byte[]>();
+ while (aliases.hasMoreElements()) {
+ final String alias = aliases.nextElement();
+ final boolean isCertificate;
+ try {
+ isCertificate = ks.isCertificateEntry(alias);
+ } catch (final KeyStoreException kse) {
+ Log.e(LOGTAG, "isCertificateEntry() failed", kse);
+ continue;
+ }
+ // Built-in certificate aliases start with "system:", whereas
+ // 3rd-party certificate aliases start with "user:". It's
+ // unfortunate to be relying on this implementation detail, but
+ // there appears to be no other way to differentiate between the
+ // two.
+ if (isCertificate && alias.startsWith("user:")) {
+ final Certificate certificate;
+ try {
+ certificate = ks.getCertificate(alias);
+ } catch (final KeyStoreException kse) {
+ Log.e(LOGTAG, "getCertificate() failed", kse);
+ continue;
+ }
+ try {
+ roots.add(certificate.getEncoded());
+ } catch (final CertificateEncodingException cee) {
+ Log.e(LOGTAG, "getEncoded() failed", cee);
+ }
+ }
+ }
+ Log.d(LOGTAG, "found " + roots.size() + " enterprise roots");
+ return roots.toArray(new byte[0][0]);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
new file mode 100644
index 0000000000..647ac5bc09
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
@@ -0,0 +1,588 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
+
+import android.os.Handler;
+import android.util.Log;
+import androidx.annotation.AnyThread;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.geckoview.BuildConfig;
+import org.mozilla.geckoview.GeckoResult;
+
+@RobocopTarget
+public final class EventDispatcher extends JNIObject {
+ private static final String LOGTAG = "GeckoEventDispatcher";
+
+ private static final EventDispatcher INSTANCE = new EventDispatcher();
+
+ /**
+ * The capacity of a HashMap is rounded up to the next power-of-2. Every time the size of the map
+ * goes beyond 75% of the capacity, the map is rehashed. Therefore, to empirically determine the
+ * initial capacity that avoids rehashing, we need to determine the initial size, divide it by
+ * 75%, and round up to the next power-of-2.
+ */
+ private static final int DEFAULT_UI_EVENTS_COUNT = 128; // Empirically measured
+
+ private static class Message {
+ final String type;
+ final GeckoBundle bundle;
+ final EventCallback callback;
+
+ Message(final String type, final GeckoBundle bundle, final EventCallback callback) {
+ this.type = type;
+ this.bundle = bundle;
+ this.callback = callback;
+ }
+ }
+
+ // GeckoBundle-based events.
+ private final MultiMap<String, BundleEventListener> mListeners =
+ new MultiMap<>(DEFAULT_UI_EVENTS_COUNT);
+ private Deque<Message> mPendingMessages = new ArrayDeque<>();
+
+ private boolean mAttachedToGecko;
+ private final NativeQueue mNativeQueue;
+ private final String mName;
+
+ private static Map<String, EventDispatcher> sDispatchers = new HashMap<>();
+
+ @ReflectionTarget
+ @WrapForJNI(calledFrom = "gecko")
+ public static EventDispatcher getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Gets a named EventDispatcher.
+ *
+ * <p>Named EventDispatchers can be used to communicate to Gecko's corresponding named
+ * EventDispatcher.
+ *
+ * <p>Messages for named EventDispatcher are queued by default when no listener is present. Queued
+ * messages will be released automatically when a listener is attached.
+ *
+ * <p>A named EventDispatcher needs to be disposed manually by calling {@link #shutdown} when it
+ * is not needed anymore.
+ *
+ * @param name Name for this EventDispatcher.
+ * @return the existing named EventDispatcher for a given name or a newly created one if it
+ * doesn't exist.
+ */
+ @ReflectionTarget
+ @WrapForJNI(calledFrom = "gecko")
+ public static EventDispatcher byName(final String name) {
+ synchronized (sDispatchers) {
+ EventDispatcher dispatcher = sDispatchers.get(name);
+
+ if (dispatcher == null) {
+ dispatcher = new EventDispatcher(name);
+ sDispatchers.put(name, dispatcher);
+ }
+
+ return dispatcher;
+ }
+ }
+
+ /* package */ EventDispatcher() {
+ mNativeQueue = GeckoThread.getNativeQueue();
+ mName = null;
+ }
+
+ /* package */ EventDispatcher(final String name) {
+ mNativeQueue = GeckoThread.getNativeQueue();
+ mName = name;
+ }
+
+ public EventDispatcher(final NativeQueue queue) {
+ mNativeQueue = queue;
+ mName = null;
+ }
+
+ private boolean isReadyForDispatchingToGecko() {
+ return mNativeQueue.isReady();
+ }
+
+ @WrapForJNI
+ @Override // JNIObject
+ protected native void disposeNative();
+
+ @WrapForJNI(stubName = "Shutdown")
+ protected native void shutdownNative();
+
+ @WrapForJNI private static final int DETACHED = 0;
+ @WrapForJNI private static final int ATTACHED = 1;
+ @WrapForJNI private static final int REATTACHING = 2;
+
+ @WrapForJNI(calledFrom = "gecko")
+ private synchronized void setAttachedToGecko(final int state) {
+ if (mAttachedToGecko && state == DETACHED) {
+ dispose(false);
+ }
+ mAttachedToGecko = (state == ATTACHED);
+ }
+
+ /**
+ * Shuts down this EventDispatcher and release resources.
+ *
+ * <p>Only named EventDispatcher can be shut down manually. A shut down EventDispatcher will not
+ * receive any further messages.
+ */
+ public void shutdown() {
+ if (mName == null) {
+ throw new RuntimeException("Only named EventDispatcher's can be shut down.");
+ }
+
+ mAttachedToGecko = false;
+ shutdownNative();
+ dispose(false);
+
+ synchronized (sDispatchers) {
+ sDispatchers.put(mName, null);
+ }
+ }
+
+ private void dispose(final boolean force) {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+ if (geckoHandler == null) {
+ return;
+ }
+
+ geckoHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (force || !mAttachedToGecko) {
+ disposeNative();
+ }
+ }
+ });
+ }
+
+ public void registerUiThreadListener(final BundleEventListener listener, final String... events) {
+ try {
+ synchronized (mListeners) {
+ for (final String event : events) {
+ if (!BuildConfig.RELEASE_OR_BETA && mListeners.containsEntry(event, listener)) {
+ throw new IllegalStateException("Already registered " + event);
+ }
+ mListeners.add(event, listener);
+ }
+ flush(events);
+ }
+ } catch (final Exception e) {
+ throw new IllegalArgumentException("Invalid new list type", e);
+ }
+ }
+
+ public void unregisterUiThreadListener(
+ final BundleEventListener listener, final String... events) {
+ synchronized (mListeners) {
+ for (final String event : events) {
+ if (!mListeners.remove(event, listener) && !BuildConfig.RELEASE_OR_BETA) {
+ throw new IllegalArgumentException(event + " was not registered");
+ }
+ }
+ }
+ }
+
+ @WrapForJNI
+ private native boolean hasGeckoListener(final String event);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private native void dispatchToGecko(
+ final String event, final GeckoBundle data, final EventCallback callback);
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param type Event type
+ * @param message Bundle message
+ */
+ public void dispatch(final String type, final GeckoBundle message) {
+ dispatch(type, message, /* callback */ null);
+ }
+
+ private abstract class CallbackResult<T> extends GeckoResult<T> implements EventCallback {
+ @Override
+ public void sendError(final Object response) {
+ completeExceptionally(new QueryException(response));
+ }
+ }
+
+ public class QueryException extends Exception {
+ public final Object data;
+
+ public QueryException(final Object data) {
+ this.data = data;
+ }
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes when the event handler returns.
+ *
+ * @param type Event type
+ */
+ public GeckoResult<Void> queryVoid(final String type) {
+ return queryVoid(type, null);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes when the event handler returns.
+ *
+ * @param type Event type
+ * @param message GeckoBundle message
+ */
+ public GeckoResult<Void> queryVoid(final String type, final GeckoBundle message) {
+ return query(type, message);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given boolean value returned by the handler.
+ *
+ * @param type Event type
+ */
+ public GeckoResult<Boolean> queryBoolean(final String type) {
+ return queryBoolean(type, null);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given boolean value returned by the handler.
+ *
+ * @param type Event type
+ * @param message GeckoBundle message
+ */
+ public GeckoResult<Boolean> queryBoolean(final String type, final GeckoBundle message) {
+ return query(type, message);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given String value returned by the handler.
+ *
+ * @param type Event type
+ */
+ public GeckoResult<String> queryString(final String type) {
+ return queryString(type, null);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given String value returned by the handler.
+ *
+ * @param type Event type
+ * @param message GeckoBundle message
+ */
+ public GeckoResult<String> queryString(final String type, final GeckoBundle message) {
+ return query(type, message);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given {@link GeckoBundle} value returned by the
+ * handler.
+ *
+ * @param type Event type
+ */
+ public GeckoResult<GeckoBundle> queryBundle(final String type) {
+ return queryBundle(type, null);
+ }
+
+ /**
+ * Query event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * <p>The returned GeckoResult completes with the given {@link GeckoBundle} value returned by the
+ * handler.
+ *
+ * @param type Event type
+ * @param message GeckoBundle message
+ */
+ public GeckoResult<GeckoBundle> queryBundle(final String type, final GeckoBundle message) {
+ return query(type, message);
+ }
+
+ private <T> GeckoResult<T> query(final String type, final GeckoBundle message) {
+ final CallbackResult<T> result =
+ new CallbackResult<T>() {
+ @Override
+ @SuppressWarnings("unchecked") // Not a lot we can do about this :(
+ public void sendSuccess(final Object response) {
+ complete((T) response);
+ }
+ };
+
+ dispatch(type, message, result);
+ return result;
+ }
+
+ /**
+ * Flushes pending messages of given types.
+ *
+ * <p>All unhandled messages are put into a pending state by default for named EventDispatcher
+ * obtained from {@link #byName}.
+ *
+ * @param types Types of message to flush.
+ */
+ private void flush(final String[] types) {
+ final Set<String> typeSet = new HashSet<>(Arrays.asList(types));
+
+ final Deque<Message> pendingMessages;
+ synchronized (mPendingMessages) {
+ pendingMessages = mPendingMessages;
+ mPendingMessages = new ArrayDeque<>(pendingMessages.size());
+ }
+
+ Message message;
+ while (!pendingMessages.isEmpty()) {
+ message = pendingMessages.removeFirst();
+ if (typeSet.contains(message.type)) {
+ dispatchToThreads(message.type, message.bundle, message.callback);
+ } else {
+ synchronized (mPendingMessages) {
+ mPendingMessages.addLast(message);
+ }
+ }
+ }
+ }
+
+ /**
+ * Dispatch event to any registered Bundle listeners (non-Gecko thread listeners).
+ *
+ * @param type Event type
+ * @param message Bundle message
+ * @param callback Optional object for callbacks from events.
+ */
+ @AnyThread
+ private void dispatch(
+ final String type, final GeckoBundle message, final EventCallback callback) {
+ final boolean isGeckoReady;
+ synchronized (this) {
+ isGeckoReady = isReadyForDispatchingToGecko();
+ if (isGeckoReady && mAttachedToGecko && hasGeckoListener(type)) {
+ dispatchToGecko(type, message, JavaCallbackDelegate.wrap(callback));
+ return;
+ }
+ }
+
+ dispatchToThreads(type, message, callback, isGeckoReady);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private boolean dispatchToThreads(
+ final String type, final GeckoBundle message, final EventCallback callback) {
+ return dispatchToThreads(type, message, callback, /* isGeckoReady */ true);
+ }
+
+ private boolean dispatchToThreads(
+ final String type,
+ final GeckoBundle message,
+ final EventCallback callback,
+ final boolean isGeckoReady) {
+ // We need to hold the lock throughout dispatching, to ensure the listeners list
+ // is consistent, while we iterate over it. We don't have to worry about listeners
+ // running for a long time while we have the lock, because the listeners will run
+ // on a separate thread.
+ synchronized (mListeners) {
+ if (mListeners.containsKey(type)) {
+ // Use a delegate to make sure callbacks happen on a specific thread.
+ final EventCallback wrappedCallback = JavaCallbackDelegate.wrap(callback);
+
+ // Event listeners will call | callback.sendError | if applicable.
+ for (final BundleEventListener listener : mListeners.get(type)) {
+ ThreadUtils.getUiHandler()
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ final Double startTime = GeckoJavaSampler.tryToGetProfilerTime();
+ listener.handleMessage(type, message, wrappedCallback);
+ GeckoJavaSampler.addMarker(
+ "EventDispatcher handleMessage", startTime, null, type);
+ }
+ });
+ }
+ return true;
+ }
+ }
+
+ if (!isGeckoReady) {
+ // Usually, we discard an event if there is no listeners for it by
+ // the time of the dispatch. However, if Gecko(View) is not ready and
+ // there is no listener for this event that's possibly headed to
+ // Gecko, we make a special exception to queue this event until
+ // Gecko(View) is ready. This way, Gecko can first register its
+ // listeners, and accept the event when it is ready.
+ mNativeQueue.queueUntilReady(
+ this,
+ "dispatchToGecko",
+ String.class,
+ type,
+ GeckoBundle.class,
+ message,
+ EventCallback.class,
+ JavaCallbackDelegate.wrap(callback));
+ return true;
+ }
+
+ // Named EventDispatchers use pending messages
+ if (mName != null) {
+ synchronized (mPendingMessages) {
+ mPendingMessages.addLast(new Message(type, message, callback));
+ }
+ return true;
+ }
+
+ final String error = "No listener for " + type;
+ if (callback != null) {
+ callback.sendError(error);
+ }
+
+ Log.w(LOGTAG, error);
+ return false;
+ }
+
+ @WrapForJNI
+ public boolean hasListener(final String event) {
+ synchronized (mListeners) {
+ return mListeners.containsKey(event);
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ dispose(true);
+ }
+
+ private static class NativeCallbackDelegate extends JNIObject implements EventCallback {
+ @WrapForJNI(calledFrom = "gecko")
+ private NativeCallbackDelegate() {}
+
+ @Override // JNIObject
+ protected void disposeNative() {
+ // We dispose in finalize().
+ throw new UnsupportedOperationException();
+ }
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // EventCallback
+ public native void sendSuccess(Object response);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // EventCallback
+ public native void sendError(Object response);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ @Override // Object
+ protected native void finalize();
+ }
+
+ private static class JavaCallbackDelegate implements EventCallback {
+ private final Thread mOriginalThread = Thread.currentThread();
+ private final EventCallback mCallback;
+
+ public static EventCallback wrap(final EventCallback callback) {
+ if (callback == null) {
+ return null;
+ }
+ if (callback instanceof NativeCallbackDelegate) {
+ // NativeCallbackDelegate always posts to Gecko thread if needed.
+ return callback;
+ }
+ return new JavaCallbackDelegate(callback);
+ }
+
+ JavaCallbackDelegate(final EventCallback callback) {
+ mCallback = callback;
+ }
+
+ private void makeCallback(final boolean callSuccess, final Object rawResponse) {
+ final Object response;
+ if (rawResponse instanceof Number) {
+ // There is ambiguity because a number can be converted to either int or
+ // double, so e.g. the user can be expecting a double when we give it an
+ // int. To avoid these pitfalls, we disallow all numbers. The workaround
+ // is to wrap the number in a JS object / GeckoBundle, which supports
+ // type coersion for numbers.
+ throw new UnsupportedOperationException("Cannot use number as Java callback result");
+ } else if (rawResponse != null && rawResponse.getClass().isArray()) {
+ // Same with arrays.
+ throw new UnsupportedOperationException("Cannot use arrays as Java callback result");
+ } else if (rawResponse instanceof Character) {
+ response = rawResponse.toString();
+ } else {
+ response = rawResponse;
+ }
+
+ // Call back synchronously if we happen to be on the same thread as the thread
+ // making the original request.
+ if (ThreadUtils.isOnThread(mOriginalThread)) {
+ if (callSuccess) {
+ mCallback.sendSuccess(response);
+ } else {
+ mCallback.sendError(response);
+ }
+ return;
+ }
+
+ // Make callback on the thread of the original request, if the original thread
+ // is the UI or Gecko thread. Otherwise default to the background thread.
+ final Handler handler =
+ mOriginalThread == ThreadUtils.getUiThread()
+ ? ThreadUtils.getUiHandler()
+ : mOriginalThread == ThreadUtils.sGeckoThread
+ ? ThreadUtils.sGeckoHandler
+ : ThreadUtils.getBackgroundHandler();
+ final EventCallback callback = mCallback;
+
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (callSuccess) {
+ callback.sendSuccess(response);
+ } else {
+ callback.sendError(response);
+ }
+ }
+ });
+ }
+
+ @Override // EventCallback
+ public void sendSuccess(final Object response) {
+ makeCallback(/* success */ true, response);
+ }
+
+ @Override // EventCallback
+ public void sendError(final Object response) {
+ makeCallback(/* success */ false, response);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
new file mode 100644
index 0000000000..568fc3a0bb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java
@@ -0,0 +1,1614 @@
+/* -*- 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;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.LocaleList;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.Display;
+import android.view.InputDevice;
+import android.view.WindowManager;
+import android.webkit.MimeTypeMap;
+import androidx.annotation.Nullable;
+import androidx.collection.SimpleArrayMap;
+import androidx.core.content.res.ResourcesCompat;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Locale;
+import java.util.StringTokenizer;
+import org.jetbrains.annotations.NotNull;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.InputDeviceUtils;
+import org.mozilla.gecko.util.ProxySelector;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.geckoview.BuildConfig;
+import org.mozilla.geckoview.CrashHandler;
+import org.mozilla.geckoview.GeckoResult;
+import org.mozilla.geckoview.R;
+
+public class GeckoAppShell {
+ private static final String LOGTAG = "GeckoAppShell";
+
+ /*
+ * Keep these values consistent with |SensorType| in HalSensor.h
+ */
+ public static final int SENSOR_ORIENTATION = 0;
+ public static final int SENSOR_ACCELERATION = 1;
+ public static final int SENSOR_PROXIMITY = 2;
+ public static final int SENSOR_LINEAR_ACCELERATION = 3;
+ public static final int SENSOR_GYROSCOPE = 4;
+ public static final int SENSOR_LIGHT = 5;
+ public static final int SENSOR_ROTATION_VECTOR = 6;
+ public static final int SENSOR_GAME_ROTATION_VECTOR = 7;
+
+ // We have static members only.
+ private GeckoAppShell() {}
+
+ // Name for app-scoped prefs
+ public static final String APP_PREFS_NAME = "GeckoApp";
+
+ private static class GeckoCrashHandler extends CrashHandler {
+
+ public GeckoCrashHandler(final Class<? extends Service> handlerService) {
+ super(handlerService);
+ }
+
+ @Override
+ public String getAppPackageName() {
+ final Context appContext = getAppContext();
+ if (appContext == null) {
+ return "<unknown>";
+ }
+ return appContext.getPackageName();
+ }
+
+ @Override
+ public Context getAppContext() {
+ return getApplicationContext();
+ }
+
+ @SuppressLint("ApplySharedPref")
+ @Override
+ public boolean reportException(final Thread thread, final Throwable exc) {
+ try {
+ if (exc instanceof OutOfMemoryError) {
+ final SharedPreferences prefs =
+ getApplicationContext().getSharedPreferences(APP_PREFS_NAME, 0);
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREFS_OOM_EXCEPTION, true);
+
+ // Synchronously write to disk so we know it's done before we
+ // shutdown
+ editor.commit();
+ }
+
+ reportJavaCrash(exc, getExceptionStackTrace(exc));
+
+ } catch (final Throwable e) {
+ }
+
+ // reportJavaCrash should have caused us to hard crash. If we're still here,
+ // it probably means Gecko is not loaded, and we should do something else.
+ if (BuildConfig.MOZ_CRASHREPORTER && BuildConfig.MOZILLA_OFFICIAL) {
+ // Only use Java crash reporter if enabled on official build.
+ return super.reportException(thread, exc);
+ }
+ return false;
+ }
+ }
+
+ private static String sAppNotes;
+ private static CrashHandler sCrashHandler;
+
+ public static synchronized CrashHandler ensureCrashHandling(
+ final Class<? extends Service> handler) {
+ if (sCrashHandler == null) {
+ sCrashHandler = new GeckoCrashHandler(handler);
+ }
+
+ return sCrashHandler;
+ }
+
+ private static Class<? extends Service> sCrashHandlerService;
+
+ public static synchronized void setCrashHandlerService(
+ final Class<? extends Service> handlerService) {
+ sCrashHandlerService = handlerService;
+ }
+
+ public static synchronized Class<? extends Service> getCrashHandlerService() {
+ return sCrashHandlerService;
+ }
+
+ @WrapForJNI(exceptionMode = "ignore")
+ public static synchronized String getAppNotes() {
+ return sAppNotes;
+ }
+
+ public static synchronized void appendAppNotesToCrashReport(final String notes) {
+ if (sAppNotes == null) {
+ sAppNotes = notes;
+ } else {
+ sAppNotes += '\n' + notes;
+ }
+ }
+
+ private static volatile boolean locationHighAccuracyEnabled;
+ private static volatile boolean locationListeningRequested = false;
+ private static volatile boolean locationPaused = false;
+
+ // See also HardwareUtils.LOW_MEMORY_THRESHOLD_MB.
+ private static final int HIGH_MEMORY_DEVICE_THRESHOLD_MB = 768;
+
+ private static int sDensityDpi;
+ private static Float sDensity;
+ private static int sScreenDepth;
+ private static boolean sUseMaxScreenDepth;
+ private static Float sScreenRefreshRate;
+
+ /* Is the value in sVibrationEndTime valid? */
+ private static boolean sVibrationMaybePlaying;
+
+ /* Time (in System.nanoTime() units) when the currently-playing vibration
+ * is scheduled to end. This value is valid only when
+ * sVibrationMaybePlaying is true. */
+ private static long sVibrationEndTime;
+
+ private static Sensor gAccelerometerSensor;
+ private static Sensor gLinearAccelerometerSensor;
+ private static Sensor gGyroscopeSensor;
+ private static Sensor gOrientationSensor;
+ private static Sensor gLightSensor;
+ private static Sensor gRotationVectorSensor;
+ private static Sensor gGameRotationVectorSensor;
+
+ /*
+ * Keep in sync with constants found here:
+ * http://searchfox.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl
+ */
+ public static final int WPL_STATE_START = 0x00000001;
+ public static final int WPL_STATE_STOP = 0x00000010;
+ public static final int WPL_STATE_IS_DOCUMENT = 0x00020000;
+ public static final int WPL_STATE_IS_NETWORK = 0x00040000;
+
+ /* Keep in sync with constants found here:
+ http://searchfox.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public static final int LINK_TYPE_UNKNOWN = 0;
+ public static final int LINK_TYPE_ETHERNET = 1;
+ public static final int LINK_TYPE_USB = 2;
+ public static final int LINK_TYPE_WIFI = 3;
+ public static final int LINK_TYPE_WIMAX = 4;
+ public static final int LINK_TYPE_MOBILE = 9;
+
+ public static final String PREFS_OOM_EXCEPTION = "OOMException";
+
+ /* The Android-side API: API methods that Android calls */
+
+ // helper methods
+ @WrapForJNI
+ /* package */ static native void reportJavaCrash(Throwable exc, String stackTrace);
+
+ private static Rect sScreenSizeOverride;
+
+ @WrapForJNI(stubName = "NotifyObservers", dispatchTo = "gecko")
+ private static native void nativeNotifyObservers(String topic, String data);
+
+ @WrapForJNI(stubName = "AppendAppNotesToCrashReport", dispatchTo = "gecko")
+ public static native void nativeAppendAppNotesToCrashReport(final String notes);
+
+ @RobocopTarget
+ public static void notifyObservers(final String topic, final String data) {
+ notifyObservers(topic, data, GeckoThread.State.RUNNING);
+ }
+
+ public static void notifyObservers(
+ final String topic, final String data, final GeckoThread.State state) {
+ if (GeckoThread.isStateAtLeast(state)) {
+ nativeNotifyObservers(topic, data);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ state,
+ GeckoAppShell.class,
+ "nativeNotifyObservers",
+ String.class,
+ topic,
+ String.class,
+ data);
+ }
+ }
+
+ /*
+ * The Gecko-side API: API methods that Gecko calls
+ */
+
+ @WrapForJNI(exceptionMode = "ignore")
+ private static String getExceptionStackTrace(final Throwable e) {
+ return CrashHandler.getExceptionStackTrace(CrashHandler.getRootException(e));
+ }
+
+ @WrapForJNI(exceptionMode = "ignore")
+ private static synchronized void handleUncaughtException(final Throwable e) {
+ if (sCrashHandler != null) {
+ sCrashHandler.uncaughtException(null, e);
+ }
+ }
+
+ private static float getLocationAccuracy(final Location location) {
+ final float radius = location.getAccuracy();
+ return (location.hasAccuracy() && radius > 0) ? radius : 1001;
+ }
+
+ private static Location determineReliableLocation(
+ @NotNull final Location locA, @NotNull final Location locB) {
+ // The 6 seconds were chosen arbitrarily
+ final long closeTime = 6000000000L;
+ final boolean isNearSameTime =
+ Math.abs((locA.getElapsedRealtimeNanos() - locB.getElapsedRealtimeNanos())) <= closeTime;
+ final boolean isAMoreAccurate = getLocationAccuracy(locA) < getLocationAccuracy(locB);
+ final boolean isAMoreRecent = locA.getElapsedRealtimeNanos() > locB.getElapsedRealtimeNanos();
+ if (isNearSameTime) {
+ return isAMoreAccurate ? locA : locB;
+ }
+ return isAMoreRecent ? locA : locB;
+ }
+
+ // Permissions are explicitly checked when requesting content permission.
+ @SuppressLint("MissingPermission")
+ private static @Nullable Location getLastKnownLocation(final LocationManager lm) {
+ Location lastKnownLocation = null;
+ final List<String> providers = lm.getAllProviders();
+
+ for (final String provider : providers) {
+ final Location location = lm.getLastKnownLocation(provider);
+ if (location == null) {
+ continue;
+ }
+
+ if (lastKnownLocation == null) {
+ lastKnownLocation = location;
+ continue;
+ }
+ lastKnownLocation = determineReliableLocation(lastKnownLocation, location);
+ }
+ return lastKnownLocation;
+ }
+
+ // Toggles the location listeners on/off, which will then provide/stop location information
+ @WrapForJNI(calledFrom = "gecko")
+ private static synchronized boolean enableLocationUpdates(final boolean enable) {
+ locationListeningRequested = enable;
+ final boolean canListen = updateLocationListeners();
+ if (!canListen && locationListeningRequested) {
+ // Didn't successfully start listener when requested
+ locationListeningRequested = false;
+ }
+ return canListen;
+ }
+
+ // Permissions are explicitly checked when requesting content permission.
+ @SuppressLint("MissingPermission")
+ private static synchronized boolean updateLocationListeners() {
+ final boolean shouldListen = locationListeningRequested && !locationPaused;
+ final LocationManager lm = getLocationManager(getApplicationContext());
+ if (lm == null) {
+ return false;
+ }
+
+ if (!shouldListen) {
+ // Could not complete request, because paused
+ if (locationListeningRequested) {
+ return false;
+ }
+ lm.removeUpdates(sAndroidListeners);
+ return true;
+ }
+
+ if (!lm.isProviderEnabled(LocationManager.GPS_PROVIDER)
+ && !lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
+ return false;
+ }
+
+ final Location lastKnownLocation = getLastKnownLocation(lm);
+ if (lastKnownLocation != null) {
+ sAndroidListeners.onLocationChanged(lastKnownLocation);
+ }
+
+ final Criteria criteria = new Criteria();
+ criteria.setSpeedRequired(false);
+ criteria.setBearingRequired(false);
+ criteria.setAltitudeRequired(false);
+ if (locationHighAccuracyEnabled) {
+ criteria.setAccuracy(Criteria.ACCURACY_FINE);
+ } else {
+ criteria.setAccuracy(Criteria.ACCURACY_COARSE);
+ }
+
+ final String provider = lm.getBestProvider(criteria, true);
+ if (provider == null) {
+ return false;
+ }
+
+ final Looper l = Looper.getMainLooper();
+ lm.requestLocationUpdates(provider, 100, 0.5f, sAndroidListeners, l);
+ return true;
+ }
+
+ public static void pauseLocation() {
+ locationPaused = true;
+ updateLocationListeners();
+ }
+
+ public static void resumeLocation() {
+ locationPaused = false;
+ updateLocationListeners();
+ }
+
+ private static LocationManager getLocationManager(final Context context) {
+ try {
+ return (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+ } catch (final NoSuchFieldError e) {
+ // Some Tegras throw exceptions about missing the CONTROL_LOCATION_UPDATES permission,
+ // which allows enabling/disabling location update notifications from the cell radio.
+ // CONTROL_LOCATION_UPDATES is not for use by normal applications, but we might be
+ // hitting this problem if the Tegras are confused about missing cell radios.
+ Log.e(LOGTAG, "LOCATION_SERVICE not found?!", e);
+ return null;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableLocationHighAccuracy(final boolean enable) {
+ locationHighAccuracyEnabled = enable;
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ /* package */ static native void onSensorChanged(
+ int halType, float x, float y, float z, float w, long time);
+
+ @WrapForJNI(calledFrom = "any", dispatchTo = "gecko")
+ /* package */ static native void onLocationChanged(
+ double latitude,
+ double longitude,
+ double altitude,
+ float accuracy,
+ float altitudeAccuracy,
+ float heading,
+ float speed);
+
+ private static class AndroidListeners implements SensorEventListener, LocationListener {
+ @Override
+ public void onAccuracyChanged(final Sensor sensor, final int accuracy) {}
+
+ @Override
+ public void onSensorChanged(final SensorEvent s) {
+ final int sensorType = s.sensor.getType();
+ int halType = 0;
+ float x = 0.0f, y = 0.0f, z = 0.0f, w = 0.0f;
+ // SensorEvent timestamp is in nanoseconds, Gecko expects microseconds.
+ final long time = s.timestamp / 1000;
+
+ switch (sensorType) {
+ case Sensor.TYPE_ACCELEROMETER:
+ case Sensor.TYPE_LINEAR_ACCELERATION:
+ case Sensor.TYPE_ORIENTATION:
+ if (sensorType == Sensor.TYPE_ACCELEROMETER) {
+ halType = SENSOR_ACCELERATION;
+ } else if (sensorType == Sensor.TYPE_LINEAR_ACCELERATION) {
+ halType = SENSOR_LINEAR_ACCELERATION;
+ } else {
+ halType = SENSOR_ORIENTATION;
+ }
+ x = s.values[0];
+ y = s.values[1];
+ z = s.values[2];
+ break;
+
+ case Sensor.TYPE_GYROSCOPE:
+ halType = SENSOR_GYROSCOPE;
+ x = (float) Math.toDegrees(s.values[0]);
+ y = (float) Math.toDegrees(s.values[1]);
+ z = (float) Math.toDegrees(s.values[2]);
+ break;
+
+ case Sensor.TYPE_LIGHT:
+ halType = SENSOR_LIGHT;
+ x = s.values[0];
+ break;
+
+ case Sensor.TYPE_ROTATION_VECTOR:
+ case Sensor.TYPE_GAME_ROTATION_VECTOR: // API >= 18
+ halType =
+ (sensorType == Sensor.TYPE_ROTATION_VECTOR
+ ? SENSOR_ROTATION_VECTOR
+ : SENSOR_GAME_ROTATION_VECTOR);
+ x = s.values[0];
+ y = s.values[1];
+ z = s.values[2];
+ if (s.values.length >= 4) {
+ w = s.values[3];
+ } else {
+ // s.values[3] was optional in API <= 18, so we need to compute it
+ // The values form a unit quaternion, so we can compute the angle of
+ // rotation purely based on the given 3 values.
+ w =
+ 1.0f
+ - s.values[0] * s.values[0]
+ - s.values[1] * s.values[1]
+ - s.values[2] * s.values[2];
+ w = (w > 0.0f) ? (float) Math.sqrt(w) : 0.0f;
+ }
+ break;
+ }
+
+ GeckoAppShell.onSensorChanged(halType, x, y, z, w, time);
+ }
+
+ // Geolocation.
+ @Override
+ public void onLocationChanged(final Location location) {
+ // No logging here: user-identifying information.
+
+ final double altitude = location.hasAltitude() ? location.getAltitude() : Double.NaN;
+
+ final float accuracy = location.hasAccuracy() ? location.getAccuracy() : Float.NaN;
+
+ final float altitudeAccuracy =
+ Build.VERSION.SDK_INT >= 26 && location.hasVerticalAccuracy()
+ ? location.getVerticalAccuracyMeters()
+ : Float.NaN;
+
+ final float speed = location.hasSpeed() ? location.getSpeed() : Float.NaN;
+
+ final float heading = location.hasBearing() ? location.getBearing() : Float.NaN;
+
+ // nsGeoPositionCoords will convert NaNs to null for optional
+ // properties of the JavaScript Coordinates object.
+ GeckoAppShell.onLocationChanged(
+ location.getLatitude(),
+ location.getLongitude(),
+ altitude,
+ accuracy,
+ altitudeAccuracy,
+ heading,
+ speed);
+ }
+
+ @Override
+ public void onProviderDisabled(final String provider) {}
+
+ @Override
+ public void onProviderEnabled(final String provider) {}
+
+ @Override
+ public void onStatusChanged(final String provider, final int status, final Bundle extras) {}
+ }
+
+ private static final AndroidListeners sAndroidListeners = new AndroidListeners();
+
+ private static SimpleArrayMap<String, PowerManager.WakeLock> sWakeLocks;
+
+ /** Wake-lock for the CPU. */
+ static final String WAKE_LOCK_CPU = "cpu";
+
+ /** Wake-lock for the screen. */
+ static final String WAKE_LOCK_SCREEN = "screen";
+
+ /** Wake-lock for the audio-playing, eqaul to LOCK_CPU. */
+ static final String WAKE_LOCK_AUDIO_PLAYING = "audio-playing";
+
+ /** Wake-lock for the video-playing, eqaul to LOCK_SCREEN.. */
+ static final String WAKE_LOCK_VIDEO_PLAYING = "video-playing";
+
+ static final int WAKE_LOCKS_COUNT = 2;
+
+ /** No one holds the wake-lock. */
+ static final int WAKE_LOCK_STATE_UNLOCKED = 0;
+
+ /** The wake-lock is held by a foreground window. */
+ static final int WAKE_LOCK_STATE_LOCKED_FOREGROUND = 1;
+
+ /** The wake-lock is held by a background window. */
+ static final int WAKE_LOCK_STATE_LOCKED_BACKGROUND = 2;
+
+ @SuppressLint("Wakelock") // We keep the wake lock independent from the function
+ // scope, so we need to suppress the linter warning.
+ private static void setWakeLockState(final String lock, final int state) {
+ if (sWakeLocks == null) {
+ sWakeLocks = new SimpleArrayMap<>(WAKE_LOCKS_COUNT);
+ }
+
+ PowerManager.WakeLock wl = sWakeLocks.get(lock);
+
+ // we should still hold the lock for background audio.
+ if (WAKE_LOCK_AUDIO_PLAYING.equals(lock) && state == WAKE_LOCK_STATE_LOCKED_BACKGROUND) {
+ return;
+ }
+
+ if (state == WAKE_LOCK_STATE_LOCKED_FOREGROUND && wl == null) {
+ final PowerManager pm =
+ (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE);
+
+ if (WAKE_LOCK_CPU.equals(lock) || WAKE_LOCK_AUDIO_PLAYING.equals(lock)) {
+ wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lock);
+ } else if (WAKE_LOCK_SCREEN.equals(lock) || WAKE_LOCK_VIDEO_PLAYING.equals(lock)) {
+ // ON_AFTER_RELEASE is set, the user activity timer will be reset when the
+ // WakeLock is released, causing the illumination to remain on a bit longer.
+ wl =
+ pm.newWakeLock(
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, lock);
+ } else {
+ Log.w(LOGTAG, "Unsupported wake-lock: " + lock);
+ return;
+ }
+
+ wl.acquire();
+ sWakeLocks.put(lock, wl);
+ } else if (state != WAKE_LOCK_STATE_LOCKED_FOREGROUND && wl != null) {
+ wl.release();
+ sWakeLocks.remove(lock);
+ }
+ }
+
+ @SuppressWarnings("fallthrough")
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableSensor(final int aSensortype) {
+ final SensorManager sm =
+ (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
+
+ switch (aSensortype) {
+ case SENSOR_GAME_ROTATION_VECTOR:
+ if (gGameRotationVectorSensor == null) {
+ gGameRotationVectorSensor = sm.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR);
+ }
+ if (gGameRotationVectorSensor != null) {
+ sm.registerListener(
+ sAndroidListeners, gGameRotationVectorSensor, SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ if (gGameRotationVectorSensor != null) {
+ break;
+ }
+ // Fallthrough
+
+ case SENSOR_ROTATION_VECTOR:
+ if (gRotationVectorSensor == null) {
+ gRotationVectorSensor = sm.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
+ }
+ if (gRotationVectorSensor != null) {
+ sm.registerListener(
+ sAndroidListeners, gRotationVectorSensor, SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ if (gRotationVectorSensor != null) {
+ break;
+ }
+ // Fallthrough
+
+ case SENSOR_ORIENTATION:
+ if (gOrientationSensor == null) {
+ gOrientationSensor = sm.getDefaultSensor(Sensor.TYPE_ORIENTATION);
+ }
+ if (gOrientationSensor != null) {
+ sm.registerListener(
+ sAndroidListeners, gOrientationSensor, SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case SENSOR_ACCELERATION:
+ if (gAccelerometerSensor == null) {
+ gAccelerometerSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ }
+ if (gAccelerometerSensor != null) {
+ sm.registerListener(
+ sAndroidListeners, gAccelerometerSensor, SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case SENSOR_LIGHT:
+ if (gLightSensor == null) {
+ gLightSensor = sm.getDefaultSensor(Sensor.TYPE_LIGHT);
+ }
+ if (gLightSensor != null) {
+ sm.registerListener(sAndroidListeners, gLightSensor, SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ break;
+
+ case SENSOR_LINEAR_ACCELERATION:
+ if (gLinearAccelerometerSensor == null) {
+ gLinearAccelerometerSensor = sm.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
+ }
+ if (gLinearAccelerometerSensor != null) {
+ sm.registerListener(
+ sAndroidListeners, gLinearAccelerometerSensor, SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ case SENSOR_GYROSCOPE:
+ if (gGyroscopeSensor == null) {
+ gGyroscopeSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
+ }
+ if (gGyroscopeSensor != null) {
+ sm.registerListener(
+ sAndroidListeners, gGyroscopeSensor, SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ break;
+
+ default:
+ Log.w(LOGTAG, "Error! Can't enable unknown SENSOR type " + aSensortype);
+ }
+ }
+
+ @SuppressWarnings("fallthrough")
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableSensor(final int aSensortype) {
+ final SensorManager sm =
+ (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
+
+ switch (aSensortype) {
+ case SENSOR_GAME_ROTATION_VECTOR:
+ if (gGameRotationVectorSensor != null) {
+ sm.unregisterListener(sAndroidListeners, gGameRotationVectorSensor);
+ break;
+ }
+ // Fallthrough
+
+ case SENSOR_ROTATION_VECTOR:
+ if (gRotationVectorSensor != null) {
+ sm.unregisterListener(sAndroidListeners, gRotationVectorSensor);
+ break;
+ }
+ // Fallthrough
+
+ case SENSOR_ORIENTATION:
+ if (gOrientationSensor != null) {
+ sm.unregisterListener(sAndroidListeners, gOrientationSensor);
+ }
+ break;
+
+ case SENSOR_ACCELERATION:
+ if (gAccelerometerSensor != null) {
+ sm.unregisterListener(sAndroidListeners, gAccelerometerSensor);
+ }
+ break;
+
+ case SENSOR_LIGHT:
+ if (gLightSensor != null) {
+ sm.unregisterListener(sAndroidListeners, gLightSensor);
+ }
+ break;
+
+ case SENSOR_LINEAR_ACCELERATION:
+ if (gLinearAccelerometerSensor != null) {
+ sm.unregisterListener(sAndroidListeners, gLinearAccelerometerSensor);
+ }
+ break;
+
+ case SENSOR_GYROSCOPE:
+ if (gGyroscopeSensor != null) {
+ sm.unregisterListener(sAndroidListeners, gGyroscopeSensor);
+ }
+ break;
+ default:
+ Log.w(LOGTAG, "Error! Can't disable unknown SENSOR type " + aSensortype);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void moveTaskToBack() {
+ // This is a vestige, to be removed as full-screen support for GeckoView is implemented.
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean hasHWVP8Encoder() {
+ return HardwareCodecCapabilityUtils.hasHWVP8(true /* aIsEncoder */);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean hasHWVP8Decoder() {
+ return HardwareCodecCapabilityUtils.hasHWVP8(false /* aIsEncoder */);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String getExtensionFromMimeType(final String aMimeType) {
+ return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String getMimeTypeFromExtensions(final String aFileExt) {
+ final StringTokenizer st = new StringTokenizer(aFileExt, ".,; ");
+ String type = null;
+ String subType = null;
+ while (st.hasMoreElements()) {
+ final String ext = st.nextToken();
+ final String mt = getMimeTypeFromExtension(ext);
+ if (mt == null) continue;
+ final int slash = mt.indexOf('/');
+ final String tmpType = mt.substring(0, slash);
+ if (!tmpType.equalsIgnoreCase(type)) type = type == null ? tmpType : "*";
+ final String tmpSubType = mt.substring(slash + 1);
+ if (!tmpSubType.equalsIgnoreCase(subType)) subType = subType == null ? tmpSubType : "*";
+ }
+ if (type == null) type = "*";
+ if (subType == null) subType = "*";
+ return type + "/" + subType;
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void notifyAlertListener(String name, String topic, String cookie);
+
+ /**
+ * Called by the NotificationListener to notify Gecko that a previously shown notification has
+ * been closed.
+ */
+ public static void onNotificationClose(final String name, final String cookie) {
+ if (GeckoThread.isRunning()) {
+ notifyAlertListener(name, "alertfinished", cookie);
+ }
+ }
+
+ /**
+ * Called by the NotificationListener to notify Gecko that a previously shown notification has
+ * been clicked on.
+ */
+ public static void onNotificationClick(final String name, final String cookie) {
+ if (GeckoThread.isRunning()) {
+ notifyAlertListener(name, "alertclickcallback", cookie);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY,
+ GeckoAppShell.class,
+ "notifyAlertListener",
+ name,
+ "alertclickcallback",
+ cookie);
+ }
+ }
+
+ public static synchronized void setDisplayDpiOverride(@Nullable final Integer dpi) {
+ if (dpi == null) {
+ return;
+ }
+ if (sDensityDpi != 0) {
+ Log.e(LOGTAG, "Tried to override screen DPI after it's already been set");
+ return;
+ }
+ sDensityDpi = dpi;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static synchronized int getDpi() {
+ if (sDensityDpi == 0) {
+ sDensityDpi = getApplicationContext().getResources().getDisplayMetrics().densityDpi;
+ }
+ return sDensityDpi;
+ }
+
+ public static synchronized void setDisplayDensityOverride(@Nullable final Float density) {
+ if (density == null) {
+ return;
+ }
+ if (sDensity != null) {
+ Log.e(LOGTAG, "Tried to override screen density after it's already been set");
+ return;
+ }
+ sDensity = density;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static synchronized float getDensity() {
+ if (sDensity == null) {
+ sDensity = Float.valueOf(getApplicationContext().getResources().getDisplayMetrics().density);
+ }
+
+ return sDensity;
+ }
+
+ private static int sTotalRam;
+
+ private static int getTotalRam(final Context context) {
+ if (sTotalRam == 0) {
+ final ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
+ final ActivityManager am =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ am.getMemoryInfo(memInfo); // `getMemoryInfo()` returns a value in B. Convert to MB.
+ sTotalRam = (int) (memInfo.totalMem / (1024 * 1024));
+ Log.d(LOGTAG, "System memory: " + sTotalRam + "MB.");
+ }
+
+ return sTotalRam;
+ }
+
+ private static boolean isHighMemoryDevice(final Context context) {
+ return getTotalRam(context) > HIGH_MEMORY_DEVICE_THRESHOLD_MB;
+ }
+
+ public static synchronized void useMaxScreenDepth(final boolean enable) {
+ sUseMaxScreenDepth = enable;
+ }
+
+ /** Returns the colour depth of the default screen. This will either be 32, 24 or 16. */
+ @WrapForJNI(calledFrom = "gecko")
+ public static synchronized int getScreenDepth() {
+ if (sScreenDepth == 0) {
+ sScreenDepth = 16;
+ final Context applicationContext = getApplicationContext();
+ final PixelFormat info = new PixelFormat();
+ final WindowManager wm =
+ (WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE);
+ PixelFormat.getPixelFormatInfo(wm.getDefaultDisplay().getPixelFormat(), info);
+ if (info.bitsPerPixel >= 24 && isHighMemoryDevice(applicationContext)) {
+ sScreenDepth = sUseMaxScreenDepth ? info.bitsPerPixel : 24;
+ }
+ }
+
+ return sScreenDepth;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static synchronized float getScreenRefreshRate() {
+ if (sScreenRefreshRate != null) {
+ return sScreenRefreshRate;
+ }
+
+ final WindowManager wm =
+ (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ final float refreshRate = wm.getDefaultDisplay().getRefreshRate();
+ // Android 11+ supports multiple refresh rate. So we have to get refresh rate per call.
+ // https://source.android.com/docs/core/graphics/multiple-refresh-rate
+ if (Build.VERSION.SDK_INT < 30) {
+ // Until Android 10, refresh rate is fixed, so we can cache it.
+ sScreenRefreshRate = Float.valueOf(refreshRate);
+ }
+ return refreshRate;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void performHapticFeedback(final boolean aIsLongPress) {
+ // Don't perform haptic feedback if a vibration is currently playing,
+ // because the haptic feedback will nuke the vibration.
+ if (!sVibrationMaybePlaying || System.nanoTime() >= sVibrationEndTime) {
+ final int[] pattern;
+ if (aIsLongPress) {
+ pattern = new int[] {0, 1, 20, 21};
+ } else {
+ pattern = new int[] {0, 10, 20, 30};
+ }
+ vibrateOnHapticFeedbackEnabled(pattern);
+ sVibrationMaybePlaying = false;
+ sVibrationEndTime = 0;
+ }
+ }
+
+ private static Vibrator vibrator() {
+ return (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE);
+ }
+
+ // Helper method to convert integer array to long array.
+ private static long[] convertIntToLongArray(final int[] input) {
+ final long[] output = new long[input.length];
+ for (int i = 0; i < input.length; i++) {
+ output[i] = input[i];
+ }
+ return output;
+ }
+
+ // Vibrate only if haptic feedback is enabled.
+ private static void vibrateOnHapticFeedbackEnabled(final int[] milliseconds) {
+ if (Settings.System.getInt(
+ getApplicationContext().getContentResolver(),
+ Settings.System.HAPTIC_FEEDBACK_ENABLED,
+ 0)
+ > 0) {
+ if (milliseconds.length == 1) {
+ vibrate(milliseconds[0]);
+ } else {
+ vibrate(convertIntToLongArray(milliseconds), -1);
+ }
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ @WrapForJNI(calledFrom = "gecko")
+ private static void vibrate(final long milliseconds) {
+ sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
+ sVibrationMaybePlaying = true;
+ try {
+ vibrator().vibrate(milliseconds);
+ } catch (final SecurityException ignore) {
+ Log.w(LOGTAG, "No VIBRATE permission");
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ @WrapForJNI(calledFrom = "gecko")
+ private static void vibrate(final long[] pattern, final int repeat) {
+ // If pattern.length is odd, the last element in the pattern is a
+ // meaningless delay, so don't include it in vibrationDuration.
+ long vibrationDuration = 0;
+ final int iterLen = pattern.length & ~1;
+ for (int i = 0; i < iterLen; i++) {
+ vibrationDuration += pattern[i];
+ }
+
+ sVibrationEndTime = System.nanoTime() + vibrationDuration * 1000000;
+ sVibrationMaybePlaying = true;
+ try {
+ vibrator().vibrate(pattern, repeat);
+ } catch (final SecurityException ignore) {
+ Log.w(LOGTAG, "No VIBRATE permission");
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ @WrapForJNI(calledFrom = "gecko")
+ private static void cancelVibrate() {
+ sVibrationMaybePlaying = false;
+ sVibrationEndTime = 0;
+ try {
+ vibrator().cancel();
+ } catch (final SecurityException ignore) {
+ Log.w(LOGTAG, "No VIBRATE permission");
+ }
+ }
+
+ private static ConnectivityManager sConnectivityManager;
+
+ private static void ensureConnectivityManager() {
+ if (sConnectivityManager == null) {
+ sConnectivityManager =
+ (ConnectivityManager)
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean isNetworkLinkUp() {
+ ensureConnectivityManager();
+ try {
+ final NetworkInfo info = sConnectivityManager.getActiveNetworkInfo();
+ if (info == null || !info.isConnected()) return false;
+ } catch (final SecurityException se) {
+ return false;
+ }
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean isNetworkLinkKnown() {
+ ensureConnectivityManager();
+ try {
+ if (sConnectivityManager.getActiveNetworkInfo() == null) return false;
+ } catch (final SecurityException se) {
+ return false;
+ }
+ return true;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getNetworkLinkType() {
+ ensureConnectivityManager();
+ final NetworkInfo info = sConnectivityManager.getActiveNetworkInfo();
+ if (info == null) {
+ return LINK_TYPE_UNKNOWN;
+ }
+
+ switch (info.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return LINK_TYPE_ETHERNET;
+ case ConnectivityManager.TYPE_WIFI:
+ return LINK_TYPE_WIFI;
+ case ConnectivityManager.TYPE_WIMAX:
+ return LINK_TYPE_WIMAX;
+ case ConnectivityManager.TYPE_MOBILE:
+ return LINK_TYPE_MOBILE;
+ default:
+ Log.w(LOGTAG, "Ignoring the current network type.");
+ return LINK_TYPE_UNKNOWN;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "nsresult")
+ private static String getDNSDomains() {
+ if (Build.VERSION.SDK_INT < 23) {
+ return "";
+ }
+
+ ensureConnectivityManager();
+ final Network net = sConnectivityManager.getActiveNetwork();
+ if (net == null) {
+ return "";
+ }
+
+ final LinkProperties lp = sConnectivityManager.getLinkProperties(net);
+ if (lp == null) {
+ return "";
+ }
+
+ return lp.getDomains();
+ }
+
+ @SuppressLint("ResourceType")
+ @WrapForJNI(calledFrom = "gecko")
+ private static int[] getSystemColors() {
+ // attrsAppearance[] must correspond to AndroidSystemColors structure in android/nsLookAndFeel.h
+ final int[] attrsAppearance = {
+ android.R.attr.textColorPrimary,
+ android.R.attr.textColorPrimaryInverse,
+ android.R.attr.textColorSecondary,
+ android.R.attr.textColorSecondaryInverse,
+ android.R.attr.textColorTertiary,
+ android.R.attr.textColorTertiaryInverse,
+ android.R.attr.textColorHighlight,
+ android.R.attr.colorForeground,
+ android.R.attr.colorBackground,
+ android.R.attr.panelColorForeground,
+ android.R.attr.panelColorBackground,
+ android.R.attr.colorAccent,
+ };
+
+ final int[] result = new int[attrsAppearance.length];
+
+ final ContextThemeWrapper contextThemeWrapper =
+ new ContextThemeWrapper(getApplicationContext(), android.R.style.TextAppearance);
+
+ final TypedArray appearance = contextThemeWrapper.obtainStyledAttributes(attrsAppearance);
+
+ if (appearance != null) {
+ for (int i = 0; i < appearance.getIndexCount(); i++) {
+ final int idx = appearance.getIndex(i);
+ final int color = appearance.getColor(idx, 0);
+ result[idx] = color;
+ }
+ appearance.recycle();
+ }
+
+ return result;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static byte[] getIconForExtension(final String aExt, final int iconSize) {
+ try {
+ int resolvedIconSize = iconSize;
+ if (iconSize <= 0) {
+ resolvedIconSize = 16;
+ }
+
+ String resolvedExt = aExt;
+ if (aExt != null && aExt.length() > 1 && aExt.charAt(0) == '.') {
+ resolvedExt = aExt.substring(1);
+ }
+
+ final PackageManager pm = getApplicationContext().getPackageManager();
+ Drawable icon = getDrawableForExtension(pm, resolvedExt);
+ if (icon == null) {
+ // Use a generic icon.
+ icon =
+ ResourcesCompat.getDrawable(
+ getApplicationContext().getResources(),
+ R.drawable.ic_generic_file,
+ getApplicationContext().getTheme());
+ }
+
+ Bitmap bitmap = getBitmapFromDrawable(icon);
+ if (bitmap.getWidth() != resolvedIconSize || bitmap.getHeight() != resolvedIconSize) {
+ bitmap = Bitmap.createScaledBitmap(bitmap, resolvedIconSize, resolvedIconSize, true);
+ }
+
+ final ByteBuffer buf = ByteBuffer.allocate(resolvedIconSize * resolvedIconSize * 4);
+ bitmap.copyPixelsToBuffer(buf);
+
+ return buf.array();
+ } catch (final Exception e) {
+ Log.w(LOGTAG, "getIconForExtension failed.", e);
+ return null;
+ }
+ }
+
+ private static Bitmap getBitmapFromDrawable(final Drawable drawable) {
+ if (drawable instanceof BitmapDrawable) {
+ return ((BitmapDrawable) drawable).getBitmap();
+ }
+
+ int width = drawable.getIntrinsicWidth();
+ width = width > 0 ? width : 1;
+ int height = drawable.getIntrinsicHeight();
+ height = height > 0 ? height : 1;
+
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+
+ return bitmap;
+ }
+
+ public static String getMimeTypeFromExtension(final String ext) {
+ final MimeTypeMap mtm = MimeTypeMap.getSingleton();
+ return mtm.getMimeTypeFromExtension(ext);
+ }
+
+ private static Drawable getDrawableForExtension(final PackageManager pm, final String aExt) {
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ final String mimeType = getMimeTypeFromExtension(aExt);
+ if (mimeType != null && mimeType.length() > 0) intent.setType(mimeType);
+ else return null;
+
+ final List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+ if (list.size() == 0) return null;
+
+ final ResolveInfo resolveInfo = list.get(0);
+
+ if (resolveInfo == null) return null;
+
+ final ActivityInfo activityInfo = resolveInfo.activityInfo;
+
+ return activityInfo.loadIcon(pm);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean getShowPasswordSetting() {
+ try {
+ final int showPassword =
+ Settings.System.getInt(
+ getApplicationContext().getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, 1);
+ return (showPassword > 0);
+ } catch (final Exception e) {
+ return true;
+ }
+ }
+
+ private static Context sApplicationContext;
+ private static Boolean sIs24HourFormat = true;
+
+ @WrapForJNI
+ public static Context getApplicationContext() {
+ return sApplicationContext;
+ }
+
+ public static void setApplicationContext(final Context context) {
+ sApplicationContext = context;
+ }
+
+ /*
+ * Battery API related methods.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableBatteryNotifications() {
+ GeckoBatteryManager.enableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableBatteryNotifications() {
+ GeckoBatteryManager.disableNotifications();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static double[] getCurrentBatteryInformation() {
+ return GeckoBatteryManager.getCurrentInformation();
+ }
+
+ /* Called by JNI from AndroidBridge, and by reflection from tests/BaseTest.java.in */
+ @WrapForJNI(calledFrom = "gecko")
+ @RobocopTarget
+ public static boolean isTablet() {
+ return HardwareUtils.isTablet(getApplicationContext());
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static double[] getCurrentNetworkInformation() {
+ return GeckoNetworkManager.getInstance().getCurrentInformation();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void enableNetworkNotifications() {
+ ThreadUtils.runOnUiThread(() -> GeckoNetworkManager.getInstance().enableNotifications());
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void disableNetworkNotifications() {
+ ThreadUtils.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ GeckoNetworkManager.getInstance().disableNotifications();
+ }
+ });
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static short getScreenOrientation() {
+ return GeckoScreenOrientation.getInstance().getScreenOrientation().value;
+ }
+
+ /* package */ static int getRotation() {
+ return sScreenCompat.getRotation();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getScreenAngle() {
+ return GeckoScreenOrientation.getInstance().getAngle();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void notifyWakeLockChanged(final String topic, final String state) {
+ final int intState;
+ if ("unlocked".equals(state)) {
+ intState = WAKE_LOCK_STATE_UNLOCKED;
+ } else if ("locked-foreground".equals(state)) {
+ intState = WAKE_LOCK_STATE_LOCKED_FOREGROUND;
+ } else if ("locked-background".equals(state)) {
+ intState = WAKE_LOCK_STATE_LOCKED_BACKGROUND;
+ } else {
+ throw new IllegalArgumentException();
+ }
+ setWakeLockState(topic, intState);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static String getProxyForURI(
+ final String spec, final String scheme, final String host, final int port) {
+ final ProxySelector ps = new ProxySelector();
+
+ final Proxy proxy = ps.select(scheme, host);
+ if (Proxy.NO_PROXY.equals(proxy)) {
+ return "DIRECT";
+ }
+
+ final InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
+ final String proxyString = proxyAddress.getHostString() + ":" + proxyAddress.getPort();
+
+ switch (proxy.type()) {
+ case HTTP:
+ return "PROXY " + proxyString;
+ case SOCKS:
+ return "SOCKS " + proxyString;
+ }
+
+ return "DIRECT";
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getMaxTouchPoints() {
+ final PackageManager pm = getApplicationContext().getPackageManager();
+ if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND)) {
+ // at least, 5+ fingers.
+ return 5;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
+ // at least, 2+ fingers.
+ return 2;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)) {
+ // 2 fingers
+ return 2;
+ } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
+ // 1 finger
+ return 1;
+ }
+ return 0;
+ }
+
+ /*
+ * Keep in sync with PointerCapabilities in ServoTypes.h
+ */
+ private static final int NO_POINTER = 0x00000000;
+ private static final int COARSE_POINTER = 0x00000001;
+ private static final int FINE_POINTER = 0x00000002;
+ private static final int HOVER_CAPABLE_POINTER = 0x00000004;
+
+ private static int getPointerCapabilities(final InputDevice inputDevice) {
+ int result = NO_POINTER;
+ final int sources = inputDevice.getSources();
+
+ // Blink checks fine pointer at first, then it check coarse pointer.
+ // So, we should use same order for compatibility.
+ // Also, if using Chrome OS, source may be SOURCE_MOUSE | SOURCE_TOUCHSCREEN | SOURCE_STYLUS
+ // even if no touch screen. So we shouldn't check TOUCHSCREEN at first.
+
+ if (hasInputDeviceSource(sources, InputDevice.SOURCE_MOUSE)
+ || hasInputDeviceSource(sources, InputDevice.SOURCE_STYLUS)
+ || hasInputDeviceSource(sources, InputDevice.SOURCE_TOUCHPAD)
+ || hasInputDeviceSource(sources, InputDevice.SOURCE_TRACKBALL)) {
+ result |= FINE_POINTER;
+ } else if (hasInputDeviceSource(sources, InputDevice.SOURCE_TOUCHSCREEN)
+ || hasInputDeviceSource(sources, InputDevice.SOURCE_JOYSTICK)) {
+ result |= COARSE_POINTER;
+ }
+
+ if (hasInputDeviceSource(sources, InputDevice.SOURCE_MOUSE)
+ || hasInputDeviceSource(sources, InputDevice.SOURCE_TOUCHPAD)
+ || hasInputDeviceSource(sources, InputDevice.SOURCE_TRACKBALL)
+ || hasInputDeviceSource(sources, InputDevice.SOURCE_JOYSTICK)) {
+ result |= HOVER_CAPABLE_POINTER;
+ }
+
+ return result;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ // For any-pointer and any-hover media queries features.
+ private static int getAllPointerCapabilities() {
+ int result = NO_POINTER;
+
+ for (final int deviceId : InputDevice.getDeviceIds()) {
+ final InputDevice inputDevice = InputDevice.getDevice(deviceId);
+ if (inputDevice == null || !InputDeviceUtils.isPointerTypeDevice(inputDevice)) {
+ continue;
+ }
+
+ result |= getPointerCapabilities(inputDevice);
+ }
+
+ return result;
+ }
+
+ private static boolean hasInputDeviceSource(final int sources, final int inputDeviceSource) {
+ return (sources & inputDeviceSource) == inputDeviceSource;
+ }
+
+ public static synchronized void setScreenSizeOverride(final Rect size) {
+ sScreenSizeOverride = size;
+ }
+
+ static final ScreenCompat sScreenCompat;
+
+ private interface ScreenCompat {
+ Rect getScreenSize();
+
+ int getRotation();
+ }
+
+ private static class JellyBeanMR1ScreenCompat implements ScreenCompat {
+ public Rect getScreenSize() {
+ final WindowManager wm =
+ (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ final Display disp = wm.getDefaultDisplay();
+ final Point size = new Point();
+ disp.getRealSize(size);
+ return new Rect(0, 0, size.x, size.y);
+ }
+
+ public int getRotation() {
+ final WindowManager wm =
+ (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ return wm.getDefaultDisplay().getRotation();
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.S)
+ private static class AndroidSScreenCompat implements ScreenCompat {
+ @SuppressLint("StaticFieldLeak")
+ private static Context sWindowContext;
+
+ private static Context getWindowContext() {
+ if (sWindowContext == null) {
+ final DisplayManager displayManager =
+ (DisplayManager) getApplicationContext().getSystemService(Context.DISPLAY_SERVICE);
+ final Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ sWindowContext =
+ getApplicationContext()
+ .createWindowContext(display, WindowManager.LayoutParams.TYPE_APPLICATION, null);
+ }
+ return sWindowContext;
+ }
+
+ public Rect getScreenSize() {
+ final WindowManager windowManager = getWindowContext().getSystemService(WindowManager.class);
+ return windowManager.getCurrentWindowMetrics().getBounds();
+ }
+
+ public int getRotation() {
+ final WindowManager windowManager = getWindowContext().getSystemService(WindowManager.class);
+ return windowManager.getDefaultDisplay().getRotation();
+ }
+ }
+
+ static {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ sScreenCompat = new AndroidSScreenCompat();
+ } else {
+ sScreenCompat = new JellyBeanMR1ScreenCompat();
+ }
+ }
+
+ /* package */ static Rect getScreenSizeIgnoreOverride() {
+ return sScreenCompat.getScreenSize();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static synchronized Rect getScreenSize() {
+ if (sScreenSizeOverride != null) {
+ return sScreenSizeOverride;
+ }
+
+ return getScreenSizeIgnoreOverride();
+ }
+
+ @WrapForJNI(calledFrom = "any")
+ public static int getAudioOutputFramesPerBuffer() {
+ final int DEFAULT = 512;
+
+ final AudioManager am =
+ (AudioManager) getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
+ if (am == null) {
+ return DEFAULT;
+ }
+ final String prop = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
+ if (prop == null) {
+ return DEFAULT;
+ }
+ return Integer.parseInt(prop);
+ }
+
+ @WrapForJNI(calledFrom = "any")
+ public static int getAudioOutputSampleRate() {
+ final int DEFAULT = 44100;
+
+ final AudioManager am =
+ (AudioManager) getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
+ if (am == null) {
+ return DEFAULT;
+ }
+ final String prop = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
+ if (prop == null) {
+ return DEFAULT;
+ }
+ return Integer.parseInt(prop);
+ }
+
+ @WrapForJNI(calledFrom = "any")
+ public static void setCommunicationAudioModeOn(final boolean on) {
+ final AudioManager am =
+ (AudioManager) getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
+ if (am == null) {
+ return;
+ }
+
+ try {
+ if (on) {
+ Log.e(LOGTAG, "Setting communication mode ON");
+ // This shouldn't throw, but does throw NullPointerException on a very
+ // small number of devices.
+ am.startBluetoothSco();
+ am.setBluetoothScoOn(true);
+ } else {
+ Log.e(LOGTAG, "Setting communication mode OFF");
+ am.stopBluetoothSco();
+ am.setBluetoothScoOn(false);
+ }
+ } catch (final SecurityException | NullPointerException e) {
+ Log.e(LOGTAG, "could not set communication mode", e);
+ }
+ }
+
+ private static String getLanguageTag(final Locale locale) {
+ final StringBuilder out = new StringBuilder(locale.getLanguage());
+ final String country = locale.getCountry();
+ final String variant = locale.getVariant();
+ if (!TextUtils.isEmpty(country)) {
+ out.append('-').append(country);
+ }
+ if (!TextUtils.isEmpty(variant)) {
+ out.append('-').append(variant);
+ }
+ // e.g. "en", "en-US", or "en-US-POSIX".
+ return out.toString();
+ }
+
+ @WrapForJNI
+ public static String[] getDefaultLocales() {
+ // XXX We may have to convert some language codes such as "id" vs "in".
+ if (Build.VERSION.SDK_INT >= 24) {
+ final LocaleList localeList = LocaleList.getDefault();
+ final String[] locales = new String[localeList.size()];
+ for (int i = 0; i < localeList.size(); i++) {
+ locales[i] = localeList.get(i).toLanguageTag();
+ }
+ return locales;
+ }
+ final String[] locales = new String[1];
+ final Locale locale = Locale.getDefault();
+ locales[0] = locale.toLanguageTag();
+ return locales;
+ }
+
+ public static void setIs24HourFormat(final Boolean is24HourFormat) {
+ sIs24HourFormat = is24HourFormat;
+ }
+
+ @WrapForJNI
+ public static boolean getIs24HourFormat() {
+ return sIs24HourFormat;
+ }
+
+ @WrapForJNI
+ public static String getAppName() {
+ final Context context = getApplicationContext();
+ final ApplicationInfo info = context.getApplicationInfo();
+ final int id = info.labelRes;
+ return id == 0 ? info.nonLocalizedLabel.toString() : context.getString(id);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static int getMemoryUsage(final String stateName) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // No API to get Java heap usages.
+ return -1;
+ }
+
+ final Debug.MemoryInfo memInfo = new Debug.MemoryInfo();
+ Debug.getMemoryInfo(memInfo);
+ final String usage = memInfo.getMemoryStat(stateName);
+ if (usage == null) {
+ return -1;
+ }
+ try {
+ return Integer.parseInt(usage);
+ } catch (final NumberFormatException e) {
+ return -1;
+ }
+ }
+
+ @WrapForJNI
+ public static native boolean isParentProcess();
+
+ /**
+ * Returns a GeckoResult that will be completed to true if the GPU process is enabled and false if
+ * it is disabled.
+ */
+ @WrapForJNI
+ public static native GeckoResult<Boolean> isGpuProcessEnabled();
+
+ @SuppressLint("NewApi")
+ public static boolean isIsolatedProcess() {
+ // This method was added in SDK 16 but remained hidden until SDK 28, meaning we are okay to call
+ // this on any SDK level but must suppress the new API lint.
+ return android.os.Process.isIsolated();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java
new file mode 100644
index 0000000000..19f489b399
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoBatteryManager.java
@@ -0,0 +1,200 @@
+/* -*- 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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public class GeckoBatteryManager extends BroadcastReceiver {
+ private static final String LOGTAG = "GeckoBatteryManager";
+
+ // Those constants should be keep in sync with the ones in:
+ // dom/battery/Constants.h
+ private static final double kDefaultLevel = 1.0;
+ private static final boolean kDefaultCharging = true;
+ private static final double kDefaultRemainingTime = 0.0;
+ private static final double kUnknownRemainingTime = -1.0;
+
+ private static long sLastLevelChange;
+ private static boolean sNotificationsEnabled;
+ private static double sLevel = kDefaultLevel;
+ private static boolean sCharging = kDefaultCharging;
+ private static double sRemainingTime = kDefaultRemainingTime;
+
+ private static final GeckoBatteryManager sInstance = new GeckoBatteryManager();
+
+ private final IntentFilter mFilter;
+ private Context mApplicationContext;
+ private boolean mIsEnabled;
+
+ public static GeckoBatteryManager getInstance() {
+ return sInstance;
+ }
+
+ private GeckoBatteryManager() {
+ mFilter = new IntentFilter();
+ mFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ }
+
+ public synchronized void start(final Context context) {
+ if (mIsEnabled) {
+ Log.w(LOGTAG, "Already started!");
+ return;
+ }
+
+ mApplicationContext = context.getApplicationContext();
+ // registerReceiver will return null if registering fails.
+ if (mApplicationContext.registerReceiver(this, mFilter) == null) {
+ Log.e(LOGTAG, "Registering receiver failed");
+ } else {
+ mIsEnabled = true;
+ }
+ }
+
+ public synchronized void stop() {
+ if (!mIsEnabled) {
+ Log.w(LOGTAG, "Already stopped!");
+ return;
+ }
+
+ mApplicationContext.unregisterReceiver(this);
+ mApplicationContext = null;
+ mIsEnabled = false;
+ }
+
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onBatteryChange(double level, boolean charging, double remainingTime);
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ if (!intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
+ Log.e(LOGTAG, "Got an unexpected intent!");
+ return;
+ }
+
+ final boolean previousCharging = isCharging();
+ final double previousLevel = getLevel();
+
+ // NOTE: it might not be common (in 2012) but technically, Android can run
+ // on a device that has no battery so we want to make sure it's not the case
+ // before bothering checking for battery state.
+ // However, the Galaxy Nexus phone advertises itself as battery-less which
+ // force us to special-case the logic.
+ // See the Google bug: https://code.google.com/p/android/issues/detail?id=22035
+ if (intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false)
+ || Build.MODEL.equals("Galaxy Nexus")) {
+ final int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
+ if (plugged == -1) {
+ sCharging = kDefaultCharging;
+ Log.e(LOGTAG, "Failed to get the plugged status!");
+ } else {
+ // Likely, if plugged > 0, it's likely plugged and charging but the doc
+ // isn't clear about that.
+ sCharging = plugged != 0;
+ }
+
+ if (sCharging != previousCharging) {
+ sRemainingTime = kUnknownRemainingTime;
+ // The new remaining time is going to take some time to show up but
+ // it's the best way to show a not too wrong value.
+ sLastLevelChange = 0;
+ }
+
+ // We need two doubles because sLevel is a double.
+ final double current = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ final double max = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ if (current == -1 || max == -1) {
+ Log.e(LOGTAG, "Failed to get battery level!");
+ sLevel = kDefaultLevel;
+ } else {
+ sLevel = current / max;
+ }
+
+ if (sLevel == 1.0 && sCharging) {
+ sRemainingTime = kDefaultRemainingTime;
+ } else if (sLevel != previousLevel) {
+ // Estimate remaining time.
+ if (sLastLevelChange != 0) {
+ // Use elapsedRealtime() because we want to track time across device sleeps.
+ final long currentTime = SystemClock.elapsedRealtime();
+ final long dt = (currentTime - sLastLevelChange) / 1000;
+ final double dLevel = sLevel - previousLevel;
+
+ if (sCharging) {
+ if (dLevel < 0) {
+ sRemainingTime = kUnknownRemainingTime;
+ } else {
+ sRemainingTime = Math.round(dt / dLevel * (1.0 - sLevel));
+ }
+ } else {
+ if (dLevel > 0) {
+ Log.w(LOGTAG, "When discharging, level should decrease!");
+ sRemainingTime = kUnknownRemainingTime;
+ } else {
+ sRemainingTime = Math.round(dt / -dLevel * sLevel);
+ }
+ }
+
+ sLastLevelChange = currentTime;
+ } else {
+ // That's the first time we got an update, we can't do anything.
+ sLastLevelChange = SystemClock.elapsedRealtime();
+ }
+ }
+ } else {
+ sLevel = kDefaultLevel;
+ sCharging = kDefaultCharging;
+ sRemainingTime = kDefaultRemainingTime;
+ }
+
+ /*
+ * We want to inform listeners if the following conditions are fulfilled:
+ * - we have at least one observer;
+ * - the charging state or the level has changed.
+ *
+ * Note: no need to check for a remaining time change given that it's only
+ * updated if there is a level change or a charging change.
+ *
+ * The idea is to prevent doing all the way to the DOM code in the child
+ * process to finally not send an event.
+ */
+ if (sNotificationsEnabled
+ && (previousCharging != isCharging() || previousLevel != getLevel())) {
+ onBatteryChange(getLevel(), isCharging(), getRemainingTime());
+ }
+ }
+
+ public static boolean isCharging() {
+ return sCharging;
+ }
+
+ public static double getLevel() {
+ return sLevel;
+ }
+
+ public static double getRemainingTime() {
+ return sRemainingTime;
+ }
+
+ public static void enableNotifications() {
+ sNotificationsEnabled = true;
+ }
+
+ public static void disableNotifications() {
+ sNotificationsEnabled = false;
+ }
+
+ public static double[] getCurrentInformation() {
+ return new double[] {getLevel(), isCharging() ? 1.0 : 0.0, getRemainingTime()};
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoDragAndDrop.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoDragAndDrop.java
new file mode 100644
index 0000000000..9c1473d4e7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoDragAndDrop.java
@@ -0,0 +1,253 @@
+/* -*- Mode: Java; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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;
+
+import android.annotation.TargetApi;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.DragEvent;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+@TargetApi(Build.VERSION_CODES.N)
+public class GeckoDragAndDrop {
+ private static final String LOGTAG = "GeckoDragAndDrop";
+ private static final boolean DEBUG = false;
+
+ /** The drag/drop data is nsITransferable and stored into nsDragService. */
+ private static final String MIMETYPE_NATIVE = "application/x-moz-draganddrop";
+
+ private static final String[] sSupportedMimeType = {
+ MIMETYPE_NATIVE, ClipDescription.MIMETYPE_TEXT_HTML, ClipDescription.MIMETYPE_TEXT_PLAIN
+ };
+
+ private static ClipData sDragClipData;
+ private static float sX;
+ private static float sY;
+ private static boolean mEndingSession;
+
+ private static class DrawDragImage extends View.DragShadowBuilder {
+ private final Bitmap mBitmap;
+
+ public DrawDragImage(final Bitmap bitmap) {
+ mBitmap = bitmap;
+ }
+
+ @Override
+ public void onProvideShadowMetrics(final Point outShadowSize, final Point outShadowTouchPoint) {
+ if (mBitmap == null) {
+ super.onProvideShadowMetrics(outShadowSize, outShadowTouchPoint);
+ return;
+ }
+ outShadowSize.set(mBitmap.getWidth(), mBitmap.getHeight());
+ }
+
+ @Override
+ public void onDrawShadow(final Canvas canvas) {
+ if (mBitmap == null) {
+ super.onDrawShadow(canvas);
+ return;
+ }
+ canvas.drawBitmap(mBitmap, 0.0f, 0.0f, null);
+ }
+ }
+
+ @WrapForJNI
+ public static class DropData {
+ public final String mimeType;
+ public final String text;
+
+ @WrapForJNI(skip = true)
+ public DropData() {
+ this.mimeType = MIMETYPE_NATIVE;
+ this.text = null;
+ }
+
+ @WrapForJNI(skip = true)
+ public DropData(final String mimeType) {
+ this.mimeType = mimeType;
+ this.text = "";
+ }
+
+ @WrapForJNI(skip = true)
+ public DropData(final String mimeType, final String text) {
+ this.mimeType = mimeType;
+ this.text = text;
+ }
+ }
+
+ public static void startDragAndDrop(final View view, final Bitmap bitmap) {
+ view.startDragAndDrop(sDragClipData, new DrawDragImage(bitmap), null, View.DRAG_FLAG_GLOBAL);
+ sDragClipData = null;
+ }
+
+ public static void updateDragImage(final View view, final Bitmap bitmap) {
+ view.updateDragShadow(new DrawDragImage(bitmap));
+ }
+
+ public static boolean onDragEvent(@NonNull final DragEvent event) {
+ if (DEBUG) {
+ final StringBuilder sb = new StringBuilder("onDragEvent: action=");
+ sb.append(event.getAction())
+ .append(", x=")
+ .append(event.getX())
+ .append(", y=")
+ .append(event.getY());
+ Log.d(LOGTAG, sb.toString());
+ }
+
+ switch (event.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ mEndingSession = false;
+ sX = event.getX();
+ sY = event.getY();
+ break;
+ case DragEvent.ACTION_DRAG_LOCATION:
+ sX = event.getX();
+ sY = event.getY();
+ break;
+ case DragEvent.ACTION_DROP:
+ sX = event.getX();
+ sY = event.getY();
+ break;
+ case DragEvent.ACTION_DRAG_ENDED:
+ mEndingSession = true;
+ return true;
+ default:
+ break;
+ }
+ if (mEndingSession) {
+ return false;
+ }
+ return true;
+ }
+
+ public static float getLocationX() {
+ return sX;
+ }
+
+ public static float getLocationY() {
+ return sY;
+ }
+
+ /**
+ * Create drop data by DragEvent. This ClipData will be stored into nsDragService as
+ * nsITransferable. If this type has MIMETYPE_NATIVE, this is already stored into nsDragService.
+ * So do nothing.
+ *
+ * @param event A DragEvent
+ * @return DropData that is from ClipData. If null, no data that we can convert to Gecko's type.
+ */
+ public static DropData createDropData(final DragEvent event) {
+ final ClipDescription description = event.getClipDescription();
+
+ if (event.getAction() == DragEvent.ACTION_DRAG_ENTERED) {
+ // Android API cannot get real dragging item until drop event. So we set MIME type only.
+ for (final String mimeType : sSupportedMimeType) {
+ if (description.hasMimeType(mimeType)) {
+ return new DropData(mimeType);
+ }
+ }
+ return null;
+ }
+
+ if (event.getAction() != DragEvent.ACTION_DROP) {
+ return null;
+ }
+
+ final ClipData clip = event.getClipData();
+ if (clip == null || clip.getItemCount() == 0) {
+ return null;
+ }
+
+ if (description.hasMimeType(MIMETYPE_NATIVE)) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "Drop data is native nsITransferable. Do nothing");
+ }
+ return new DropData();
+ }
+ if (description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) {
+ final CharSequence data = clip.getItemAt(0).getHtmlText();
+ if (data == null) {
+ return null;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "Drop data is text/html");
+ }
+ return new DropData(ClipDescription.MIMETYPE_TEXT_HTML, data.toString());
+ }
+
+ final CharSequence text = clip.getItemAt(0).coerceToText(GeckoAppShell.getApplicationContext());
+ if (!TextUtils.isEmpty(text)) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "Drop data is text/plain");
+ }
+ return new DropData(ClipDescription.MIMETYPE_TEXT_PLAIN, text.toString());
+ }
+ return null;
+ }
+
+ private static void setDragClipData(final ClipData clipData) {
+ sDragClipData = clipData;
+ }
+
+ private static @Nullable ClipData getDragClipData() {
+ return sDragClipData;
+ }
+
+ /**
+ * Set drag item before calling View.startDragAndDrop. This is set from nsITransferable, so it
+ * marks as native data.
+ */
+ @WrapForJNI
+ private static void setDragData(final CharSequence text, final String htmlText) {
+ if (TextUtils.isEmpty(text)) {
+ final ClipDescription description =
+ new ClipDescription("drag item", new String[] {MIMETYPE_NATIVE});
+ final ClipData.Item item = new ClipData.Item("");
+ final ClipData clipData = new ClipData(description, item);
+ setDragClipData(clipData);
+ return;
+ }
+
+ if (TextUtils.isEmpty(htmlText)) {
+ final ClipDescription description =
+ new ClipDescription(
+ "drag item", new String[] {MIMETYPE_NATIVE, ClipDescription.MIMETYPE_TEXT_PLAIN});
+ final ClipData.Item item = new ClipData.Item(text);
+ final ClipData clipData = new ClipData(description, item);
+ setDragClipData(clipData);
+ return;
+ }
+
+ final ClipDescription description =
+ new ClipDescription(
+ "drag item",
+ new String[] {
+ MIMETYPE_NATIVE,
+ ClipDescription.MIMETYPE_TEXT_HTML,
+ ClipDescription.MIMETYPE_TEXT_PLAIN
+ });
+ final ClipData.Item item = new ClipData.Item(text, htmlText);
+ final ClipData clipData = new ClipData(description, item);
+ setDragClipData(clipData);
+ return;
+ }
+
+ @WrapForJNI
+ private static void endDragSession() {
+ mEndingSession = true;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java
new file mode 100644
index 0000000000..8a76548c1d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoEditableChild.java
@@ -0,0 +1,456 @@
+/* -*- 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;
+
+import android.graphics.RectF;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.KeyEvent;
+import androidx.annotation.Nullable;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * GeckoEditableChild implements the Gecko-facing side of IME operation. Each nsWindow in the main
+ * process and each PuppetWidget in each child content process has an instance of
+ * GeckoEditableChild, which communicates with the GeckoEditableParent instance in the main process.
+ */
+public final class GeckoEditableChild extends JNIObject implements IGeckoEditableChild {
+
+ private static final boolean DEBUG = false;
+ private static final String LOGTAG = "GeckoEditableChild";
+
+ private static final int NOTIFY_IME_TO_CANCEL_COMPOSITION = 9;
+
+ private final class RemoteChild extends IGeckoEditableChild.Stub {
+ @Override // IGeckoEditableChild
+ public void transferParent(final IGeckoEditableParent editableParent) {
+ GeckoEditableChild.this.transferParent(editableParent);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onKeyEvent(
+ final int action,
+ final int keyCode,
+ final int scanCode,
+ final int metaState,
+ final int keyPressMetaState,
+ final long time,
+ final int domPrintableKeyValue,
+ final int repeatCount,
+ final int flags,
+ final boolean isSynthesizedImeKey,
+ final KeyEvent event) {
+ GeckoEditableChild.this.onKeyEvent(
+ action,
+ keyCode,
+ scanCode,
+ metaState,
+ keyPressMetaState,
+ time,
+ domPrintableKeyValue,
+ repeatCount,
+ flags,
+ isSynthesizedImeKey,
+ event);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeSynchronize() {
+ GeckoEditableChild.this.onImeSynchronize();
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeReplaceText(final int start, final int end, final String text) {
+ GeckoEditableChild.this.onImeReplaceText(start, end, text);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeInsertImage(final byte[] data, final String mimeType) {
+ GeckoEditableChild.this.onImeInsertImage(data, mimeType);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeAddCompositionRange(
+ final int start,
+ final int end,
+ final int rangeType,
+ final int rangeStyles,
+ final int rangeLineStyle,
+ final boolean rangeBoldLine,
+ final int rangeForeColor,
+ final int rangeBackColor,
+ final int rangeLineColor) {
+ GeckoEditableChild.this.onImeAddCompositionRange(
+ start,
+ end,
+ rangeType,
+ rangeStyles,
+ rangeLineStyle,
+ rangeBoldLine,
+ rangeForeColor,
+ rangeBackColor,
+ rangeLineColor);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeUpdateComposition(final int start, final int end, final int flags) {
+ GeckoEditableChild.this.onImeUpdateComposition(start, end, flags);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeRequestCursorUpdates(final int requestMode) {
+ GeckoEditableChild.this.onImeRequestCursorUpdates(requestMode);
+ }
+
+ @Override // IGeckoEditableChild
+ public void onImeRequestCommit() {
+ GeckoEditableChild.this.onImeRequestCommit();
+ }
+ }
+
+ private final IGeckoEditableChild mEditableChild;
+ private final boolean mIsDefault;
+
+ private IGeckoEditableParent mEditableParent;
+ private int mCurrentTextLength; // Used by Gecko thread
+
+ @WrapForJNI(calledFrom = "gecko")
+ private GeckoEditableChild(
+ @Nullable final IGeckoEditableParent editableParent, final boolean isDefault) {
+ mIsDefault = isDefault;
+
+ if (editableParent != null
+ && editableParent.asBinder().queryLocalInterface(IGeckoEditableParent.class.getName())
+ != null) {
+ // IGeckoEditableParent is local; i.e. we're in the main process.
+ mEditableChild = this;
+ } else {
+ // IGeckoEditableParent is remote; i.e. we're in a content process.
+ mEditableChild = new RemoteChild();
+ }
+
+ if (editableParent != null) {
+ setParent(editableParent);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void setParent(final IGeckoEditableParent editableParent) {
+ mEditableParent = editableParent;
+
+ if (mIsDefault) {
+ // Tell the parent we're the default child.
+ try {
+ editableParent.setDefaultChild(mEditableChild);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Failed to set default child", e);
+ }
+ }
+ }
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void transferParent(IGeckoEditableParent editableParent);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onKeyEvent(
+ int action,
+ int keyCode,
+ int scanCode,
+ int metaState,
+ int keyPressMetaState,
+ long time,
+ int domPrintableKeyValue,
+ int repeatCount,
+ int flags,
+ boolean isSynthesizedImeKey,
+ KeyEvent event);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeSynchronize();
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeReplaceText(int start, int end, String text);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeAddCompositionRange(
+ int start,
+ int end,
+ int rangeType,
+ int rangeStyles,
+ int rangeLineStyle,
+ boolean rangeBoldLine,
+ int rangeForeColor,
+ int rangeBackColor,
+ int rangeLineColor);
+
+ // Don't update to the new composition if it's different than the current composition.
+ @WrapForJNI public static final int FLAG_KEEP_CURRENT_COMPOSITION = 1;
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeUpdateComposition(int start, int end, int flags);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeRequestCursorUpdates(int requestMode);
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeRequestCommit();
+
+ @WrapForJNI(dispatchTo = "proxy")
+ @Override // IGeckoEditableChild
+ public native void onImeInsertImage(byte[] data, String mimeType);
+
+ @Override // JNIObject
+ protected void disposeNative() {
+ // Disposal happens in native code.
+ throw new UnsupportedOperationException();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private boolean hasEditableParent() {
+ if (mEditableParent != null) {
+ return true;
+ }
+ Log.w(LOGTAG, "No editable parent");
+ return false;
+ }
+
+ @Override // IInterface
+ public IBinder asBinder() {
+ // Return the GeckoEditableParent's binder as fallback for comparison purposes.
+ return mEditableChild != this
+ ? mEditableChild.asBinder()
+ : hasEditableParent() ? mEditableParent.asBinder() : null;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIME(final int type) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "notifyIME(" + type + ")");
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+ if (type == NOTIFY_IME_TO_CANCEL_COMPOSITION) {
+ // Composition should have been canceled on the parent side through text
+ // update notifications. We cannot verify that here because we don't
+ // keep track of spans on the child side, but it's simple to add the
+ // check to the parent side if ever needed.
+ return;
+ }
+
+ try {
+ mEditableParent.notifyIME(mEditableChild, type);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Remote call failed", e);
+ return;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void notifyIMEContext(
+ final int state,
+ final String typeHint,
+ final String modeHint,
+ final String actionHint,
+ final String autocapitalize,
+ final int flags) {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ final StringBuilder sb = new StringBuilder("notifyIMEContext(");
+ sb.append(state)
+ .append(", \"")
+ .append(typeHint)
+ .append("\", \"")
+ .append(modeHint)
+ .append("\", \"")
+ .append(actionHint)
+ .append("\", \"")
+ .append(autocapitalize)
+ .append("\", 0x")
+ .append(Integer.toHexString(flags))
+ .append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ try {
+ mEditableParent.notifyIMEContext(
+ mEditableChild.asBinder(), state, typeHint, modeHint, actionHint, autocapitalize, flags);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Remote call failed", e);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onSelectionChange(
+ final int start, final int end, final boolean causedOnlyByComposition)
+ throws RemoteException {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ final StringBuilder sb = new StringBuilder("onSelectionChange(");
+ sb.append(start)
+ .append(", ")
+ .append(end)
+ .append(", ")
+ .append(causedOnlyByComposition)
+ .append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ final int currentLength = mCurrentTextLength;
+ if (start < 0 || start > currentLength || end < 0 || end > currentLength) {
+ Log.e(
+ LOGTAG,
+ "invalid selection notification range: "
+ + start
+ + " to "
+ + end
+ + ", length: "
+ + currentLength);
+ throw new IllegalArgumentException("invalid selection notification range");
+ }
+
+ mEditableParent.onSelectionChange(
+ mEditableChild.asBinder(), start, end, causedOnlyByComposition);
+ }
+
+ @WrapForJNI(calledFrom = "gecko", exceptionMode = "ignore")
+ private void onTextChange(
+ final CharSequence text,
+ final int start,
+ final int unboundedOldEnd,
+ final int unboundedNewEnd,
+ final boolean causedOnlyByComposition)
+ throws RemoteException {
+ if (DEBUG) {
+ ThreadUtils.assertOnGeckoThread();
+ final StringBuilder sb = new StringBuilder("onTextChange(");
+ sb.append(text)
+ .append(", ")
+ .append(start)
+ .append(", ")
+ .append(unboundedOldEnd)
+ .append(", ")
+ .append(unboundedNewEnd)
+ .append(", ")
+ .append(causedOnlyByComposition)
+ .append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ if (start < 0 || start > unboundedOldEnd) {
+ Log.e(LOGTAG, "invalid text notification range: " + start + " to " + unboundedOldEnd);
+ throw new IllegalArgumentException("invalid text notification range");
+ }
+
+ /* For the "end" parameters, Gecko can pass in a large
+ number to denote "end of the text". Fix that here */
+ final int currentLength = mCurrentTextLength;
+ final int oldEnd = unboundedOldEnd > currentLength ? currentLength : unboundedOldEnd;
+ // new end should always match text
+ if (unboundedOldEnd <= currentLength && unboundedNewEnd != (start + text.length())) {
+ Log.e(
+ LOGTAG,
+ "newEnd does not match text: " + unboundedNewEnd + " vs " + (start + text.length()));
+ throw new IllegalArgumentException("newEnd does not match text");
+ }
+
+ mCurrentTextLength += start + text.length() - oldEnd;
+ // Need unboundedOldEnd so GeckoEditable can distinguish changed text vs cleared text.
+ if (text.length() == 0) {
+ // Remove text in range.
+ mEditableParent.onTextChange(
+ mEditableChild.asBinder(), text, start, unboundedOldEnd, causedOnlyByComposition);
+ return;
+ }
+ // Using large text causes TransactionTooLargeException, so split text data.
+ int offset = 0;
+ int newUnboundedOldEnd = unboundedOldEnd;
+ while (offset < text.length()) {
+ final int end = Math.min(offset + 1024 * 64 /* 64KB */, text.length());
+ mEditableParent.onTextChange(
+ mEditableChild.asBinder(),
+ text.subSequence(offset, end),
+ start + offset,
+ newUnboundedOldEnd,
+ causedOnlyByComposition);
+ offset = end;
+ newUnboundedOldEnd = start + offset;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onDefaultKeyEvent(final KeyEvent event) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ final StringBuilder sb = new StringBuilder("onDefaultKeyEvent(");
+ sb.append("action=")
+ .append(event.getAction())
+ .append(", ")
+ .append("keyCode=")
+ .append(event.getKeyCode())
+ .append(", ")
+ .append("metaState=")
+ .append(event.getMetaState())
+ .append(", ")
+ .append("time=")
+ .append(event.getEventTime())
+ .append(", ")
+ .append("repeatCount=")
+ .append(event.getRepeatCount())
+ .append(")");
+ Log.d(LOGTAG, sb.toString());
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ try {
+ mEditableParent.onDefaultKeyEvent(mEditableChild.asBinder(), event);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Remote call failed", e);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void updateCompositionRects(final RectF[] rects, final RectF caretRect) {
+ if (DEBUG) {
+ // GeckoEditableListener methods should all be called from the Gecko thread
+ ThreadUtils.assertOnGeckoThread();
+ Log.d(LOGTAG, "updateCompositionRects(rects.length = " + rects.length + ")");
+ }
+ if (!hasEditableParent()) {
+ return;
+ }
+
+ try {
+ mEditableParent.updateCompositionRects(mEditableChild.asBinder(), rects, caretRect);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Remote call failed", e);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java
new file mode 100644
index 0000000000..0e18cec515
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java
@@ -0,0 +1,807 @@
+/* -*- 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;
+
+import android.os.Build;
+import android.os.Looper;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.geckoview.GeckoResult;
+
+/**
+ * Takes samples and adds markers for Java threads for the Gecko profiler.
+ *
+ * <p>This class is thread safe because it uses synchronized on accesses to its mutable state. One
+ * exception is {@link #isProfilerActive()}: see the javadoc for details.
+ */
+public class GeckoJavaSampler {
+ private static final String LOGTAG = "GeckoJavaSampler";
+
+ /**
+ * The thread ID to use for the main thread instead of its true thread ID.
+ *
+ * <p>The main thread is sampled twice: once for native code and once on the JVM. The native
+ * version uses the thread's id so we replace it to avoid a collision. We use this thread ID
+ * because it's unlikely any other thread currently has it. We can't use 0 because 0 is considered
+ * "unspecified" in native code:
+ * https://searchfox.org/mozilla-central/rev/d4ebb53e719b913afdbcf7c00e162f0e96574701/mozglue/baseprofiler/public/BaseProfilerUtils.h#194
+ */
+ private static final long REPLACEMENT_MAIN_THREAD_ID = 1;
+
+ /**
+ * The thread name to use for the main thread instead of its true thread name. The name is "main",
+ * which is ambiguous with the JS main thread, so we rename it to match the C++ replacement. We
+ * expect our code to later add a suffix to avoid a collision with the C++ thread name. See {@link
+ * #REPLACEMENT_MAIN_THREAD_ID} for related details.
+ */
+ private static final String REPLACEMENT_MAIN_THREAD_NAME = "AndroidUI";
+
+ @GuardedBy("GeckoJavaSampler.class")
+ private static SamplingRunnable sSamplingRunnable;
+
+ @GuardedBy("GeckoJavaSampler.class")
+ private static ScheduledExecutorService sSamplingScheduler;
+
+ // See isProfilerActive for details on the AtomicReference.
+ @GuardedBy("GeckoJavaSampler.class")
+ private static final AtomicReference<ScheduledFuture<?>> sSamplingFuture =
+ new AtomicReference<>();
+
+ private static final MarkerStorage sMarkerStorage = new MarkerStorage();
+
+ /**
+ * Returns true if profiler is running and unpaused at the moment which means it's allowed to add
+ * a marker.
+ *
+ * <p>Thread policy: we want this method to be inexpensive (i.e. non-blocking) because we want to
+ * be able to use it in performance-sensitive code. That's why we rely on an AtomicReference. If
+ * this requirement didn't exist, the AtomicReference could be removed because the class thread
+ * policy is to call synchronized on mutable state access.
+ */
+ public static boolean isProfilerActive() {
+ // This value will only be present if the profiler is started and not paused.
+ return sSamplingFuture.get() != null;
+ }
+
+ // Use the same timer primitive as the profiler
+ // to get a perfect sample syncing.
+ @WrapForJNI
+ private static native double getProfilerTime();
+
+ /** Try to get the profiler time. Returns null if profiler is not running. */
+ public static @Nullable Double tryToGetProfilerTime() {
+ if (!isProfilerActive()) {
+ // Android profiler hasn't started yet.
+ return null;
+ }
+ if (!GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) {
+ // getProfilerTime is not available yet; either libs are not loaded,
+ // or profiling hasn't started on the Gecko side yet
+ return null;
+ }
+
+ return getProfilerTime();
+ }
+
+ /**
+ * A data container for a profiler sample. This class is effectively immutable (i.e. technically
+ * mutable but never mutated after construction) so is thread safe *if it is safely published*
+ * (see Java Concurrency in Practice, 2nd Ed., Section 3.5.3 for safe publication idioms).
+ */
+ private static class Sample {
+ public final long mThreadId;
+ public final Frame[] mFrames;
+ public final double mTime;
+ public final long mJavaTime; // non-zero if Android system time is used
+
+ public Sample(final long aThreadId, final StackTraceElement[] aStack) {
+ mThreadId = aThreadId;
+ mFrames = new Frame[aStack.length];
+ mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0;
+
+ // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded,
+ // or profiling hasn't started on the Gecko side yet
+ mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0;
+
+ for (int i = 0; i < aStack.length; i++) {
+ mFrames[aStack.length - 1 - i] =
+ new Frame(aStack[i].getMethodName(), aStack[i].getClassName());
+ }
+ }
+ }
+
+ /**
+ * A container for the metadata around a call in a stack. This class is thread safe by being
+ * immutable.
+ */
+ private static class Frame {
+ public final String methodName;
+ public final String className;
+
+ private Frame(final String methodName, final String className) {
+ this.methodName = methodName;
+ this.className = className;
+ }
+ }
+
+ /** A data container for thread metadata. */
+ private static class ThreadInfo {
+ private final long mId;
+ private final String mName;
+
+ public ThreadInfo(final long mId, final String mName) {
+ this.mId = mId;
+ this.mName = mName;
+ }
+
+ @WrapForJNI
+ public long getId() {
+ return mId;
+ }
+
+ @WrapForJNI
+ public String getName() {
+ return mName;
+ }
+ }
+
+ /**
+ * A data container for metadata around a marker. This class is thread safe by being immutable.
+ */
+ private static class Marker extends JNIObject {
+ /** The id of the thread this marker was captured on. */
+ private final long mThreadId;
+
+ /** Name of the marker */
+ private final String mMarkerName;
+
+ /** Either start time for the duration markers or time for a point-in-time markers. */
+ private final double mTime;
+
+ /**
+ * A fallback field of {@link #mTime} but it only exists when {@link #getProfilerTime()} is
+ * failed. It is non-zero if Android time is used.
+ */
+ private final long mJavaTime;
+
+ /** End time for the duration markers. It's zero for point-in-time markers. */
+ private final double mEndTime;
+
+ /**
+ * A fallback field of {@link #mEndTime} but it only exists when {@link #getProfilerTime()} is
+ * failed. It is non-zero if Android time is used.
+ */
+ private final long mEndJavaTime;
+
+ /** A nullable additional information field for the marker. */
+ private @Nullable final String mText;
+
+ /**
+ * Constructor for the Marker class. It initializes different kinds of markers depending on the
+ * parameters. Here are some combinations to create different kinds of markers:
+ *
+ * <p>If you want to create a marker that points a single point in time: <code>
+ * new Marker("name", null, null, null)</code> to implicitly get the time when this marker is
+ * added, or <code>new Marker("name", null, endTime, null)</code> to use an explicit time as an
+ * end time retrieved from {@link #tryToGetProfilerTime()}.
+ *
+ * <p>If you want to create a marker that has a start and end time: <code>
+ * new Marker("name", startTime, null, null)</code> to implicitly get the end time when this
+ * marker is added, or <code>new Marker("name", startTime, endTime, null)</code> to explicitly
+ * give the marker start and end time retrieved from {@link #tryToGetProfilerTime()}.
+ *
+ * <p>Last parameter is optional and can be given with any combination. This gives users the
+ * ability to add more context into a marker.
+ *
+ * @param aThreadId The id of the thread this marker was captured on.
+ * @param aMarkerName Identifier of the marker as a string.
+ * @param aStartTime Start time as Double. It can be null if you want to mark a point of time.
+ * @param aEndTime End time as Double. If it's null, this function implicitly gets the end time.
+ * @param aText An optional string field for more information about the marker.
+ */
+ public Marker(
+ final long aThreadId,
+ @NonNull final String aMarkerName,
+ @Nullable final Double aStartTime,
+ @Nullable final Double aEndTime,
+ @Nullable final String aText) {
+ mThreadId = getAdjustedThreadId(aThreadId);
+ mMarkerName = aMarkerName;
+ mText = aText;
+
+ if (aStartTime != null) {
+ // Start time is provided. This is an interval marker.
+ mTime = aStartTime;
+ mJavaTime = 0;
+ if (aEndTime != null) {
+ // End time is also provided.
+ mEndTime = aEndTime;
+ mEndJavaTime = 0;
+ } else {
+ // End time is not provided. Get the profiler time now and use it.
+ mEndTime =
+ GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0;
+
+ // if mEndTime == 0, getProfilerTime is not available yet; either libs are not loaded,
+ // or profiling hasn't started on the Gecko side yet
+ mEndJavaTime = mEndTime == 0.0d ? SystemClock.elapsedRealtime() : 0;
+ }
+
+ } else {
+ // Start time is not provided. This is point-in-time marker.
+ mEndTime = 0;
+ mEndJavaTime = 0;
+
+ if (aEndTime != null) {
+ // End time is also provided. Use that to point the time.
+ mTime = aEndTime;
+ mJavaTime = 0;
+ } else {
+ mTime = GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY) ? getProfilerTime() : 0;
+
+ // if mTime == 0, getProfilerTime is not available yet; either libs are not loaded,
+ // or profiling hasn't started on the Gecko side yet
+ mJavaTime = mTime == 0.0d ? SystemClock.elapsedRealtime() : 0;
+ }
+ }
+ }
+
+ @WrapForJNI
+ @Override // JNIObject
+ protected native void disposeNative();
+
+ @WrapForJNI
+ public double getStartTime() {
+ if (mJavaTime != 0) {
+ return (mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime();
+ }
+ return mTime;
+ }
+
+ @WrapForJNI
+ public double getEndTime() {
+ if (mEndJavaTime != 0) {
+ return (mEndJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime();
+ }
+ return mEndTime;
+ }
+
+ @WrapForJNI
+ public long getThreadId() {
+ return mThreadId;
+ }
+
+ @WrapForJNI
+ public @NonNull String getMarkerName() {
+ return mMarkerName;
+ }
+
+ @WrapForJNI
+ public @Nullable String getMarkerText() {
+ return mText;
+ }
+ }
+
+ /**
+ * Public method to add a new marker to Gecko profiler. This can be used to add a marker *inside*
+ * the geckoview code, but ideally ProfilerController methods should be used instead.
+ *
+ * @see Marker#Marker(long, String, Double, Double, String) for information about the parameter
+ * options.
+ */
+ public static void addMarker(
+ @NonNull final String aMarkerName,
+ @Nullable final Double aStartTime,
+ @Nullable final Double aEndTime,
+ @Nullable final String aText) {
+ sMarkerStorage.addMarker(aMarkerName, aStartTime, aEndTime, aText);
+ }
+
+ /**
+ * A routine to store profiler samples. This class is thread safe because it synchronizes access
+ * to its mutable state.
+ */
+ private static class SamplingRunnable implements Runnable {
+ private final long mMainThreadId = Looper.getMainLooper().getThread().getId();
+
+ // Sampling interval that is used by start and unpause
+ public final int mInterval;
+ private final int mSampleCount;
+
+ @GuardedBy("GeckoJavaSampler.class")
+ private boolean mBufferOverflowed = false;
+
+ @GuardedBy("GeckoJavaSampler.class")
+ private @NonNull final List<Thread> mThreadsToProfile;
+
+ @GuardedBy("GeckoJavaSampler.class")
+ private final Sample[] mSamples;
+
+ @GuardedBy("GeckoJavaSampler.class")
+ private int mSamplePos;
+
+ public SamplingRunnable(
+ @NonNull final List<Thread> aThreadsToProfile,
+ final int aInterval,
+ final int aSampleCount) {
+ mThreadsToProfile = aThreadsToProfile;
+ // Sanity check of sampling interval.
+ mInterval = Math.max(1, aInterval);
+ mSampleCount = aSampleCount;
+ mSamples = new Sample[mSampleCount];
+ mSamplePos = 0;
+ }
+
+ @Override
+ public void run() {
+ synchronized (GeckoJavaSampler.class) {
+ // To minimize allocation in the critical section, we use a traditional for loop instead of
+ // a for each (i.e. `elem : coll`) loop because that allocates an iterator.
+ //
+ // We won't capture threads that are started during profiling because we iterate through an
+ // unchanging list of threads (bug 1759550).
+ for (int i = 0; i < mThreadsToProfile.size(); i++) {
+ final Thread thread = mThreadsToProfile.get(i);
+
+ // getStackTrace will return an empty trace if the thread is not alive: we call continue
+ // to avoid wasting space in the buffer for an empty sample.
+ final StackTraceElement[] stackTrace = thread.getStackTrace();
+ if (stackTrace.length == 0) {
+ continue;
+ }
+
+ mSamples[mSamplePos] = new Sample(thread.getId(), stackTrace);
+ mSamplePos += 1;
+ if (mSamplePos == mSampleCount) {
+ // Sample array is full now, go back to start of
+ // the array and override old samples
+ mSamplePos = 0;
+ mBufferOverflowed = true;
+ }
+ }
+ }
+ }
+
+ private Sample getSample(final int aSampleId) {
+ synchronized (GeckoJavaSampler.class) {
+ if (aSampleId >= mSampleCount) {
+ // Return early because there is no more sample left.
+ return null;
+ }
+
+ int samplePos = aSampleId;
+ if (mBufferOverflowed) {
+ // This is a circular buffer and the buffer is overflowed. Start
+ // of the buffer is mSamplePos now. Calculate the real index.
+ samplePos = (samplePos + mSamplePos) % mSampleCount;
+ }
+
+ // Since the array elements are initialized to null, it will return
+ // null whenever we access to an element that's not been written yet.
+ // We want it to return null in that case, so it's okay.
+ return mSamples[samplePos];
+ }
+ }
+ }
+
+ /**
+ * Returns the sample with the given sample ID.
+ *
+ * <p>Thread safety code smell: this method call is synchronized but this class returns a
+ * reference to an effectively immutable object so that the reference is accessible after
+ * synchronization ends. It's unclear if this is thread safe. However, this is safe with the
+ * current callers (because they are all synchronized and don't leak the Sample) so we don't
+ * investigate it further.
+ */
+ private static synchronized Sample getSample(final int aSampleId) {
+ return sSamplingRunnable.getSample(aSampleId);
+ }
+
+ @WrapForJNI
+ public static Marker pollNextMarker() {
+ return sMarkerStorage.pollNextMarker();
+ }
+
+ @WrapForJNI
+ public static synchronized int getRegisteredThreadCount() {
+ return sSamplingRunnable.mThreadsToProfile.size();
+ }
+
+ @WrapForJNI
+ public static synchronized ThreadInfo getRegisteredThreadInfo(final int aIndex) {
+ final Thread thread = sSamplingRunnable.mThreadsToProfile.get(aIndex);
+
+ // See REPLACEMENT_MAIN_THREAD_NAME for why we do this.
+ String adjustedThreadName =
+ thread.getId() == sSamplingRunnable.mMainThreadId
+ ? REPLACEMENT_MAIN_THREAD_NAME
+ : thread.getName();
+
+ // To distinguish JVM threads from native threads, we append a JVM-specific suffix.
+ adjustedThreadName += " (JVM)";
+ return new ThreadInfo(getAdjustedThreadId(thread.getId()), adjustedThreadName);
+ }
+
+ @WrapForJNI
+ public static synchronized long getThreadId(final int aSampleId) {
+ final Sample sample = getSample(aSampleId);
+ return getAdjustedThreadId(sample != null ? sample.mThreadId : 0);
+ }
+
+ private static synchronized long getAdjustedThreadId(final long threadId) {
+ // See REPLACEMENT_MAIN_THREAD_ID for why we do this.
+ return threadId == sSamplingRunnable.mMainThreadId ? REPLACEMENT_MAIN_THREAD_ID : threadId;
+ }
+
+ @WrapForJNI
+ public static synchronized double getSampleTime(final int aSampleId) {
+ final Sample sample = getSample(aSampleId);
+ if (sample != null) {
+ if (sample.mJavaTime != 0) {
+ return (sample.mJavaTime - SystemClock.elapsedRealtime()) + getProfilerTime();
+ }
+ return sample.mTime;
+ }
+ return 0;
+ }
+
+ @WrapForJNI
+ public static synchronized String getFrameName(final int aSampleId, final int aFrameId) {
+ final Sample sample = getSample(aSampleId);
+ if (sample != null && aFrameId < sample.mFrames.length) {
+ final Frame frame = sample.mFrames[aFrameId];
+ if (frame == null) {
+ return null;
+ }
+ return frame.className + "." + frame.methodName + "()";
+ }
+ return null;
+ }
+
+ /**
+ * A start/stop-aware container for storing profiler markers.
+ *
+ * <p>This class is thread safe: see {@link #mMarkers} and other member variables for the
+ * threading policy. Start/stop are guaranteed to execute in the order they are called but other
+ * methods do not have such ordering guarantees.
+ */
+ private static class MarkerStorage {
+ /**
+ * The underlying storage for the markers. This field maintains thread safety without using
+ * synchronized everywhere by:
+ * <li>- using volatile to allow non-blocking reads
+ * <li>- leveraging a thread safe collection when accessing the underlying data
+ * <li>- looping until success for compound read-write operations
+ */
+ private volatile Queue<Marker> mMarkers;
+
+ /**
+ * The thread ids of the threads we're profiling. This field maintains thread safety by writing
+ * a read-only value to this volatile field before concurrency begins and only reading it during
+ * concurrent sections.
+ */
+ private volatile Set<Long> mProfiledThreadIds = Collections.emptySet();
+
+ MarkerStorage() {}
+
+ public synchronized void start(final int aMarkerCount, final List<Thread> aProfiledThreads) {
+ if (this.mMarkers != null) {
+ return;
+ }
+ this.mMarkers = new LinkedBlockingQueue<>(aMarkerCount);
+
+ final Set<Long> profiledThreadIds = new HashSet<>(aProfiledThreads.size());
+ for (final Thread thread : aProfiledThreads) {
+ profiledThreadIds.add(thread.getId());
+ }
+
+ // We use a temporary collection, rather than mutating the collection within the member
+ // variable, to ensure the collection is fully written before the state is made available to
+ // all threads via the volatile write into the member variable. This collection must be
+ // read-only for it to remain thread safe.
+ mProfiledThreadIds = Collections.unmodifiableSet(profiledThreadIds);
+ }
+
+ public synchronized void stop() {
+ if (this.mMarkers == null) {
+ return;
+ }
+ this.mMarkers = null;
+ mProfiledThreadIds = Collections.emptySet();
+ }
+
+ private void addMarker(
+ @NonNull final String aMarkerName,
+ @Nullable final Double aStartTime,
+ @Nullable final Double aEndTime,
+ @Nullable final String aText) {
+ final Queue<Marker> markersQueue = this.mMarkers;
+ if (markersQueue == null) {
+ // Profiler is not active.
+ return;
+ }
+
+ final long threadId = Thread.currentThread().getId();
+ if (!mProfiledThreadIds.contains(threadId)) {
+ return;
+ }
+
+ final Marker newMarker = new Marker(threadId, aMarkerName, aStartTime, aEndTime, aText);
+ boolean successful = markersQueue.offer(newMarker);
+ while (!successful) {
+ // Marker storage is full, remove the head and add again.
+ markersQueue.poll();
+ successful = markersQueue.offer(newMarker);
+ }
+ }
+
+ private Marker pollNextMarker() {
+ final Queue<Marker> markersQueue = this.mMarkers;
+ if (markersQueue == null) {
+ // Profiler is not active.
+ return null;
+ }
+ // Retrieve and return the head of this queue.
+ // Returns null if the queue is empty.
+ return markersQueue.poll();
+ }
+ }
+
+ @WrapForJNI
+ public static void start(
+ @NonNull final Object[] aFilters, final int aInterval, final int aEntryCount) {
+ synchronized (GeckoJavaSampler.class) {
+ if (sSamplingRunnable != null) {
+ return;
+ }
+
+ final ScheduledFuture<?> future = sSamplingFuture.get();
+ if (future != null && !future.isDone()) {
+ return;
+ }
+
+ Log.i(LOGTAG, "Profiler starting. Calling thread: " + Thread.currentThread().getName());
+
+ // Setting a limit of 120000 (2 mins with 1ms interval) for samples and markers for now
+ // to make sure we are not allocating too much.
+ final int limitedEntryCount = Math.min(aEntryCount, 120000);
+
+ final List<Thread> threadsToProfile = getThreadsToProfile(aFilters);
+ if (threadsToProfile.size() < 1) {
+ throw new IllegalStateException("Expected >= 1 thread to profile (main thread).");
+ }
+ Log.i(LOGTAG, "Number of threads to profile: " + threadsToProfile.size());
+
+ sSamplingRunnable = new SamplingRunnable(threadsToProfile, aInterval, limitedEntryCount);
+ sMarkerStorage.start(limitedEntryCount, threadsToProfile);
+ sSamplingScheduler = Executors.newSingleThreadScheduledExecutor();
+ sSamplingFuture.set(
+ sSamplingScheduler.scheduleAtFixedRate(
+ sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ private static @NonNull List<Thread> getThreadsToProfile(final Object[] aFilters) {
+ // Clean up filters.
+ final List<String> cleanedFilters = new ArrayList<>();
+ for (final Object rawFilter : aFilters) {
+ // aFilters is a String[] but jni can only accept Object[] so we're forced to cast.
+ //
+ // We could pass the lowercased filters from native code but it may not handle lowercasing the
+ // same way Java does so we lower case here so it's consistent later when we lower case the
+ // thread name and compare against it.
+ final String filter = ((String) rawFilter).trim().toLowerCase(Locale.US);
+
+ // If the filter is empty, it's not meaningful: skip.
+ if (filter.isEmpty()) {
+ continue;
+ }
+
+ cleanedFilters.add(filter);
+ }
+
+ final ThreadGroup rootThreadGroup = getRootThreadGroup();
+ final Thread[] activeThreads = getActiveThreads(rootThreadGroup);
+ final Thread mainThread = Looper.getMainLooper().getThread();
+
+ // We model these catch-all filters after the C++ code (which we should eventually deduplicate):
+ // https://searchfox.org/mozilla-central/rev/b0779bcc485dc1c04334dfb9ea024cbfff7b961a/tools/profiler/core/platform.cpp#778-801
+ if (cleanedFilters.contains("*") || doAnyFiltersMatchPid(cleanedFilters, Process.myPid())) {
+ final List<Thread> activeThreadList = new ArrayList<>();
+ Collections.addAll(activeThreadList, activeThreads);
+ if (!activeThreadList.contains(mainThread)) {
+ activeThreadList.add(mainThread); // see below for why this is necessary.
+ }
+ return activeThreadList;
+ }
+
+ // We always want to profile the main thread. We're not certain getActiveThreads returns
+ // all active threads since we've observed that getActiveThreads doesn't include the main thread
+ // during xpcshell tests even though it's alive (bug 1760716). We intentionally don't rely on
+ // that method to add the main thread here.
+ final List<Thread> threadsToProfile = new ArrayList<>();
+ threadsToProfile.add(mainThread);
+
+ for (final Thread thread : activeThreads) {
+ if (shouldProfileThread(thread, cleanedFilters, mainThread)) {
+ threadsToProfile.add(thread);
+ }
+ }
+ return threadsToProfile;
+ }
+
+ private static boolean shouldProfileThread(
+ final Thread aThread, final List<String> aFilters, final Thread aMainThread) {
+ final String threadName = aThread.getName().trim().toLowerCase(Locale.US);
+ if (threadName.isEmpty()) {
+ return false; // We can't match against a thread with no name: skip.
+ }
+
+ if (aThread.equals(aMainThread)) {
+ return false; // We've already added the main thread outside of this method.
+ }
+
+ for (final String filter : aFilters) {
+ // In order to generically support thread pools with thread names like "arch_disk_io_0" (the
+ // kotlin IO dispatcher), we check if the filter is inside the thread name (e.g. a filter of
+ // "io" will match all of the threads in that pool) rather than an equality check.
+ if (threadName.contains(filter)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean doAnyFiltersMatchPid(
+ @NonNull final List<String> aFilters, final long aPid) {
+ final String prefix = "pid:";
+ for (final String filter : aFilters) {
+ if (!filter.startsWith(prefix)) {
+ continue;
+ }
+
+ try {
+ final long filterPid = Long.parseLong(filter.substring(prefix.length()));
+ if (filterPid == aPid) {
+ return true;
+ }
+ } catch (final NumberFormatException e) {
+ /* do nothing. */
+ }
+ }
+
+ return false;
+ }
+
+ private static @NonNull Thread[] getActiveThreads(final @NonNull ThreadGroup rootThreadGroup) {
+ // We need the root thread group to get all of the active threads because of how
+ // ThreadGroup.enumerate works.
+ //
+ // ThreadGroup.enumerate is inherently racey so we loop until we capture all of the active
+ // threads. We can only detect if we didn't capture all of the threads if the number of threads
+ // found (the value returned by enumerate) is smaller than the array we're capturing them in.
+ // Therefore, we make the array slightly larger than the known number of threads.
+ Thread[] allThreads;
+ int threadsFound;
+ do {
+ allThreads = new Thread[rootThreadGroup.activeCount() + 15];
+ threadsFound = rootThreadGroup.enumerate(allThreads, /* recurse */ true);
+ } while (threadsFound >= allThreads.length);
+
+ // There will be more indices in the array than threads and these will be set to null. We remove
+ // the null values to minimize bugs.
+ return Arrays.copyOfRange(allThreads, 0, threadsFound);
+ }
+
+ private static @NonNull ThreadGroup getRootThreadGroup() {
+ // Assert non-null: getThreadGroup only returns null for dead threads but the current thread
+ // can't be dead.
+ ThreadGroup parentGroup = Objects.requireNonNull(Thread.currentThread().getThreadGroup());
+
+ ThreadGroup group = null;
+ while (parentGroup != null) {
+ group = parentGroup;
+ parentGroup = group.getParent();
+ }
+ return group;
+ }
+
+ @WrapForJNI
+ public static void pauseSampling() {
+ synchronized (GeckoJavaSampler.class) {
+ final ScheduledFuture<?> future = sSamplingFuture.getAndSet(null);
+ future.cancel(false /* mayInterruptIfRunning */);
+ }
+ }
+
+ @WrapForJNI
+ public static void unpauseSampling() {
+ synchronized (GeckoJavaSampler.class) {
+ if (sSamplingFuture.get() != null) {
+ return;
+ }
+ sSamplingFuture.set(
+ sSamplingScheduler.scheduleAtFixedRate(
+ sSamplingRunnable, 0, sSamplingRunnable.mInterval, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @WrapForJNI
+ public static void stop() {
+ synchronized (GeckoJavaSampler.class) {
+ if (sSamplingRunnable == null) {
+ return;
+ }
+
+ Log.i(
+ LOGTAG,
+ "Profiler stopping. Sample array position: "
+ + sSamplingRunnable.mSamplePos
+ + ". Overflowed? "
+ + sSamplingRunnable.mBufferOverflowed);
+
+ try {
+ sSamplingScheduler.shutdown();
+ // 1s is enough to wait shutdown.
+ sSamplingScheduler.awaitTermination(1000, TimeUnit.MILLISECONDS);
+ } catch (final InterruptedException e) {
+ Log.e(LOGTAG, "Sampling scheduler isn't terminated. Last sampling data might be broken.");
+ sSamplingScheduler.shutdownNow();
+ }
+ sSamplingScheduler = null;
+ sSamplingRunnable = null;
+ sSamplingFuture.set(null);
+ sMarkerStorage.stop();
+ }
+ }
+
+ @WrapForJNI(dispatchTo = "gecko", stubName = "StartProfiler")
+ private static native void startProfilerNative(String[] aFilters, String[] aFeaturesArr);
+
+ @WrapForJNI(dispatchTo = "gecko", stubName = "StopProfiler")
+ private static native void stopProfilerNative(GeckoResult<byte[]> aResult);
+
+ public static void startProfiler(final String[] aFilters, final String[] aFeaturesArr) {
+ startProfilerNative(aFilters, aFeaturesArr);
+ }
+
+ public static GeckoResult<byte[]> stopProfiler() {
+ final GeckoResult<byte[]> result = new GeckoResult<byte[]>();
+ stopProfilerNative(result);
+ return result;
+ }
+
+ /** Returns the device brand and model as a string. */
+ @WrapForJNI
+ public static String getDeviceInformation() {
+ final StringBuilder sb = new StringBuilder(Build.BRAND);
+ sb.append(" ");
+ sb.append(Build.MODEL);
+ return sb.toString();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
new file mode 100644
index 0000000000..02ed848f6b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoNetworkManager.java
@@ -0,0 +1,413 @@
+/* -*- 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;
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.DhcpInfo;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.NetworkUtils;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionSubType;
+import org.mozilla.gecko.util.NetworkUtils.ConnectionType;
+import org.mozilla.gecko.util.NetworkUtils.NetworkStatus;
+
+/**
+ * Provides connection type, subtype and general network status (up/down).
+ *
+ * <p>According to spec of Network Information API version 3, connection types include: bluetooth,
+ * cellular, ethernet, none, wifi and other. The objective of providing such general connection is
+ * due to some security concerns. In short, we don't want to expose exact network type, especially
+ * the cellular network type.
+ *
+ * <p>Specific mobile subtypes are mapped to general 2G, 3G and 4G buckets.
+ *
+ * <p>Logic is implemented as a state machine, so see the transition matrix to figure out what
+ * happens when. This class depends on access to the context, so only use after GeckoAppShell has
+ * been initialized.
+ */
+public class GeckoNetworkManager extends BroadcastReceiver {
+ private static final String LOGTAG = "GeckoNetworkManager";
+
+ // If network configuration and/or status changed, we send details of what changed.
+ // If we received a "check out new network state!" intent from the OS but nothing in it looks
+ // different, we ignore it. See Bug 1330836 for some relevant details.
+ private static final String LINK_DATA_CHANGED = "changed";
+
+ private static GeckoNetworkManager instance;
+
+ // We hackishly (yet harmlessly, in this case) keep a Context reference passed in via the start
+ // method.
+ // See context handling notes in handleManagerEvent, and Bug 1277333.
+ private Context mContext;
+
+ public static void destroy() {
+ if (instance != null) {
+ instance.onDestroy();
+ instance = null;
+ }
+ }
+
+ public enum ManagerState {
+ OffNoListeners,
+ OffWithListeners,
+ OnNoListeners,
+ OnWithListeners
+ }
+
+ public enum ManagerEvent {
+ start,
+ stop,
+ enableNotifications,
+ disableNotifications,
+ receivedUpdate
+ }
+
+ private ManagerState mCurrentState = ManagerState.OffNoListeners;
+ private ConnectionType mCurrentConnectionType = ConnectionType.NONE;
+ private ConnectionType mPreviousConnectionType = ConnectionType.NONE;
+ private ConnectionSubType mCurrentConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private ConnectionSubType mPreviousConnectionSubtype = ConnectionSubType.UNKNOWN;
+ private NetworkStatus mCurrentNetworkStatus = NetworkStatus.UNKNOWN;
+ private NetworkStatus mPreviousNetworkStatus = NetworkStatus.UNKNOWN;
+
+ private GeckoNetworkManager() {}
+
+ private void onDestroy() {
+ handleManagerEvent(ManagerEvent.stop);
+ }
+
+ public static GeckoNetworkManager getInstance() {
+ if (instance == null) {
+ instance = new GeckoNetworkManager();
+ }
+
+ return instance;
+ }
+
+ public double[] getCurrentInformation() {
+ final Context applicationContext = GeckoAppShell.getApplicationContext();
+ final ConnectionType connectionType = mCurrentConnectionType;
+ return new double[] {
+ connectionType.value,
+ connectionType == ConnectionType.WIFI ? 1.0 : 0.0,
+ connectionType == ConnectionType.WIFI ? wifiDhcpGatewayAddress(applicationContext) : 0.0
+ };
+ }
+
+ @Override
+ public void onReceive(final Context aContext, final Intent aIntent) {
+ handleManagerEvent(ManagerEvent.receivedUpdate);
+ }
+
+ public void start(final Context context) {
+ mContext = context;
+ handleManagerEvent(ManagerEvent.start);
+ }
+
+ public void stop() {
+ handleManagerEvent(ManagerEvent.stop);
+ }
+
+ public void enableNotifications() {
+ handleManagerEvent(ManagerEvent.enableNotifications);
+ }
+
+ public void disableNotifications() {
+ handleManagerEvent(ManagerEvent.disableNotifications);
+ }
+
+ /**
+ * For a given event, figure out the next state, run any transition by-product actions, and switch
+ * current state to the next state. If event is invalid for the current state, this is a no-op.
+ *
+ * @param event Incoming event
+ * @return Boolean indicating if transition was performed.
+ */
+ private synchronized boolean handleManagerEvent(final ManagerEvent event) {
+ final ManagerState nextState = getNextState(mCurrentState, event);
+
+ Log.d(LOGTAG, "Incoming event " + event + " for state " + mCurrentState + " -> " + nextState);
+ if (nextState == null) {
+ Log.w(LOGTAG, "Invalid event " + event + " for state " + mCurrentState);
+ return false;
+ }
+
+ // We're being deliberately careful about handling context here; it's possible that in some
+ // rare cases and possibly related to timing of when this is called (seems to be early in the
+ // startup phase),
+ // GeckoAppShell.getApplicationContext() will be null, and .start() wasn't called yet,
+ // so we don't have a local Context reference either. If both of these are true, we have to drop
+ // the event.
+ // NB: this is hacky (and these checks attempt to isolate the hackiness), and root cause
+ // seems to be how this class fits into the larger ecosystem and general flow of events.
+ // See Bug 1277333.
+ final Context contextForAction;
+ if (mContext != null) {
+ contextForAction = mContext;
+ } else {
+ contextForAction = GeckoAppShell.getApplicationContext();
+ }
+
+ if (contextForAction == null) {
+ Log.w(
+ LOGTAG,
+ "Context is not available while processing event "
+ + event
+ + " for state "
+ + mCurrentState);
+ return false;
+ }
+
+ performActionsForStateEvent(contextForAction, mCurrentState, event);
+ mCurrentState = nextState;
+
+ return true;
+ }
+
+ /**
+ * Defines a transition matrix for our state machine. For a given state/event pair, returns
+ * nextState.
+ *
+ * @param currentState Current state against which we have an incoming event
+ * @param event Incoming event for which we'd like to figure out the next state
+ * @return State into which we should transition as result of given event
+ */
+ @Nullable
+ public static ManagerState getNextState(
+ final @NonNull ManagerState currentState, final @NonNull ManagerEvent event) {
+ switch (currentState) {
+ case OffNoListeners:
+ switch (event) {
+ case start:
+ return ManagerState.OnNoListeners;
+ case enableNotifications:
+ return ManagerState.OffWithListeners;
+ default:
+ return null;
+ }
+ case OnNoListeners:
+ switch (event) {
+ case stop:
+ return ManagerState.OffNoListeners;
+ case enableNotifications:
+ return ManagerState.OnWithListeners;
+ case receivedUpdate:
+ return ManagerState.OnNoListeners;
+ default:
+ return null;
+ }
+ case OnWithListeners:
+ switch (event) {
+ case stop:
+ return ManagerState.OffWithListeners;
+ case disableNotifications:
+ return ManagerState.OnNoListeners;
+ case receivedUpdate:
+ return ManagerState.OnWithListeners;
+ default:
+ return null;
+ }
+ case OffWithListeners:
+ switch (event) {
+ case start:
+ return ManagerState.OnWithListeners;
+ case disableNotifications:
+ return ManagerState.OffNoListeners;
+ default:
+ return null;
+ }
+ default:
+ throw new IllegalStateException("Unknown current state: " + currentState.name());
+ }
+ }
+
+ /**
+ * For a given state/event combination, run any actions which are by-products of leaving the state
+ * because of a given event. Since this is a deterministic state machine, we can easily do that
+ * without any additional information.
+ *
+ * @param currentState State which we are leaving
+ * @param event Event which is causing us to leave the state
+ */
+ private void performActionsForStateEvent(
+ final Context context, final ManagerState currentState, final ManagerEvent event) {
+ // NB: network state might be queried via getCurrentInformation at any time; pre-rewrite
+ // behaviour was
+ // that network state was updated whenever enableNotifications was called. To avoid deviating
+ // from previous behaviour and causing weird side-effects, we call
+ // updateNetworkStateAndConnectionType
+ // whenever notifications are enabled.
+ switch (currentState) {
+ case OffNoListeners:
+ if (event == ManagerEvent.start) {
+ updateNetworkStateAndConnectionType(context);
+ registerBroadcastReceiver(context, this);
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType(context);
+ }
+ break;
+ case OnNoListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType(context);
+ sendNetworkStateToListeners(context);
+ }
+ if (event == ManagerEvent.enableNotifications) {
+ updateNetworkStateAndConnectionType(context);
+ registerBroadcastReceiver(context, this);
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver(context, this);
+ }
+ break;
+ case OnWithListeners:
+ if (event == ManagerEvent.receivedUpdate) {
+ updateNetworkStateAndConnectionType(context);
+ sendNetworkStateToListeners(context);
+ }
+ if (event == ManagerEvent.stop) {
+ unregisterBroadcastReceiver(context, this);
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ case OffWithListeners:
+ if (event == ManagerEvent.start) {
+ registerBroadcastReceiver(context, this);
+ }
+ /* no-op event: ManagerEvent.disableNotifications */
+ break;
+ default:
+ throw new IllegalStateException("Unknown current state: " + currentState.name());
+ }
+ }
+
+ /** Update current network state and connection types. */
+ private void updateNetworkStateAndConnectionType(final Context context) {
+ final ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ // Type/status getters below all have a defined behaviour for when connectivityManager == null
+ if (connectivityManager == null) {
+ Log.e(LOGTAG, "ConnectivityManager does not exist.");
+ }
+ mCurrentConnectionType = NetworkUtils.getConnectionType(connectivityManager);
+ mCurrentNetworkStatus = NetworkUtils.getNetworkStatus(connectivityManager);
+ mCurrentConnectionSubtype = NetworkUtils.getConnectionSubType(connectivityManager);
+ Log.d(
+ LOGTAG,
+ "New network state: "
+ + mCurrentNetworkStatus
+ + ", "
+ + mCurrentConnectionType
+ + ", "
+ + mCurrentConnectionSubtype);
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onConnectionChanged(
+ int type, String subType, boolean isWifi, int dhcpGateway);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void onStatusChanged(String status);
+
+ /** Send current network state and connection type to whomever is listening. */
+ private void sendNetworkStateToListeners(final Context context) {
+ final boolean connectionTypeOrSubtypeChanged =
+ mCurrentConnectionType != mPreviousConnectionType
+ || mCurrentConnectionSubtype != mPreviousConnectionSubtype;
+ if (connectionTypeOrSubtypeChanged) {
+ mPreviousConnectionType = mCurrentConnectionType;
+ mPreviousConnectionSubtype = mCurrentConnectionSubtype;
+
+ final boolean isWifi = mCurrentConnectionType == ConnectionType.WIFI;
+ final int gateway = !isWifi ? 0 : wifiDhcpGatewayAddress(context);
+
+ if (GeckoThread.isRunning()) {
+ onConnectionChanged(
+ mCurrentConnectionType.value, mCurrentConnectionSubtype.value, isWifi, gateway);
+ } else {
+ GeckoThread.queueNativeCall(
+ GeckoNetworkManager.class,
+ "onConnectionChanged",
+ mCurrentConnectionType.value,
+ String.class,
+ mCurrentConnectionSubtype.value,
+ isWifi,
+ gateway);
+ }
+ }
+
+ // If neither network status nor network configuration changed, do nothing.
+ if (mCurrentNetworkStatus == mPreviousNetworkStatus && !connectionTypeOrSubtypeChanged) {
+ return;
+ }
+
+ // If network status remains the same, send "changed". Otherwise, send new network status.
+ // See Bug 1330836 for relevant details.
+ final String status;
+ if (mCurrentNetworkStatus == mPreviousNetworkStatus) {
+ status = LINK_DATA_CHANGED;
+ } else {
+ mPreviousNetworkStatus = mCurrentNetworkStatus;
+ status = mCurrentNetworkStatus.value;
+ }
+
+ if (GeckoThread.isRunning()) {
+ onStatusChanged(status);
+ } else {
+ GeckoThread.queueNativeCall(
+ GeckoNetworkManager.class, "onStatusChanged", String.class, status);
+ }
+ }
+
+ /** Stop listening for network state updates. */
+ private static void unregisterBroadcastReceiver(
+ final Context context, final BroadcastReceiver receiver) {
+ context.unregisterReceiver(receiver);
+ }
+
+ /** Start listening for network state updates. */
+ private static void registerBroadcastReceiver(
+ final Context context, final BroadcastReceiver receiver) {
+ final IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ context.registerReceiver(receiver, filter);
+ }
+
+ private static int wifiDhcpGatewayAddress(final Context context) {
+ if (context == null) {
+ return 0;
+ }
+
+ try {
+ final WifiManager mgr =
+ (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ if (mgr == null) {
+ return 0;
+ }
+
+ @SuppressLint("MissingPermission")
+ final DhcpInfo d = mgr.getDhcpInfo();
+ if (d == null) {
+ return 0;
+ }
+
+ return d.gateway;
+
+ } catch (final Exception ex) {
+ // getDhcpInfo() is not documented to require any permissions, but on some devices
+ // requires android.permission.ACCESS_WIFI_STATE. Just catch the generic exception
+ // here and returning 0. Not logging because this could be noisy.
+ return 0;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenChangeListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenChangeListener.java
new file mode 100644
index 0000000000..78d66cc352
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenChangeListener.java
@@ -0,0 +1,73 @@
+/* -*- Mode: Java; c-basic-offset: 2; 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;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.util.Log;
+import android.view.Display;
+
+public class GeckoScreenChangeListener implements DisplayManager.DisplayListener {
+ private static final String LOGTAG = "ScreenChangeListener";
+ private static final boolean DEBUG = false;
+
+ public GeckoScreenChangeListener() {}
+
+ @Override
+ public void onDisplayAdded(final int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(final int displayId) {}
+
+ @Override
+ public void onDisplayChanged(final int displayId) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onDisplayChanged");
+ }
+
+ // Even if onDisplayChanged is called, Configuration may not updated yet.
+ // So we use Display's data instead.
+ if (displayId != Display.DEFAULT_DISPLAY) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "Primary display is only supported");
+ }
+ return;
+ }
+
+ final DisplayManager displayManager = getDisplayManager();
+ if (displayManager == null) {
+ return;
+ }
+
+ if (GeckoScreenOrientation.getInstance().update(displayManager.getDisplay(displayId))) {
+ // refreshScreenInfo is already called.
+ return;
+ }
+
+ ScreenManagerHelper.refreshScreenInfo();
+ }
+
+ private static DisplayManager getDisplayManager() {
+ return (DisplayManager)
+ GeckoAppShell.getApplicationContext().getSystemService(Context.DISPLAY_SERVICE);
+ }
+
+ public void enable() {
+ final DisplayManager displayManager = getDisplayManager();
+ if (displayManager == null) {
+ return;
+ }
+ displayManager.registerDisplayListener(this, null);
+ }
+
+ public void disable() {
+ final DisplayManager displayManager = getDisplayManager();
+ if (displayManager == null) {
+ return;
+ }
+ displayManager.unregisterDisplayListener(this);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java
new file mode 100644
index 0000000000..ce7a48c4da
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoScreenOrientation.java
@@ -0,0 +1,273 @@
+/* -*- 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;
+
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.Display;
+import android.view.Surface;
+import java.util.ArrayList;
+import java.util.List;
+import org.mozilla.gecko.util.ThreadUtils;
+
+/*
+ * Updates, locks and unlocks the screen orientation.
+ *
+ * Note: Replaces the OnOrientationChangeListener to avoid redundant rotation
+ * event handling.
+ */
+public class GeckoScreenOrientation {
+ private static final String LOGTAG = "GeckoScreenOrientation";
+
+ // Make sure that any change in hal/HalScreenConfiguration.h happens here too.
+ public enum ScreenOrientation {
+ NONE(0),
+ PORTRAIT_PRIMARY(1 << 0),
+ PORTRAIT_SECONDARY(1 << 1),
+ PORTRAIT(PORTRAIT_PRIMARY.value | PORTRAIT_SECONDARY.value),
+ LANDSCAPE_PRIMARY(1 << 2),
+ LANDSCAPE_SECONDARY(1 << 3),
+ LANDSCAPE(LANDSCAPE_PRIMARY.value | LANDSCAPE_SECONDARY.value),
+ ANY(
+ PORTRAIT_PRIMARY.value
+ | PORTRAIT_SECONDARY.value
+ | LANDSCAPE_PRIMARY.value
+ | LANDSCAPE_SECONDARY.value),
+ DEFAULT(1 << 4);
+
+ public final short value;
+
+ ScreenOrientation(final int value) {
+ this.value = (short) value;
+ }
+
+ private static final ScreenOrientation[] sValues = ScreenOrientation.values();
+
+ public static ScreenOrientation get(final int value) {
+ for (final ScreenOrientation orient : sValues) {
+ if (orient.value == value) {
+ return orient;
+ }
+ }
+ return NONE;
+ }
+ }
+
+ // Singleton instance.
+ private static GeckoScreenOrientation sInstance;
+ // Default rotation, used when device rotation is unknown.
+ private static final int DEFAULT_ROTATION = Surface.ROTATION_0;
+ // Last updated screen orientation with Gecko value space.
+ private ScreenOrientation mScreenOrientation = ScreenOrientation.PORTRAIT_PRIMARY;
+
+ public interface OrientationChangeListener {
+ void onScreenOrientationChanged(ScreenOrientation newOrientation);
+ }
+
+ private final List<OrientationChangeListener> mListeners;
+
+ public static GeckoScreenOrientation getInstance() {
+ if (sInstance == null) {
+ sInstance = new GeckoScreenOrientation();
+ }
+ return sInstance;
+ }
+
+ private GeckoScreenOrientation() {
+ mListeners = new ArrayList<>();
+ update();
+ }
+
+ /** Add a listener that will be notified when the screen orientation has changed. */
+ public void addListener(final OrientationChangeListener aListener) {
+ ThreadUtils.assertOnUiThread();
+ mListeners.add(aListener);
+ }
+
+ /** Remove a OrientationChangeListener again. */
+ public void removeListener(final OrientationChangeListener aListener) {
+ ThreadUtils.assertOnUiThread();
+ mListeners.remove(aListener);
+ }
+
+ /*
+ * Update screen orientation.
+ * Retrieve orientation and rotation via GeckoAppShell.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update() {
+ // Check whether we have the application context for fenix/a-c unit test.
+ final Context appContext = GeckoAppShell.getApplicationContext();
+ if (appContext == null) {
+ return false;
+ }
+ final Rect rect = GeckoAppShell.getScreenSizeIgnoreOverride();
+ final int orientation =
+ rect.width() >= rect.height() ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
+ return update(getScreenOrientation(orientation, getRotation()));
+ }
+
+ /*
+ * Update screen orientation.
+ * Retrieve orientation and rotation via Display.
+ *
+ * @param aDisplay The Display that has screen orientation information
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update(final Display aDisplay) {
+ return update(getScreenOrientation(aDisplay));
+ }
+
+ /*
+ * Update screen orientation given the android orientation.
+ * Retrieve rotation via GeckoAppShell.
+ *
+ * @param aAndroidOrientation
+ * Android screen orientation from Configuration.orientation.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public boolean update(final int aAndroidOrientation) {
+ return update(getScreenOrientation(aAndroidOrientation, getRotation()));
+ }
+
+ /*
+ * Update screen orientation given the screen orientation.
+ *
+ * @param aScreenOrientation
+ * Gecko screen orientation based on android orientation and rotation.
+ *
+ * @return Whether the screen orientation has changed.
+ */
+ public synchronized boolean update(final ScreenOrientation aScreenOrientation) {
+ // Gecko expects a definite screen orientation, so we default to the
+ // primary orientations.
+ final ScreenOrientation screenOrientation;
+ if ((aScreenOrientation.value & ScreenOrientation.PORTRAIT_PRIMARY.value) != 0) {
+ screenOrientation = ScreenOrientation.PORTRAIT_PRIMARY;
+ } else if ((aScreenOrientation.value & ScreenOrientation.PORTRAIT_SECONDARY.value) != 0) {
+ screenOrientation = ScreenOrientation.PORTRAIT_SECONDARY;
+ } else if ((aScreenOrientation.value & ScreenOrientation.LANDSCAPE_PRIMARY.value) != 0) {
+ screenOrientation = ScreenOrientation.LANDSCAPE_PRIMARY;
+ } else if ((aScreenOrientation.value & ScreenOrientation.LANDSCAPE_SECONDARY.value) != 0) {
+ screenOrientation = ScreenOrientation.LANDSCAPE_SECONDARY;
+ } else {
+ screenOrientation = ScreenOrientation.PORTRAIT_PRIMARY;
+ }
+ if (mScreenOrientation == screenOrientation) {
+ return false;
+ }
+ mScreenOrientation = screenOrientation;
+ Log.d(LOGTAG, "updating to new orientation " + mScreenOrientation);
+ notifyListeners(mScreenOrientation);
+ ScreenManagerHelper.refreshScreenInfo();
+ return true;
+ }
+
+ private void notifyListeners(final ScreenOrientation newOrientation) {
+ final Runnable notifier =
+ new Runnable() {
+ @Override
+ public void run() {
+ for (final OrientationChangeListener listener : mListeners) {
+ listener.onScreenOrientationChanged(newOrientation);
+ }
+ }
+ };
+
+ if (ThreadUtils.isOnUiThread()) {
+ notifier.run();
+ } else {
+ ThreadUtils.runOnUiThread(notifier);
+ }
+ }
+
+ /*
+ * @return The Gecko screen orientation derived from Android orientation and
+ * rotation.
+ */
+ public ScreenOrientation getScreenOrientation() {
+ return mScreenOrientation;
+ }
+
+ /*
+ * Combine the Android orientation and rotation to the Gecko orientation.
+ *
+ * @param aAndroidOrientation
+ * Android orientation from Configuration.orientation.
+ * @param aRotation
+ * Device rotation from Display.getRotation().
+ *
+ * @return Gecko screen orientation.
+ */
+ private ScreenOrientation getScreenOrientation(
+ final int aAndroidOrientation, final int aRotation) {
+ final boolean isPrimary = aRotation == Surface.ROTATION_0 || aRotation == Surface.ROTATION_90;
+ if (aAndroidOrientation == ORIENTATION_PORTRAIT) {
+ if (isPrimary) {
+ // Non-rotated portrait device or landscape device rotated
+ // to primary portrait mode counter-clockwise.
+ return ScreenOrientation.PORTRAIT_PRIMARY;
+ }
+ return ScreenOrientation.PORTRAIT_SECONDARY;
+ }
+ if (aAndroidOrientation == ORIENTATION_LANDSCAPE) {
+ if (isPrimary) {
+ // Non-rotated landscape device or portrait device rotated
+ // to primary landscape mode counter-clockwise.
+ return ScreenOrientation.LANDSCAPE_PRIMARY;
+ }
+ return ScreenOrientation.LANDSCAPE_SECONDARY;
+ }
+ return ScreenOrientation.NONE;
+ }
+
+ /*
+ * Get the Gecko orientation from Display.
+ *
+ * @param aDisplay The display that has orientation information.
+ *
+ * @return Gecko screen orientation.
+ */
+ private ScreenOrientation getScreenOrientation(final Display aDisplay) {
+ final Rect rect = GeckoAppShell.getScreenSizeIgnoreOverride();
+ final int orientation =
+ rect.width() >= rect.height() ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
+ return getScreenOrientation(orientation, aDisplay.getRotation());
+ }
+
+ /*
+ * @return Device rotation converted to an angle.
+ */
+ public short getAngle() {
+ switch (getRotation()) {
+ case Surface.ROTATION_0:
+ return 0;
+ case Surface.ROTATION_90:
+ return 90;
+ case Surface.ROTATION_180:
+ return 180;
+ case Surface.ROTATION_270:
+ return 270;
+ default:
+ Log.w(LOGTAG, "getAngle: unexpected rotation value");
+ return 0;
+ }
+ }
+
+ /*
+ * @return Device rotation.
+ */
+ private int getRotation() {
+ return GeckoAppShell.getRotation();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSystemStateListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSystemStateListener.java
new file mode 100644
index 0000000000..8b188438a4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoSystemStateListener.java
@@ -0,0 +1,195 @@
+/* -*- 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;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.database.ContentObserver;
+import android.hardware.input.InputManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.InputDevice;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.InputDeviceUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class GeckoSystemStateListener implements InputManager.InputDeviceListener {
+ private static final String LOGTAG = "SystemStateListener";
+
+ private static final GeckoSystemStateListener listenerInstance = new GeckoSystemStateListener();
+
+ private boolean mInitialized;
+ private ContentObserver mContentObserver;
+ private static Context sApplicationContext;
+ private InputManager mInputManager;
+ private boolean mIsNightMode;
+
+ public static GeckoSystemStateListener getInstance() {
+ return listenerInstance;
+ }
+
+ private GeckoSystemStateListener() {}
+
+ public synchronized void initialize(final Context context) {
+ if (mInitialized) {
+ Log.w(LOGTAG, "Already initialized!");
+ return;
+ }
+ mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
+ mInputManager.registerInputDeviceListener(listenerInstance, ThreadUtils.getUiHandler());
+
+ sApplicationContext = context;
+ final ContentResolver contentResolver = sApplicationContext.getContentResolver();
+ final Uri animationSetting = Settings.System.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE);
+ mContentObserver =
+ new ContentObserver(new Handler(Looper.getMainLooper())) {
+ @Override
+ public void onChange(final boolean selfChange) {
+ onDeviceChanged();
+ }
+ };
+ contentResolver.registerContentObserver(animationSetting, false, mContentObserver);
+
+ final Uri invertSetting =
+ Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED);
+ contentResolver.registerContentObserver(invertSetting, false, mContentObserver);
+
+ final Uri textContrastSetting =
+ Settings.Secure.getUriFor(
+ /*Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED*/ "high_text_contrast_enabled");
+ contentResolver.registerContentObserver(textContrastSetting, false, mContentObserver);
+
+ mIsNightMode =
+ (sApplicationContext.getResources().getConfiguration().uiMode
+ & Configuration.UI_MODE_NIGHT_MASK)
+ == Configuration.UI_MODE_NIGHT_YES;
+
+ mInitialized = true;
+ }
+
+ public synchronized void shutdown() {
+ if (!mInitialized) {
+ Log.w(LOGTAG, "Already shut down!");
+ return;
+ }
+
+ if (mInputManager == null) {
+ Log.e(LOGTAG, "mInputManager should be valid!");
+ return;
+ }
+
+ mInputManager.unregisterInputDeviceListener(listenerInstance);
+
+ final ContentResolver contentResolver = sApplicationContext.getContentResolver();
+ contentResolver.unregisterContentObserver(mContentObserver);
+
+ mInitialized = false;
+ mInputManager = null;
+ mContentObserver = null;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ /**
+ * For prefers-reduced-motion media queries feature.
+ *
+ * <p>Uses `Settings.Global` which was introduced in API version 17.
+ */
+ private static boolean prefersReducedMotion() {
+ final ContentResolver contentResolver = sApplicationContext.getContentResolver();
+
+ return Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1)
+ == 0.0f;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ /**
+ * For inverted-colors queries feature.
+ *
+ * <p>Uses `Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED` which was introduced in API
+ * version 21.
+ */
+ private static boolean isInvertedColors() {
+ final ContentResolver contentResolver = sApplicationContext.getContentResolver();
+
+ return Settings.Secure.getInt(
+ contentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0)
+ == 1;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ /**
+ * For prefers-contrast queries feature.
+ *
+ * <p>Uses `Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED` which was introduced in API
+ * version 21.
+ */
+ private static boolean prefersContrast() {
+ final ContentResolver contentResolver = sApplicationContext.getContentResolver();
+
+ return Settings.Secure.getInt(
+ contentResolver, /*Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED*/
+ "high_text_contrast_enabled",
+ 0)
+ == 1;
+ }
+
+ /** For prefers-color-scheme media queries feature. */
+ public boolean isNightMode() {
+ return mIsNightMode;
+ }
+
+ public void updateNightMode(final int newUIMode) {
+ final boolean isNightMode =
+ (newUIMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
+ if (isNightMode == mIsNightMode) {
+ return;
+ }
+ mIsNightMode = isNightMode;
+ onDeviceChanged();
+ }
+
+ @WrapForJNI(stubName = "OnDeviceChanged", calledFrom = "any", dispatchTo = "gecko")
+ private static native void nativeOnDeviceChanged();
+
+ public static void onDeviceChanged() {
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeOnDeviceChanged();
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY, GeckoSystemStateListener.class, "nativeOnDeviceChanged");
+ }
+ }
+
+ private void notifyDeviceChanged(final int deviceId) {
+ final InputDevice device = InputDevice.getDevice(deviceId);
+ if (device == null || !InputDeviceUtils.isPointerTypeDevice(device)) {
+ return;
+ }
+ onDeviceChanged();
+ }
+
+ @Override
+ public void onInputDeviceAdded(final int deviceId) {
+ notifyDeviceChanged(deviceId);
+ }
+
+ @Override
+ public void onInputDeviceRemoved(final int deviceId) {
+ // Call onDeviceChanged directly without checking device source types
+ // since we can no longer get a valid `InputDevice` in the case of
+ // device removal.
+ onDeviceChanged();
+ }
+
+ @Override
+ public void onInputDeviceChanged(final int deviceId) {
+ notifyDeviceChanged(deviceId);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
new file mode 100644
index 0000000000..f88421ad03
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
@@ -0,0 +1,967 @@
+/* -*- 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;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.gecko.process.GeckoProcessManager;
+import org.mozilla.gecko.process.GeckoProcessType;
+import org.mozilla.gecko.util.GeckoBundle;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.geckoview.BuildConfig;
+import org.mozilla.geckoview.GeckoResult;
+
+public class GeckoThread extends Thread {
+ private static final String LOGTAG = "GeckoThread";
+
+ public enum State implements NativeQueue.State {
+ // After being loaded by class loader.
+ @WrapForJNI
+ INITIAL(0),
+ // After launching Gecko thread
+ @WrapForJNI
+ LAUNCHED(1),
+ // After loading the mozglue library.
+ @WrapForJNI
+ MOZGLUE_READY(2),
+ // After loading the libxul library.
+ @WrapForJNI
+ LIBS_READY(3),
+ // After initializing nsAppShell and JNI calls.
+ @WrapForJNI
+ JNI_READY(4),
+ // After initializing profile and prefs.
+ @WrapForJNI
+ PROFILE_READY(5),
+ // After initializing frontend JS
+ @WrapForJNI
+ RUNNING(6),
+ // After granting request to shutdown
+ @WrapForJNI
+ EXITING(3),
+ // After granting request to restart
+ @WrapForJNI
+ RESTARTING(3),
+ // After failed lib extraction due to corrupted APK
+ CORRUPT_APK(2),
+ // After exiting GeckoThread (corresponding to "Gecko:Exited" event)
+ @WrapForJNI
+ EXITED(0);
+
+ /* The rank is an arbitrary value reflecting the amount of components or features
+ * that are available for use. During startup and up to the RUNNING state, the
+ * rank value increases because more components are initialized and available for
+ * use. During shutdown and up to the EXITED state, the rank value decreases as
+ * components are shut down and become unavailable. EXITING has the same rank as
+ * LIBS_READY because both states have a similar amount of components available.
+ */
+ private final int mRank;
+
+ State(final int rank) {
+ mRank = rank;
+ }
+
+ @Override
+ public boolean is(final NativeQueue.State other) {
+ return this == other;
+ }
+
+ @Override
+ public boolean isAtLeast(final NativeQueue.State other) {
+ if (other instanceof State) {
+ return mRank >= ((State) other).mRank;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return name();
+ }
+ }
+
+ // -1 denotes an invalid or missing File Descriptor
+ private static final int INVALID_FD = -1;
+
+ private static final NativeQueue sNativeQueue = new NativeQueue(State.INITIAL, State.RUNNING);
+
+ /* package */ static NativeQueue getNativeQueue() {
+ return sNativeQueue;
+ }
+
+ public static final State MIN_STATE = State.INITIAL;
+ public static final State MAX_STATE = State.EXITED;
+
+ private static final Runnable UI_THREAD_CALLBACK =
+ new Runnable() {
+ @Override
+ public void run() {
+ ThreadUtils.assertOnUiThread();
+ final long nextDelay = runUiThreadCallback();
+ if (nextDelay >= 0) {
+ ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
+ }
+ }
+ };
+
+ private static final GeckoThread INSTANCE = new GeckoThread();
+
+ @WrapForJNI private static final ClassLoader clsLoader = GeckoThread.class.getClassLoader();
+ @WrapForJNI private static MessageQueue msgQueue;
+ @WrapForJNI private static int uiThreadId;
+
+ private static TelemetryUtils.Timer sInitTimer;
+ private static LinkedList<StateGeckoResult> sStateListeners = new LinkedList<>();
+
+ // Main process parameters
+ public static final int FLAG_DEBUGGING = 1 << 0; // Debugging mode.
+ public static final int FLAG_PRELOAD_CHILD = 1 << 1; // Preload child during main thread start.
+ public static final int FLAG_ENABLE_NATIVE_CRASHREPORTER =
+ 1 << 2; // Enable native crash reporting.
+
+ /* package */ static final String EXTRA_ARGS = "args";
+
+ private boolean mInitialized;
+ private InitInfo mInitInfo;
+
+ public static final class ParcelFileDescriptors {
+ public final @Nullable ParcelFileDescriptor prefs;
+ public final @Nullable ParcelFileDescriptor prefMap;
+ public final @NonNull ParcelFileDescriptor ipc;
+ public final @Nullable ParcelFileDescriptor crashReporter;
+
+ private ParcelFileDescriptors(final Builder builder) {
+ prefs = builder.prefs;
+ prefMap = builder.prefMap;
+ ipc = builder.ipc;
+ crashReporter = builder.crashReporter;
+ }
+
+ public FileDescriptors detach() {
+ return FileDescriptors.builder()
+ .prefs(detach(prefs))
+ .prefMap(detach(prefMap))
+ .ipc(detach(ipc))
+ .crashReporter(detach(crashReporter))
+ .build();
+ }
+
+ private static int detach(final ParcelFileDescriptor pfd) {
+ if (pfd == null) {
+ return INVALID_FD;
+ }
+ return pfd.detachFd();
+ }
+
+ public void close() {
+ close(prefs, prefMap, ipc, crashReporter);
+ }
+
+ private static void close(final ParcelFileDescriptor... pfds) {
+ for (final ParcelFileDescriptor pfd : pfds) {
+ if (pfd != null) {
+ try {
+ pfd.close();
+ } catch (final IOException ex) {
+ // Nothing we can do about this really.
+ Log.w(LOGTAG, "Failed to close File Descriptors.", ex);
+ }
+ }
+ }
+ }
+
+ public static ParcelFileDescriptors from(final FileDescriptors fds) {
+ return ParcelFileDescriptors.builder()
+ .prefs(from(fds.prefs))
+ .prefMap(from(fds.prefMap))
+ .ipc(from(fds.ipc))
+ .crashReporter(from(fds.crashReporter))
+ .build();
+ }
+
+ private static ParcelFileDescriptor from(final int fd) {
+ if (fd == INVALID_FD) {
+ return null;
+ }
+ try {
+ return ParcelFileDescriptor.fromFd(fd);
+ } catch (final IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ ParcelFileDescriptor prefs;
+ ParcelFileDescriptor prefMap;
+ ParcelFileDescriptor ipc;
+ ParcelFileDescriptor crashReporter;
+
+ private Builder() {}
+
+ public ParcelFileDescriptors build() {
+ return new ParcelFileDescriptors(this);
+ }
+
+ public Builder prefs(final ParcelFileDescriptor prefs) {
+ this.prefs = prefs;
+ return this;
+ }
+
+ public Builder prefMap(final ParcelFileDescriptor prefMap) {
+ this.prefMap = prefMap;
+ return this;
+ }
+
+ public Builder ipc(final ParcelFileDescriptor ipc) {
+ this.ipc = ipc;
+ return this;
+ }
+
+ public Builder crashReporter(final ParcelFileDescriptor crashReporter) {
+ this.crashReporter = crashReporter;
+ return this;
+ }
+ }
+ }
+
+ public static final class FileDescriptors {
+ final int prefs;
+ final int prefMap;
+ final int ipc;
+ final int crashReporter;
+
+ private FileDescriptors(final Builder builder) {
+ prefs = builder.prefs;
+ prefMap = builder.prefMap;
+ ipc = builder.ipc;
+ crashReporter = builder.crashReporter;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ int prefs = INVALID_FD;
+ int prefMap = INVALID_FD;
+ int ipc = INVALID_FD;
+ int crashReporter = INVALID_FD;
+
+ private Builder() {}
+
+ public FileDescriptors build() {
+ return new FileDescriptors(this);
+ }
+
+ public Builder prefs(final int prefs) {
+ this.prefs = prefs;
+ return this;
+ }
+
+ public Builder prefMap(final int prefMap) {
+ this.prefMap = prefMap;
+ return this;
+ }
+
+ public Builder ipc(final int ipc) {
+ this.ipc = ipc;
+ return this;
+ }
+
+ public Builder crashReporter(final int crashReporter) {
+ this.crashReporter = crashReporter;
+ return this;
+ }
+ }
+ }
+
+ public static class InitInfo {
+ public final String[] args;
+ public final Bundle extras;
+ public final int flags;
+ public final Map<String, Object> prefs;
+ public final String userSerialNumber;
+
+ public final boolean xpcshell;
+ public final String outFilePath;
+
+ public final FileDescriptors fds;
+
+ private InitInfo(final Builder builder) {
+ final List<String> result = new ArrayList<>(builder.mArgs.length);
+
+ boolean xpcshell = false;
+ for (final String argument : builder.mArgs) {
+ if ("-xpcshell".equals(argument)) {
+ xpcshell = true;
+ } else {
+ result.add(argument);
+ }
+ }
+ this.xpcshell = xpcshell;
+
+ args = result.toArray(new String[0]);
+
+ extras = builder.mExtras != null ? new Bundle(builder.mExtras) : new Bundle(3);
+ flags = builder.mFlags;
+ prefs = builder.mPrefs;
+ userSerialNumber = builder.mUserSerialNumber;
+
+ outFilePath = xpcshell ? builder.mOutFilePath : null;
+
+ if (builder.mFds != null) {
+ fds = builder.mFds;
+ } else {
+ fds = FileDescriptors.builder().build();
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String[] mArgs;
+ private Bundle mExtras;
+ private int mFlags;
+ private Map<String, Object> mPrefs;
+ private String mUserSerialNumber;
+
+ private String mOutFilePath;
+
+ private FileDescriptors mFds;
+
+ // Prevent direct instantiation
+ private Builder() {}
+
+ public InitInfo build() {
+ return new InitInfo(this);
+ }
+
+ public Builder args(final String[] args) {
+ mArgs = args;
+ return this;
+ }
+
+ public Builder extras(final Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ public Builder flags(final int flags) {
+ mFlags = flags;
+ return this;
+ }
+
+ public Builder prefs(final Map<String, Object> prefs) {
+ mPrefs = prefs;
+ return this;
+ }
+
+ public Builder userSerialNumber(final String userSerialNumber) {
+ mUserSerialNumber = userSerialNumber;
+ return this;
+ }
+
+ public Builder outFilePath(final String outFilePath) {
+ mOutFilePath = outFilePath;
+ return this;
+ }
+
+ public Builder fds(final FileDescriptors fds) {
+ mFds = fds;
+ return this;
+ }
+ }
+ }
+
+ private static class StateGeckoResult extends GeckoResult<Void> {
+ final State state;
+
+ public StateGeckoResult(final State state) {
+ this.state = state;
+ }
+ }
+
+ GeckoThread() {
+ // Request more (virtual) stack space to avoid overflows in the CSS frame
+ // constructor. 8 MB matches desktop.
+ super(null, null, "Gecko", 8 * 1024 * 1024);
+ }
+
+ @WrapForJNI
+ private static boolean isChildProcess() {
+ final InitInfo info = INSTANCE.mInitInfo;
+ return info != null && info.fds.ipc != INVALID_FD;
+ }
+
+ public static boolean init(final InitInfo info) {
+ return INSTANCE.initInternal(info);
+ }
+
+ private synchronized boolean initInternal(final InitInfo info) {
+ ThreadUtils.assertOnUiThread();
+ uiThreadId = Process.myTid();
+
+ if (mInitialized) {
+ return false;
+ }
+
+ sInitTimer = new TelemetryUtils.UptimeTimer("GV_STARTUP_RUNTIME_MS");
+
+ mInitInfo = info;
+ mInitialized = true;
+ notifyAll();
+ return true;
+ }
+
+ public static boolean launch() {
+ ThreadUtils.assertOnUiThread();
+
+ if (checkAndSetState(State.INITIAL, State.LAUNCHED)) {
+ INSTANCE.start();
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isLaunched() {
+ return !isState(State.INITIAL);
+ }
+
+ @RobocopTarget
+ public static boolean isRunning() {
+ return isState(State.RUNNING);
+ }
+
+ private static void loadGeckoLibs(final Context context) {
+ GeckoLoader.loadSQLiteLibs(context);
+ GeckoLoader.loadNSSLibs(context);
+ GeckoLoader.loadGeckoLibs(context);
+ setState(State.LIBS_READY);
+ }
+
+ private static void initGeckoEnvironment() {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final Locale locale = Locale.getDefault();
+ final Resources res = context.getResources();
+ if (locale.toString().equalsIgnoreCase("zh_hk")) {
+ final Locale mappedLocale = Locale.TRADITIONAL_CHINESE;
+ Locale.setDefault(mappedLocale);
+ final Configuration config = res.getConfiguration();
+ config.locale = mappedLocale;
+ res.updateConfiguration(config, null);
+ }
+
+ if (!isChildProcess()) {
+ GeckoSystemStateListener.getInstance().initialize(context);
+ }
+
+ loadGeckoLibs(context);
+ }
+
+ private String[] getMainProcessArgs() {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final ArrayList<String> args = new ArrayList<>();
+
+ // argv[0] is the program name, which for us is the package name.
+ args.add(context.getPackageName());
+
+ if (!mInitInfo.xpcshell) {
+ args.add("-greomni");
+ args.add(context.getPackageResourcePath());
+ }
+
+ if (mInitInfo.args != null) {
+ args.addAll(Arrays.asList(mInitInfo.args));
+ }
+
+ // Legacy "args" parameter
+ final String extraArgs = mInitInfo.extras.getString(EXTRA_ARGS, null);
+ if (extraArgs != null) {
+ final StringTokenizer st = new StringTokenizer(extraArgs);
+ while (st.hasMoreTokens()) {
+ args.add(st.nextToken());
+ }
+ }
+
+ // "argX" parameters
+ for (int i = 0; mInitInfo.extras.containsKey("arg" + i); i++) {
+ final String arg = mInitInfo.extras.getString("arg" + i);
+ args.add(arg);
+ }
+
+ return args.toArray(new String[0]);
+ }
+
+ public static @Nullable Bundle getActiveExtras() {
+ synchronized (INSTANCE) {
+ if (!INSTANCE.mInitialized) {
+ return null;
+ }
+ return new Bundle(INSTANCE.mInitInfo.extras);
+ }
+ }
+
+ public static int getActiveFlags() {
+ synchronized (INSTANCE) {
+ if (!INSTANCE.mInitialized) {
+ return 0;
+ }
+
+ return INSTANCE.mInitInfo.flags;
+ }
+ }
+
+ private static ArrayList<String> getEnvFromExtras(final Bundle extras) {
+ if (extras == null) {
+ return new ArrayList<>();
+ }
+
+ final ArrayList<String> result = new ArrayList<>();
+ if (extras != null) {
+ String env = extras.getString("env0");
+ for (int c = 1; env != null; c++) {
+ if (BuildConfig.DEBUG_BUILD) {
+ Log.d(LOGTAG, "env var: " + env);
+ }
+ result.add(env);
+ env = extras.getString("env" + c);
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public void run() {
+ Log.i(LOGTAG, "preparing to run Gecko");
+
+ Looper.prepare();
+ GeckoThread.msgQueue = Looper.myQueue();
+ ThreadUtils.sGeckoThread = this;
+ ThreadUtils.sGeckoHandler = new Handler();
+
+ // Preparation for pumpMessageLoop()
+ final MessageQueue.IdleHandler idleHandler =
+ new MessageQueue.IdleHandler() {
+ @Override
+ public boolean queueIdle() {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+ final Message idleMsg = Message.obtain(geckoHandler);
+ // Use |Message.obj == GeckoHandler| to identify our "queue is empty" message
+ idleMsg.obj = geckoHandler;
+ geckoHandler.sendMessageAtFrontOfQueue(idleMsg);
+ // Keep this IdleHandler
+ return true;
+ }
+ };
+ Looper.myQueue().addIdleHandler(idleHandler);
+
+ // Wait until initialization before preparing environment.
+ synchronized (this) {
+ while (!mInitialized) {
+ try {
+ wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+ final List<String> env = getEnvFromExtras(mInitInfo.extras);
+
+ // In Gecko, the native crash reporter is enabled by default in opt builds, and
+ // disabled by default in debug builds.
+ if ((mInitInfo.flags & FLAG_ENABLE_NATIVE_CRASHREPORTER) == 0 && !BuildConfig.DEBUG_BUILD) {
+ env.add(0, "MOZ_CRASHREPORTER_DISABLE=1");
+ } else if ((mInitInfo.flags & FLAG_ENABLE_NATIVE_CRASHREPORTER) != 0
+ && BuildConfig.DEBUG_BUILD) {
+ env.add(0, "MOZ_CRASHREPORTER=1");
+ }
+
+ if (mInitInfo.userSerialNumber != null) {
+ env.add(0, "MOZ_ANDROID_USER_SERIAL_NUMBER=" + mInitInfo.userSerialNumber);
+ }
+
+ // Start the profiler before even loading mozglue, so we can capture more
+ // things that are happening on the JVM side.
+ maybeStartGeckoProfiler(env);
+
+ GeckoLoader.loadMozGlue(context);
+ setState(State.MOZGLUE_READY);
+
+ final boolean isChildProcess = isChildProcess();
+
+ GeckoLoader.setupGeckoEnvironment(
+ context,
+ isChildProcess,
+ context.getFilesDir().getPath(),
+ env,
+ mInitInfo.prefs,
+ mInitInfo.xpcshell);
+
+ initGeckoEnvironment();
+
+ if ((mInitInfo.flags & FLAG_PRELOAD_CHILD) != 0) {
+ // Preload the content ("tab") child process.
+ GeckoProcessManager.getInstance().preload(GeckoProcessType.CONTENT);
+ }
+
+ if ((mInitInfo.flags & FLAG_DEBUGGING) != 0) {
+ try {
+ Thread.sleep(5 * 1000 /* 5 seconds */);
+ } catch (final InterruptedException e) {
+ }
+ }
+
+ Log.w(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() + " - runGecko");
+
+ final String[] args = isChildProcess ? mInitInfo.args : getMainProcessArgs();
+
+ if ((mInitInfo.flags & FLAG_DEBUGGING) != 0) {
+ Log.i(LOGTAG, "RunGecko - args = " + TextUtils.join(" ", args));
+ }
+
+ // And go.
+ GeckoLoader.nativeRun(
+ args,
+ mInitInfo.fds.prefs,
+ mInitInfo.fds.prefMap,
+ mInitInfo.fds.ipc,
+ mInitInfo.fds.crashReporter,
+ !isChildProcess && mInitInfo.xpcshell,
+ isChildProcess ? null : mInitInfo.outFilePath);
+
+ // And... we're done.
+ final boolean restarting = isState(State.RESTARTING);
+ setState(State.EXITED);
+
+ final GeckoBundle data = new GeckoBundle(1);
+ data.putBoolean("restart", restarting);
+ EventDispatcher.getInstance().dispatch("Gecko:Exited", data);
+
+ // Remove pumpMessageLoop() idle handler
+ Looper.myQueue().removeIdleHandler(idleHandler);
+
+ if (isChildProcess) {
+ // The child process is completely controlled by Gecko so we don't really need to keep
+ // it alive after Gecko exits.
+ System.exit(0);
+ }
+ }
+
+ // This may start the gecko profiler early by looking at the environment variables.
+ // Refer to the platform side for more information about the environment variables:
+ // https://searchfox.org/mozilla-central/rev/2f9eacd9d3d995c937b4251a5557d95d494c9be1/tools/profiler/core/platform.cpp#2969-3072
+ private static void maybeStartGeckoProfiler(final @NonNull List<String> env) {
+ final String startupEnv = "MOZ_PROFILER_STARTUP=";
+ final String intervalEnv = "MOZ_PROFILER_STARTUP_INTERVAL=";
+ final String capacityEnv = "MOZ_PROFILER_STARTUP_ENTRIES=";
+ final String filtersEnv = "MOZ_PROFILER_STARTUP_FILTERS=";
+ boolean isStartupProfiling = false;
+ // Putting default values for now, but they can be overwritten.
+ // Keep these values in sync with profiler defaults.
+ int interval = 1;
+
+ // The default capacity value is the same with the min capacity, but users
+ // can still enter a different capacity. We also keep this variable to make
+ // sure that the entered value is not below the min capacity.
+ // This value is kept in `scMinimumBufferEntries` variable in the cpp side:
+ // https://searchfox.org/mozilla-central/rev/fa7f47027917a186fb2052dee104cd06c21dd76f/tools/profiler/core/platform.cpp#749
+ // This number represents 128MiB in entry size.
+ // This is calculated as:
+ // 128 * 1024 * 1024 / 8 = 16777216
+ final int minCapacity = 16777216;
+
+ // ~16M entries which is 128MiB in entry size.
+ // Keep this in sync with `PROFILER_DEFAULT_STARTUP_ENTRIES`.
+ // It's computed as 16 * 1024 * 1024 there, which is the same number.
+ int capacity = minCapacity;
+
+ // Set the default value of no filters - an empty array - which is safer than using null.
+ // If we find a user provided value, this will be overwritten.
+ String[] filters = new String[0];
+
+ // Looping the environment variable list to check known variable names.
+ for (final String envItem : env) {
+ if (envItem == null) {
+ continue;
+ }
+
+ if (envItem.startsWith(startupEnv)) {
+ // Check the environment variable value to see if it's positive.
+ final String value = envItem.substring(startupEnv.length());
+ if (value.isEmpty() || value.equals("0") || value.equals("n") || value.equals("N")) {
+ // ''/'0'/'n'/'N' values mean do not start the startup profiler.
+ // There's no need to inspect other environment variables,
+ // so let's break out of the loop
+ break;
+ }
+
+ isStartupProfiling = true;
+ } else if (envItem.startsWith(intervalEnv)) {
+ // Parse the interval environment variable if present
+ final String value = envItem.substring(intervalEnv.length());
+
+ try {
+ final int intValue = Integer.parseInt(value);
+ interval = Math.max(intValue, interval);
+ } catch (final NumberFormatException err) {
+ // Failed to parse. Do nothing and just use the default value.
+ }
+ } else if (envItem.startsWith(capacityEnv)) {
+ // Parse the capacity environment variable if present
+ final String value = envItem.substring(capacityEnv.length());
+
+ try {
+ final int intValue = Integer.parseInt(value);
+ // See `scMinimumBufferEntries` variable for this value on the platform side.
+ capacity = Math.max(intValue, minCapacity);
+ } catch (final NumberFormatException err) {
+ // Failed to parse. Do nothing and just use the default value.
+ }
+ } else if (envItem.startsWith(filtersEnv)) {
+ filters = envItem.substring(filtersEnv.length()).split(",");
+ }
+ }
+
+ if (isStartupProfiling) {
+ GeckoJavaSampler.start(filters, interval, capacity);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean pumpMessageLoop(final Message msg) {
+ final Handler geckoHandler = ThreadUtils.sGeckoHandler;
+
+ if (msg.obj == geckoHandler && msg.getTarget() == geckoHandler) {
+ // Our "queue is empty" message; see runGecko()
+ return false;
+ }
+
+ if (msg.getTarget() == null) {
+ Looper.myLooper().quit();
+ } else {
+ msg.getTarget().dispatchMessage(msg);
+ }
+
+ return true;
+ }
+
+ /**
+ * Check that the current Gecko thread state matches the given state.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isState(final State state) {
+ return sNativeQueue.getState().is(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or further along, according to
+ * the order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtLeast(final State state) {
+ return sNativeQueue.getState().isAtLeast(state);
+ }
+
+ /**
+ * Check that the current Gecko thread state is at the given state or prior, according to the
+ * order defined in the State enum.
+ *
+ * @param state State to check
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateAtMost(final State state) {
+ return state.isAtLeast(sNativeQueue.getState());
+ }
+
+ /**
+ * Check that the current Gecko thread state falls into an inclusive range of states, according to
+ * the order defined in the State enum.
+ *
+ * @param minState Lower range of allowable states
+ * @param maxState Upper range of allowable states
+ * @return True if the current Gecko thread state matches
+ */
+ public static boolean isStateBetween(final State minState, final State maxState) {
+ return isStateAtLeast(minState) && isStateAtMost(maxState);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setState(final State newState) {
+ checkAndSetState(null, newState);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static boolean checkAndSetState(final State expectedState, final State newState) {
+ final boolean result = sNativeQueue.checkAndSetState(expectedState, newState);
+ if (result) {
+ Log.d(LOGTAG, "State changed to " + newState);
+
+ if (sInitTimer != null && isRunning()) {
+ sInitTimer.stop();
+ sInitTimer = null;
+ }
+
+ notifyStateListeners();
+ }
+ return result;
+ }
+
+ @WrapForJNI(stubName = "SpeculativeConnect")
+ private static native void speculativeConnectNative(String uri);
+
+ public static void speculativeConnect(final String uri) {
+ // This is almost always called before Gecko loads, so we don't
+ // bother checking here if Gecko is actually loaded or not.
+ // Speculative connection depends on proxy settings,
+ // so the earliest it can happen is after profile is ready.
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class, "speculativeConnectNative", uri);
+ }
+
+ @UiThread
+ public static GeckoResult<Void> waitForState(final State state) {
+ final StateGeckoResult result = new StateGeckoResult(state);
+ if (isStateAtLeast(state)) {
+ result.complete(null);
+ return result;
+ }
+
+ synchronized (sStateListeners) {
+ sStateListeners.add(result);
+ }
+ return result;
+ }
+
+ private static void notifyStateListeners() {
+ synchronized (sStateListeners) {
+ final LinkedList<StateGeckoResult> newListeners = new LinkedList<>();
+ for (final StateGeckoResult result : sStateListeners) {
+ if (!isStateAtLeast(result.state)) {
+ newListeners.add(result);
+ continue;
+ }
+
+ result.complete(null);
+ }
+
+ sStateListeners = newListeners;
+ }
+ }
+
+ @WrapForJNI(stubName = "OnPause", dispatchTo = "gecko")
+ private static native void nativeOnPause();
+
+ public static void onPause() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnPause();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class, "nativeOnPause");
+ }
+ }
+
+ @WrapForJNI(stubName = "OnResume", dispatchTo = "gecko")
+ private static native void nativeOnResume();
+
+ public static void onResume() {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeOnResume();
+ } else {
+ queueNativeCallUntil(State.PROFILE_READY, GeckoThread.class, "nativeOnResume");
+ }
+ }
+
+ @WrapForJNI(stubName = "CreateServices", dispatchTo = "gecko")
+ private static native void nativeCreateServices(String category, String data);
+
+ public static void createServices(final String category, final String data) {
+ if (isStateAtLeast(State.PROFILE_READY)) {
+ nativeCreateServices(category, data);
+ } else {
+ queueNativeCallUntil(
+ State.PROFILE_READY,
+ GeckoThread.class,
+ "nativeCreateServices",
+ String.class,
+ category,
+ String.class,
+ data);
+ }
+ }
+
+ @WrapForJNI(calledFrom = "ui")
+ /* package */ static native long runUiThreadCallback();
+
+ @WrapForJNI(dispatchTo = "gecko")
+ public static native void forceQuit();
+
+ @WrapForJNI(dispatchTo = "gecko")
+ public static native void crash();
+
+ @WrapForJNI
+ private static void requestUiThreadCallback(final long delay) {
+ ThreadUtils.getUiHandler().postDelayed(UI_THREAD_CALLBACK, delay);
+ }
+
+ /** Queue a call to the given static method until Gecko is in the RUNNING state. */
+ public static void queueNativeCall(
+ final Class<?> cls, final String methodName, final Object... args) {
+ sNativeQueue.queueUntilReady(cls, methodName, args);
+ }
+
+ /** Queue a call to the given instance method until Gecko is in the RUNNING state. */
+ public static void queueNativeCall(
+ final Object obj, final String methodName, final Object... args) {
+ sNativeQueue.queueUntilReady(obj, methodName, args);
+ }
+
+ /** Queue a call to the given instance method until Gecko is in the RUNNING state. */
+ public static void queueNativeCallUntil(
+ final State state, final Object obj, final String methodName, final Object... args) {
+ sNativeQueue.queueUntil(state, obj, methodName, args);
+ }
+
+ /** Queue a call to the given static method until Gecko is in the RUNNING state. */
+ public static void queueNativeCallUntil(
+ final State state, final Class<?> cls, final String methodName, final Object... args) {
+ sNativeQueue.queueUntil(state, cls, methodName, args);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java
new file mode 100644
index 0000000000..120098a931
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/InputMethods.java
@@ -0,0 +1,104 @@
+/* -*- 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;
+
+import android.content.Context;
+import android.provider.Settings.Secure;
+import android.view.View;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import java.util.Collection;
+
+public final class InputMethods {
+ public static final String METHOD_ANDROID_LATINIME = "com.android.inputmethod.latin/.LatinIME";
+ // ATOK has a lot of package names since they release custom versions.
+ public static final String METHOD_ATOK_PREFIX = "com.justsystems.atokmobile";
+ public static final String METHOD_ATOK_OEM_PREFIX = "com.atok.mobile.";
+ public static final String METHOD_GOOGLE_JAPANESE_INPUT =
+ "com.google.android.inputmethod.japanese/.MozcService";
+ public static final String METHOD_ATOK_OEM_SOFTBANK =
+ "com.mobiroo.n.justsystems.atok/.AtokInputMethodService";
+ public static final String METHOD_GOOGLE_LATINIME =
+ "com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME";
+ public static final String METHOD_HTC_TOUCH_INPUT = "com.htc.android.htcime/.HTCIMEService";
+ public static final String METHOD_IWNN =
+ "jp.co.omronsoft.iwnnime.ml/.standardcommon.IWnnLanguageSwitcher";
+ public static final String METHOD_OPENWNN_PLUS = "com.owplus.ime.openwnnplus/.OpenWnnJAJP";
+ public static final String METHOD_SAMSUNG = "com.sec.android.inputmethod/.SamsungKeypad";
+ public static final String METHOD_SIMEJI = "com.adamrocker.android.input.simeji/.OpenWnnSimeji";
+ public static final String METHOD_SONY =
+ "com.sonyericsson.textinput.uxp/.glue.InputMethodServiceGlue";
+ public static final String METHOD_SWIFTKEY =
+ "com.touchtype.swiftkey/com.touchtype.KeyboardService";
+ public static final String METHOD_SWYPE = "com.swype.android.inputmethod/.SwypeInputMethod";
+ public static final String METHOD_SWYPE_BETA = "com.nuance.swype.input/.IME";
+ public static final String METHOD_TOUCHPAL_KEYBOARD =
+ "com.cootek.smartinputv5/com.cootek.smartinput5.TouchPalIME";
+
+ private InputMethods() {}
+
+ public static String getCurrentInputMethod(final Context context) {
+ final String inputMethod =
+ Secure.getString(context.getContentResolver(), Secure.DEFAULT_INPUT_METHOD);
+ return (inputMethod != null ? inputMethod : "");
+ }
+
+ public static InputMethodInfo getInputMethodInfo(
+ final Context context, final String inputMethod) {
+ final InputMethodManager imm = getInputMethodManager(context);
+ final Collection<InputMethodInfo> infos = imm.getEnabledInputMethodList();
+ for (final InputMethodInfo info : infos) {
+ if (info.getId().equals(inputMethod)) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ public static InputMethodManager getInputMethodManager(final Context context) {
+ return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ }
+
+ public static void restartInput(final Context context, final View view) {
+ final InputMethodManager imm = getInputMethodManager(context);
+ if (imm != null) {
+ imm.restartInput(view);
+ }
+ }
+
+ public static boolean needsSoftResetWorkaround(final String inputMethod) {
+ // Stock latin IME on Android 4.2 and above
+ return (METHOD_ANDROID_LATINIME.equals(inputMethod)
+ || METHOD_GOOGLE_LATINIME.equals(inputMethod));
+ }
+
+ /**
+ * Check input method if we require a workaround to remove composition in {@link
+ * android.view.inputmethod.InputMethodManager.updateSelection}.
+ *
+ * @param inputMethod The input method name by {@link #getCurrentInputMethod}.
+ * @return true if {@link android.view.inputmethod.InputMethodManager.updateSelection} doesn't
+ * remove the composition, use {@link
+ * android.view.inputmethod.InputMehtodManager.restartInput} to remove it in this case.
+ */
+ public static boolean needsRestartInput(final String inputMethod) {
+ return inputMethod.startsWith(METHOD_ATOK_PREFIX)
+ || inputMethod.startsWith(METHOD_ATOK_OEM_PREFIX)
+ || METHOD_ATOK_OEM_SOFTBANK.equals(inputMethod);
+ }
+
+ public static boolean shouldCommitCharAsKey(final String inputMethod) {
+ return METHOD_HTC_TOUCH_INPUT.equals(inputMethod);
+ }
+
+ public static boolean needsRestartOnReplaceRemove(final Context context) {
+ final String inputMethod = getCurrentInputMethod(context);
+ return METHOD_SONY.equals(inputMethod);
+ }
+
+ // TODO: Replace usages by definition in EditorInfoCompat once available (bug 1385726).
+ public static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000;
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/MagnifiableSurfaceView.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/MagnifiableSurfaceView.java
new file mode 100644
index 0000000000..2003abcc6f
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/MagnifiableSurfaceView.java
@@ -0,0 +1,137 @@
+/* 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;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+/**
+ * A {@link android.view.SurfaceView} which allows a {@link android.widget.Magnifier} widget to
+ * magnify a custom {@link android.view.Surface} rather than the SurfaceView's default Surface.
+ */
+public class MagnifiableSurfaceView extends SurfaceView {
+ private static final String LOGTAG = "MagnifiableSurfaceView";
+
+ private SurfaceHolderWrapper mHolder;
+
+ public MagnifiableSurfaceView(final Context context) {
+ super(context);
+ }
+
+ @Override
+ public SurfaceHolder getHolder() {
+ if (mHolder != null) {
+ // Only return our custom holder if we are being called from the Magnifier class.
+ // Throwable.getStackTrace() is faster than Thread.getStackTrace(), but still has a cost,
+ // hence why we only check the caller if we have set an override Surface.
+ final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
+ if (stackTrace.length >= 2
+ && stackTrace[1].getClassName().equals("android.widget.Magnifier")) {
+ return mHolder;
+ }
+ }
+ return super.getHolder();
+ }
+
+ /**
+ * Sets the Surface that should be magnified by a Magnifier widget.
+ *
+ * <p>This should be set immediately before calling {@link android.widget.Magnifier#show()} or
+ * {@link android.widget.Magnifier#update()}, and unset immediately afterwards.
+ *
+ * @param surface The Surface to be magnified. If null, the SurfaceView's default Surface will be
+ * used.
+ */
+ public void setMagnifierSurface(final Surface surface) {
+ if (surface != null) {
+ mHolder = new SurfaceHolderWrapper(getHolder(), surface);
+ } else {
+ mHolder = null;
+ }
+ }
+
+ /**
+ * A {@link android.view.SurfaceHolder} implementation that simply forwards all methods to a
+ * provided SurfaceHolder instance, except for getSurface() which returns a custom Surface.
+ */
+ private class SurfaceHolderWrapper implements SurfaceHolder {
+ private final SurfaceHolder mHolder;
+ private final Surface mSurface;
+
+ public SurfaceHolderWrapper(final SurfaceHolder holder, final Surface surface) {
+ mHolder = holder;
+ mSurface = surface;
+ }
+
+ @Override
+ public void addCallback(final Callback callback) {
+ mHolder.addCallback(callback);
+ }
+
+ @Override
+ public void removeCallback(final Callback callback) {
+ mHolder.removeCallback(callback);
+ }
+
+ @Override
+ public boolean isCreating() {
+ return mHolder.isCreating();
+ }
+
+ @Override
+ public void setType(final int type) {
+ mHolder.setType(type);
+ }
+
+ @Override
+ public void setFixedSize(final int width, final int height) {
+ mHolder.setFixedSize(width, height);
+ }
+
+ @Override
+ public void setSizeFromLayout() {
+ mHolder.setSizeFromLayout();
+ }
+
+ @Override
+ public void setFormat(final int format) {
+ mHolder.setFormat(format);
+ }
+
+ @Override
+ public void setKeepScreenOn(final boolean screenOn) {
+ mHolder.setKeepScreenOn(screenOn);
+ }
+
+ @Override
+ public Canvas lockCanvas() {
+ return mHolder.lockCanvas();
+ }
+
+ @Override
+ public Canvas lockCanvas(final Rect dirty) {
+ return mHolder.lockCanvas(dirty);
+ }
+
+ @Override
+ public void unlockCanvasAndPost(final Canvas canvas) {
+ mHolder.unlockCanvasAndPost(canvas);
+ }
+
+ @Override
+ public Rect getSurfaceFrame() {
+ return mHolder.getSurfaceFrame();
+ }
+
+ @Override
+ public Surface getSurface() {
+ return mSurface;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/MultiMap.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/MultiMap.java
new file mode 100644
index 0000000000..ff26d99dea
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/MultiMap.java
@@ -0,0 +1,186 @@
+/* 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;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Defines a map that holds a collection of values against each key.
+ *
+ * @param <K> Key type
+ * @param <T> Value type
+ */
+public class MultiMap<K, T> {
+ private HashMap<K, List<T>> mMap;
+ private final List<T> mEmptyList = Collections.unmodifiableList(new ArrayList<>());
+
+ /**
+ * Creates a MultiMap with specified initial capacity.
+ *
+ * @param count Initial capacity
+ */
+ public MultiMap(final int count) {
+ mMap = new HashMap<>(count);
+ }
+
+ /** Creates a MultiMap with default initial capacity. */
+ public MultiMap() {
+ mMap = new HashMap<>();
+ }
+
+ private void ensure(final K key) {
+ if (!mMap.containsKey(key)) {
+ mMap.put(key, new ArrayList<>());
+ }
+ }
+
+ /**
+ * @return A map of key to the list of values associated to it
+ */
+ public Map<K, List<T>> asMap() {
+ return mMap;
+ }
+
+ /**
+ * @return The number of keys present in this map
+ */
+ public int size() {
+ return mMap.size();
+ }
+
+ /**
+ * @return whether this map is empty or not
+ */
+ public boolean isEmpty() {
+ return mMap.isEmpty();
+ }
+
+ /**
+ * Checks if a key is present in this map.
+ *
+ * @param key the key to check
+ * @return True if the map contains this key, false otherwise.
+ */
+ public boolean containsKey(final @Nullable K key) {
+ return mMap.containsKey(key);
+ }
+
+ /**
+ * Checks if a (key, value) pair is present in this map.
+ *
+ * @param key the key to check
+ * @param value the value to check
+ * @return true if there is a value associated to the given key, false otherwise
+ */
+ public boolean containsEntry(final @Nullable K key, final @Nullable T value) {
+ if (!mMap.containsKey(key)) {
+ return false;
+ }
+
+ return mMap.get(key).contains(value);
+ }
+
+ /**
+ * Gets the values associated with the given key.
+ *
+ * @param key the key to check
+ * @return the list of values associated with keys, an empty list if no values are associated with
+ * key.
+ */
+ @NonNull
+ public List<T> get(final @Nullable K key) {
+ if (!mMap.containsKey(key)) {
+ return mEmptyList;
+ }
+
+ return mMap.get(key);
+ }
+
+ /**
+ * Add a (key, value) mapping to this map.
+ *
+ * @param key the key to add
+ * @param value the value to add
+ */
+ @Nullable
+ public void add(final @NonNull K key, final @NonNull T value) {
+ ensure(key);
+ mMap.get(key).add(value);
+ }
+
+ /**
+ * Add a list of values to the given key.
+ *
+ * @param key the key to add
+ * @param values the list of values to add
+ * @return the final list of values or null if no value was added
+ */
+ @Nullable
+ public List<T> addAll(final @NonNull K key, final @NonNull List<T> values) {
+ if (values == null || values.isEmpty()) {
+ return null;
+ }
+
+ ensure(key);
+
+ final List<T> result = mMap.get(key);
+ result.addAll(values);
+ return result;
+ }
+
+ /**
+ * Remove all mappings for the given key.
+ *
+ * @param key the key
+ * @return values associated with the key or null if no values was present.
+ */
+ @Nullable
+ public List<T> remove(final @Nullable K key) {
+ return mMap.remove(key);
+ }
+
+ /**
+ * Remove a (key, value) mapping from this map
+ *
+ * @param key the key to remove
+ * @param value the value to remove
+ * @return true if the (key, value) mapping was present, false otherwise
+ */
+ @Nullable
+ public boolean remove(final @Nullable K key, final @Nullable T value) {
+ if (!mMap.containsKey(key)) {
+ return false;
+ }
+
+ final List<T> values = mMap.get(key);
+ final boolean wasPresent = values.remove(value);
+
+ if (values.isEmpty()) {
+ mMap.remove(key);
+ }
+
+ return wasPresent;
+ }
+
+ /** Remove all mappings from this map. */
+ public void clear() {
+ mMap.clear();
+ }
+
+ /**
+ * @return a set with all the keys for this map.
+ */
+ @NonNull
+ public Set<K> keySet() {
+ return mMap.keySet();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NativeQueue.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NativeQueue.java
new file mode 100644
index 0000000000..7932e6c839
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/NativeQueue.java
@@ -0,0 +1,225 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+
+public class NativeQueue {
+ private static final String LOGTAG = "GeckoNativeQueue";
+
+ public interface State {
+ boolean is(final State other);
+
+ boolean isAtLeast(final State other);
+ }
+
+ private volatile State mState;
+ private final State mReadyState;
+
+ public NativeQueue(final State initial, final State ready) {
+ mState = initial;
+ mReadyState = ready;
+ }
+
+ public boolean isReady() {
+ return getState().isAtLeast(mReadyState);
+ }
+
+ public State getState() {
+ return mState;
+ }
+
+ public boolean setState(final State newState) {
+ return checkAndSetState(null, newState);
+ }
+
+ public synchronized boolean checkAndSetState(final State expectedState, final State newState) {
+ if (expectedState != null && !mState.is(expectedState)) {
+ return false;
+ }
+ flushQueuedLocked(newState);
+ mState = newState;
+ return true;
+ }
+
+ private static class QueuedCall {
+ public Method method;
+ public Object target;
+ public Object[] args;
+ public State state;
+
+ public QueuedCall(
+ final Method method, final Object target, final Object[] args, final State state) {
+ this.method = method;
+ this.target = target;
+ this.args = args;
+ this.state = state;
+ }
+ }
+
+ private static final int QUEUED_CALLS_COUNT = 16;
+ /* package */ final ArrayList<QueuedCall> mQueue = new ArrayList<>(QUEUED_CALLS_COUNT);
+
+ // Invoke the given Method and handle checked Exceptions.
+ private static void invokeMethod(final Method method, final Object obj, final Object[] args) {
+ try {
+ method.setAccessible(true);
+ method.invoke(obj, args);
+ } catch (final IllegalAccessException e) {
+ throw new IllegalStateException("Unexpected exception", e);
+ } catch (final InvocationTargetException e) {
+ throw new UnsupportedOperationException("Cannot make call", e.getCause());
+ }
+ }
+
+ // Queue a call to the given method.
+ private void queueNativeCallLocked(
+ final Class<?> cls,
+ final String methodName,
+ final Object obj,
+ final Object[] args,
+ final State state) {
+ final ArrayList<Class<?>> argTypes = new ArrayList<>(args.length);
+ final ArrayList<Object> argValues = new ArrayList<>(args.length);
+
+ for (int i = 0; i < args.length; i++) {
+ if (args[i] instanceof Class) {
+ argTypes.add((Class<?>) args[i]);
+ argValues.add(args[++i]);
+ continue;
+ }
+ Class<?> argType = args[i].getClass();
+ if (argType == Boolean.class) argType = Boolean.TYPE;
+ else if (argType == Byte.class) argType = Byte.TYPE;
+ else if (argType == Character.class) argType = Character.TYPE;
+ else if (argType == Double.class) argType = Double.TYPE;
+ else if (argType == Float.class) argType = Float.TYPE;
+ else if (argType == Integer.class) argType = Integer.TYPE;
+ else if (argType == Long.class) argType = Long.TYPE;
+ else if (argType == Short.class) argType = Short.TYPE;
+ argTypes.add(argType);
+ argValues.add(args[i]);
+ }
+ final Method method;
+ try {
+ method = cls.getDeclaredMethod(methodName, argTypes.toArray(new Class<?>[argTypes.size()]));
+ } catch (final NoSuchMethodException e) {
+ throw new IllegalArgumentException("Cannot find method", e);
+ }
+
+ if (!Modifier.isNative(method.getModifiers())) {
+ // As a precaution, we disallow queuing non-native methods. Queuing non-native
+ // methods is dangerous because the method could end up being called on either
+ // the original thread or the Gecko thread depending on timing. Native methods
+ // usually handle this by posting an event to the Gecko thread automatically,
+ // but there is no automatic mechanism for non-native methods.
+ throw new UnsupportedOperationException("Not allowed to queue non-native methods");
+ }
+
+ if (getState().isAtLeast(state)) {
+ invokeMethod(method, obj, argValues.toArray());
+ return;
+ }
+
+ mQueue.add(new QueuedCall(method, obj, argValues.toArray(), state));
+ }
+
+ /**
+ * Queue a call to the given instance method if the given current state does not satisfy the
+ * isReady condition.
+ *
+ * @param obj Object that declares the instance method.
+ * @param methodName Name of the instance method.
+ * @param args Args to call the instance method with; to specify a parameter type, pass in a Class
+ * instance first, followed by the value.
+ */
+ public synchronized void queueUntilReady(
+ final Object obj, final String methodName, final Object... args) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, mReadyState);
+ }
+
+ /**
+ * Queue a call to the given static method if the given current state does not satisfy the isReady
+ * condition.
+ *
+ * @param cls Class that declares the static method.
+ * @param methodName Name of the instance method.
+ * @param args Args to call the instance method with; to specify a parameter type, pass in a Class
+ * instance first, followed by the value.
+ */
+ public synchronized void queueUntilReady(
+ final Class<?> cls, final String methodName, final Object... args) {
+ queueNativeCallLocked(cls, methodName, null, args, mReadyState);
+ }
+
+ /**
+ * Queue a call to the given instance method if the given current state does not satisfy the given
+ * state.
+ *
+ * @param state The state in which the native call could be executed.
+ * @param obj Object that declares the instance method.
+ * @param methodName Name of the instance method.
+ * @param args Args to call the instance method with; to specify a parameter type, pass in a Class
+ * instance first, followed by the value.
+ */
+ public synchronized void queueUntil(
+ final State state, final Object obj, final String methodName, final Object... args) {
+ queueNativeCallLocked(obj.getClass(), methodName, obj, args, state);
+ }
+
+ /**
+ * Queue a call to the given static method if the given current state does not satisfy the given
+ * state.
+ *
+ * @param state The state in which the native call could be executed.
+ * @param cls Class that declares the static method.
+ * @param methodName Name of the instance method.
+ * @param args Args to call the instance method with; to specify a parameter type, pass in a Class
+ * instance first, followed by the value.
+ */
+ public synchronized void queueUntil(
+ final State state, final Class<?> cls, final String methodName, final Object... args) {
+ queueNativeCallLocked(cls, methodName, null, args, state);
+ }
+
+ // Run all queued methods
+ private void flushQueuedLocked(final State state) {
+ int lastSkipped = -1;
+ for (int i = 0; i < mQueue.size(); i++) {
+ final QueuedCall call = mQueue.get(i);
+ if (call == null) {
+ // We already handled the call.
+ continue;
+ }
+ if (!state.isAtLeast(call.state)) {
+ // The call is not ready yet; skip it.
+ lastSkipped = i;
+ continue;
+ }
+ // Mark as handled.
+ mQueue.set(i, null);
+
+ invokeMethod(call.method, call.target, call.args);
+ }
+ if (lastSkipped < 0) {
+ // We're done here; release the memory
+ mQueue.clear();
+ } else if (lastSkipped < mQueue.size() - 1) {
+ // We skipped some; free up null entries at the end,
+ // but keep all the previous entries for later.
+ mQueue.subList(lastSkipped + 1, mQueue.size()).clear();
+ }
+ }
+
+ public synchronized void reset(final State initial) {
+ mQueue.clear();
+ mState = initial;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ScreenManagerHelper.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ScreenManagerHelper.java
new file mode 100644
index 0000000000..edd6c7418a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/ScreenManagerHelper.java
@@ -0,0 +1,24 @@
+/* -*- 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;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+class ScreenManagerHelper {
+
+ /** Trigger a refresh of the cached screen information held by Gecko. */
+ public static void refreshScreenInfo() {
+ // Screen data is initialised automatically on startup, so no need to queue the call if
+ // Gecko isn't running yet.
+ if (GeckoThread.isRunning()) {
+ nativeRefreshScreenInfo();
+ }
+ }
+
+ @WrapForJNI(stubName = "RefreshScreenInfo", dispatchTo = "gecko")
+ private static native void nativeRefreshScreenInfo();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java
new file mode 100644
index 0000000000..9bb116451e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SpeechSynthesisService.java
@@ -0,0 +1,227 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil -*- */
+/* vim: set ts=20 sts=4 et sw=4: */
+/* 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;
+
+import android.content.Context;
+import android.os.Build;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.util.Log;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class SpeechSynthesisService {
+ private static final String LOGTAG = "GeckoSpeechSynthesis";
+ // Object type is used to make it easier to remove android.speech dependencies using Proguard.
+ private static Object sTTS;
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void initSynth() {
+ initSynthInternal();
+ }
+
+ // Extra internal method to make it easier to remove android.speech dependencies using Proguard.
+ private static void initSynthInternal() {
+ if (sTTS != null) {
+ return;
+ }
+
+ final Context ctx = GeckoAppShell.getApplicationContext();
+
+ sTTS =
+ new TextToSpeech(
+ ctx,
+ new TextToSpeech.OnInitListener() {
+ @Override
+ public void onInit(final int status) {
+ if (status != TextToSpeech.SUCCESS) {
+ Log.w(LOGTAG, "Failed to initialize TextToSpeech");
+ return;
+ }
+
+ setUtteranceListener();
+ registerVoicesByLocale();
+ }
+ });
+ }
+
+ private static TextToSpeech getTTS() {
+ return (TextToSpeech) sTTS;
+ }
+
+ private static void registerVoicesByLocale() {
+ ThreadUtils.postToBackgroundThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ final TextToSpeech tss = getTTS();
+ if (tss == null) {
+ Log.w(LOGTAG, "TextToSpeech is not initialized");
+ return;
+ }
+ final Locale defaultLocale = tss.getDefaultLanguage();
+ for (final Locale locale : getAvailableLanguages()) {
+ final Set<String> features = tss.getFeatures(locale);
+ final boolean isLocal =
+ features != null
+ && features.contains(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS);
+ final String localeStr = locale.toString();
+ registerVoice(
+ "moz-tts:android:" + localeStr,
+ locale.getDisplayName(),
+ localeStr.replace("_", "-"),
+ !isLocal,
+ defaultLocale == locale);
+ }
+ doneRegisteringVoices();
+ }
+ });
+ }
+
+ private static Set<Locale> getAvailableLanguages() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // While this method was introduced in 21, it seems that it
+ // has not been implemented in the speech service side until 23.
+ final Set<Locale> availableLanguages = getTTS().getAvailableLanguages();
+ if (availableLanguages != null) {
+ return availableLanguages;
+ }
+ }
+ final Set<Locale> locales = new HashSet<Locale>();
+ for (final Locale locale : Locale.getAvailableLocales()) {
+ if (locale.getVariant().isEmpty() && getTTS().isLanguageAvailable(locale) > 0) {
+ locales.add(locale);
+ }
+ }
+
+ return locales;
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void registerVoice(
+ String uri, String name, String locale, boolean isNetwork, boolean isDefault);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void doneRegisteringVoices();
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static String speak(
+ final String uri,
+ final String text,
+ final float rate,
+ final float pitch,
+ final float volume) {
+ final AtomicBoolean result = new AtomicBoolean(false);
+ final String utteranceId = UUID.randomUUID().toString();
+ speakInternal(uri, text, rate, pitch, volume, utteranceId, result);
+ return result.get() ? utteranceId : null;
+ }
+
+ // Extra internal method to make it easier to remove android.speech dependencies using Proguard.
+ private static void speakInternal(
+ final String uri,
+ final String text,
+ final float rate,
+ final float pitch,
+ final float volume,
+ final String utteranceId,
+ final AtomicBoolean result) {
+ if (sTTS == null) {
+ Log.w(LOGTAG, "TextToSpeech is not initialized");
+ return;
+ }
+
+ final HashMap<String, String> params = new HashMap<String, String>();
+ params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Float.toString(volume));
+ params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
+ final TextToSpeech tss = (TextToSpeech) sTTS;
+ tss.setLanguage(new Locale(uri.substring("moz-tts:android:".length())));
+ tss.setSpeechRate(rate);
+ tss.setPitch(pitch);
+ final int speakRes = tss.speak(text, TextToSpeech.QUEUE_FLUSH, params);
+ result.set(speakRes == TextToSpeech.SUCCESS);
+ }
+
+ private static void setUtteranceListener() {
+ if (sTTS == null) {
+ Log.w(LOGTAG, "TextToSpeech is not initialized");
+ return;
+ }
+
+ getTTS()
+ .setOnUtteranceProgressListener(
+ new UtteranceProgressListener() {
+ @Override
+ public void onDone(final String utteranceId) {
+ dispatchEnd(utteranceId);
+ }
+
+ @Override
+ public void onError(final String utteranceId) {
+ dispatchError(utteranceId);
+ }
+
+ @Override
+ public void onStart(final String utteranceId) {
+ dispatchStart(utteranceId);
+ }
+
+ @Override
+ public void onStop(final String utteranceId, final boolean interrupted) {
+ if (interrupted) {
+ dispatchEnd(utteranceId);
+ } else {
+ // utterance isn't started yet.
+ dispatchError(utteranceId);
+ }
+ }
+
+ public void onRangeStart(
+ final String utteranceId, final int start, final int end, final int frame) {
+ dispatchBoundary(utteranceId, start, end);
+ }
+ });
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void dispatchStart(String utteranceId);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void dispatchEnd(String utteranceId);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void dispatchError(String utteranceId);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void dispatchBoundary(String utteranceId, int start, int end);
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static void stop() {
+ stopInternal();
+ }
+
+ // Extra internal method to make it easier to remove android.speech dependencies using Proguard.
+ private static void stopInternal() {
+ if (sTTS == null) {
+ Log.w(LOGTAG, "TextToSpeech is not initialized");
+ return;
+ }
+
+ getTTS().stop();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // Android M has onStop method. If Android L or above, dispatch
+ // event
+ dispatchEnd(null);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceViewWrapper.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceViewWrapper.java
new file mode 100644
index 0000000000..d5258d7bd0
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/SurfaceViewWrapper.java
@@ -0,0 +1,198 @@
+/* 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;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.SurfaceTexture;
+import android.os.Build;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+
+/** Provides transparent access to either a SurfaceView or TextureView */
+public class SurfaceViewWrapper {
+ private static final String LOGTAG = "SurfaceViewWrapper";
+
+ private ListenerWrapper mListenerWrapper;
+ private View mView;
+
+ // Only one of these will be non-null at any point in time
+ SurfaceView mSurfaceView;
+ TextureView mTextureView;
+
+ public SurfaceViewWrapper(final Context context) {
+ // By default, use SurfaceView
+ mListenerWrapper = new ListenerWrapper();
+ initSurfaceView(context);
+ }
+
+ private void initSurfaceView(final Context context) {
+ mSurfaceView = new MagnifiableSurfaceView(context);
+ mSurfaceView.setBackgroundColor(Color.TRANSPARENT);
+ mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
+ mView = mSurfaceView;
+ }
+
+ public void useSurfaceView(final Context context) {
+ if (mTextureView != null) {
+ mListenerWrapper.onSurfaceTextureDestroyed(mTextureView.getSurfaceTexture());
+ mTextureView = null;
+ }
+ mListenerWrapper.reset();
+ initSurfaceView(context);
+ }
+
+ public void useTextureView(final Context context) {
+ if (mSurfaceView != null) {
+ mListenerWrapper.surfaceDestroyed(mSurfaceView.getHolder());
+ mSurfaceView = null;
+ }
+ mListenerWrapper.reset();
+ mTextureView = new TextureView(context);
+ mTextureView.setSurfaceTextureListener(mListenerWrapper);
+ mView = mTextureView;
+ }
+
+ public void setBackgroundColor(final int color) {
+ if (mSurfaceView != null) {
+ mSurfaceView.setBackgroundColor(color);
+ } else {
+ Log.e(LOGTAG, "TextureView doesn't support background color.");
+ }
+ }
+
+ public void setListener(final Listener listener) {
+ mListenerWrapper.mListener = listener;
+ mSurfaceView.getHolder().addCallback(mListenerWrapper);
+ }
+
+ public int getWidth() {
+ if (mSurfaceView != null) {
+ return mSurfaceView.getHolder().getSurfaceFrame().right;
+ }
+ return mListenerWrapper.mWidth;
+ }
+
+ public int getHeight() {
+ if (mSurfaceView != null) {
+ return mSurfaceView.getHolder().getSurfaceFrame().bottom;
+ }
+ return mListenerWrapper.mHeight;
+ }
+
+ /**
+ * Returns the SurfaceControl associated with the SurfaceView, or null on unsupported SDK versions
+ * or when using the TextureView backend.
+ */
+ public SurfaceControl getSurfaceControl() {
+ if (mSurfaceView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ return mSurfaceView.getSurfaceControl();
+ }
+
+ return null;
+ }
+
+ public Surface getSurface() {
+ if (mSurfaceView != null) {
+ return mSurfaceView.getHolder().getSurface();
+ }
+
+ return mListenerWrapper.mSurface;
+ }
+
+ public View getView() {
+ return mView;
+ }
+
+ /**
+ * Translates SurfaceTextureListener and SurfaceHolder.Callback into a common interface
+ * SurfaceViewWrapper.Listener
+ */
+ private class ListenerWrapper
+ implements TextureView.SurfaceTextureListener, SurfaceHolder.Callback {
+ private Listener mListener;
+
+ // TextureView doesn't provide getters for these so we keep track of them here
+ private Surface mSurface;
+ private int mWidth;
+ private int mHeight;
+
+ public void reset() {
+ mWidth = 0;
+ mHeight = 0;
+ mSurface = null;
+ }
+
+ // TextureView
+ @Override
+ public void onSurfaceTextureAvailable(
+ final SurfaceTexture surface, final int width, final int height) {
+ mSurface = new Surface(surface);
+ mWidth = width;
+ mHeight = height;
+ if (mListener != null) {
+ mListener.onSurfaceChanged(mSurface, null, width, height);
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(
+ final SurfaceTexture surface, final int width, final int height) {
+ mWidth = width;
+ mHeight = height;
+ if (mListener != null) {
+ mListener.onSurfaceChanged(mSurface, null, mWidth, mHeight);
+ }
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) {
+ if (mListener != null) {
+ mListener.onSurfaceDestroyed();
+ }
+ mSurface = null;
+ return false;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(final SurfaceTexture surface) {
+ mSurface = new Surface(surface);
+ if (mListener != null) {
+ mListener.onSurfaceChanged(mSurface, null, mWidth, mHeight);
+ }
+ }
+
+ // SurfaceView
+ @Override
+ public void surfaceCreated(final SurfaceHolder holder) {}
+
+ @Override
+ public void surfaceChanged(
+ final SurfaceHolder holder, final int format, final int width, final int height) {
+ if (mListener != null) {
+ mListener.onSurfaceChanged(holder.getSurface(), getSurfaceControl(), width, height);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(final SurfaceHolder holder) {
+ if (mListener != null) {
+ mListener.onSurfaceDestroyed();
+ }
+ }
+ }
+
+ public interface Listener {
+ void onSurfaceChanged(Surface surface, SurfaceControl surfaceControl, int width, int height);
+
+ void onSurfaceDestroyed();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TelemetryUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TelemetryUtils.java
new file mode 100644
index 0000000000..3c9c1f90a0
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/TelemetryUtils.java
@@ -0,0 +1,102 @@
+/* -*- 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;
+
+import android.os.SystemClock;
+import android.util.Log;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * All telemetry times are relative to one of two clocks:
+ *
+ * <p>* Real time since the device was booted, including deep sleep. Use this as a substitute for
+ * wall clock. * Uptime since the device was booted, excluding deep sleep. Use this to avoid timing
+ * a user activity when their phone is in their pocket!
+ *
+ * <p>The majority of methods in this class are defined in terms of real time.
+ */
+public class TelemetryUtils {
+ private static final String LOGTAG = "TelemetryUtils";
+
+ @WrapForJNI(stubName = "AddHistogram", dispatchTo = "gecko")
+ private static native void nativeAddHistogram(String name, int value);
+
+ public static long uptime() {
+ return SystemClock.uptimeMillis();
+ }
+
+ public static long realtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ // Define new histograms in:
+ // toolkit/components/telemetry/Histograms.json
+ public static void addToHistogram(final String name, final int value) {
+ if (GeckoThread.isRunning()) {
+ nativeAddHistogram(name, value);
+ } else {
+ GeckoThread.queueNativeCall(
+ TelemetryUtils.class, "nativeAddHistogram", String.class, name, value);
+ }
+ }
+
+ public abstract static class Timer {
+ private final long mStartTime;
+ private final String mName;
+
+ private volatile boolean mHasFinished;
+ private volatile long mElapsed = -1;
+
+ protected abstract long now();
+
+ public Timer(final String name) {
+ mName = name;
+ mStartTime = now();
+ }
+
+ public void cancel() {
+ mHasFinished = true;
+ }
+
+ public long getElapsed() {
+ return mElapsed;
+ }
+
+ public void stop() {
+ // Only the first stop counts.
+ if (mHasFinished) {
+ return;
+ }
+
+ mHasFinished = true;
+
+ final long elapsed = now() - mStartTime;
+ if (elapsed < 0) {
+ Log.e(LOGTAG, "Current time less than start time -- clock shenanigans?");
+ return;
+ }
+
+ mElapsed = elapsed;
+ if (elapsed > Integer.MAX_VALUE) {
+ Log.e(LOGTAG, "Duration of " + elapsed + "ms is too great to add to histogram.");
+ return;
+ }
+
+ addToHistogram(mName, (int) (elapsed));
+ }
+ }
+
+ public static class UptimeTimer extends Timer {
+ public UptimeTimer(final String name) {
+ super(name);
+ }
+
+ @Override
+ protected long now() {
+ return TelemetryUtils.uptime();
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/BuildFlag.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/BuildFlag.java
new file mode 100644
index 0000000000..805e0a3f79
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/BuildFlag.java
@@ -0,0 +1,25 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is used to tag classes that are conditionally built behind build flags. Any
+ * generated JNI bindings will incorporate the specified build flags.
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface BuildFlag {
+ /**
+ * Preprocessor macro for conditionally building the generated bindings. "MOZ_FOO" wraps generated
+ * bindings in "#ifdef MOZ_FOO / #endif" "!MOZ_FOO" wraps generated bindings in "#ifndef MOZ_FOO /
+ * #endif"
+ */
+ String value() default "";
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java
new file mode 100644
index 0000000000..d6140a1ffb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/JNITarget.java
@@ -0,0 +1,14 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface JNITarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java
new file mode 100644
index 0000000000..e873ebeb96
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/ReflectionTarget.java
@@ -0,0 +1,18 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/*
+ * Used to indicate to ProGuard that this definition is accessed
+ * via reflection and should not be stripped from the source.
+ */
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface ReflectionTarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java
new file mode 100644
index 0000000000..e15875dc8b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/RobocopTarget.java
@@ -0,0 +1,14 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface RobocopTarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java
new file mode 100644
index 0000000000..f58dea1487
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WebRTCJNITarget.java
@@ -0,0 +1,14 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+@Retention(RetentionPolicy.CLASS)
+public @interface WebRTCJNITarget {}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java
new file mode 100644
index 0000000000..6a3fcfcb1c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/annotation/WrapForJNI.java
@@ -0,0 +1,56 @@
+/* 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.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is used to tag methods that are to have wrapper methods generated. Such methods
+ * will be protected from destruction by ProGuard, and allow us to avoid writing by hand large
+ * amounts of boring boilerplate.
+ */
+@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface WrapForJNI {
+ /** Skip this member when generating wrappers for a whole class. */
+ boolean skip() default false;
+
+ /**
+ * Optional parameter specifying the name of the generated method stub. If omitted, the
+ * capitalized name of the Java method will be used.
+ */
+ String stubName() default "";
+
+ /**
+ * Action to take if member access returns an exception. - "abort" will cause a crash if there is
+ * a pending exception. - "ignore" will not handle any pending exceptions; it is then the caller's
+ * responsibility to handle exceptions. - "nsresult" will clear any pending exceptions and return
+ * an error code; not supported for native methods.
+ */
+ String exceptionMode() default "abort";
+
+ /**
+ * The thread that the method will be called from. One of "any", "gecko", or "ui". Not supported
+ * for fields.
+ */
+ String calledFrom() default "any";
+
+ /**
+ * The thread that the method call will be dispatched to. - "current" indicates no dispatching;
+ * only supported value for fields, constructors, non-native methods, and non-void native methods.
+ * - "gecko" indicates dispatching to the Gecko XPCOM (nsThread) event queue. - "gecko_priority"
+ * indicates dispatching to the Gecko widget (nsAppShell) event queue; in most cases, events in
+ * the widget event queue (aka native event queue) are favored over events in the XPCOM event
+ * queue. - "proxy" indicates dispatching to a proxy function as a function object; see
+ * widget/jni/Natives.h.
+ */
+ String dispatchTo() default "current";
+
+ /** Generate a getter instead of a literal. Only supported for static final fields. */
+ boolean noLiteral() default false;
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/AndroidVsync.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/AndroidVsync.java
new file mode 100644
index 0000000000..c87bf466d0
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/AndroidVsync.java
@@ -0,0 +1,72 @@
+/* -*- 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.gfx;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.view.Choreographer;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+
+/** This class receives HW vsync events through a {@link Choreographer}. */
+@WrapForJNI
+/* package */ final class AndroidVsync extends JNIObject implements Choreographer.FrameCallback {
+ @WrapForJNI
+ @Override // JNIObject
+ protected native void disposeNative();
+
+ private static final String LOGTAG = "AndroidVsync";
+
+ /* package */ Choreographer mChoreographer;
+ private volatile boolean mObservingVsync;
+
+ public AndroidVsync() {
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+ mainHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ mChoreographer = Choreographer.getInstance();
+ if (mObservingVsync) {
+ mChoreographer.postFrameCallback(AndroidVsync.this);
+ }
+ }
+ });
+ }
+
+ @WrapForJNI(stubName = "NotifyVsync")
+ private native void nativeNotifyVsync(final long frameTimeNanos);
+
+ // Choreographer callback implementation.
+ public void doFrame(final long frameTimeNanos) {
+ if (mObservingVsync) {
+ mChoreographer.postFrameCallback(this);
+ nativeNotifyVsync(frameTimeNanos);
+ }
+ }
+
+ /**
+ * Start/stop observing Vsync event.
+ *
+ * @param enable true to start observing; false to stop.
+ * @return true if observing and false if not.
+ */
+ @WrapForJNI
+ public synchronized boolean observeVsync(final boolean enable) {
+ if (mObservingVsync != enable) {
+ mObservingVsync = enable;
+
+ if (mChoreographer != null) {
+ if (enable) {
+ mChoreographer.postFrameCallback(this);
+ } else {
+ mChoreographer.removeFrameCallback(this);
+ }
+ }
+ }
+ return mObservingVsync;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/CompositorSurfaceManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/CompositorSurfaceManager.java
new file mode 100644
index 0000000000..1378a284b7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/CompositorSurfaceManager.java
@@ -0,0 +1,26 @@
+/* -*- 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.gfx;
+
+import android.os.RemoteException;
+import android.view.Surface;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public final class CompositorSurfaceManager {
+ private static final String LOGTAG = "CompSurfManager";
+
+ private ICompositorSurfaceManager mManager;
+
+ public CompositorSurfaceManager(final ICompositorSurfaceManager aManager) {
+ mManager = aManager;
+ }
+
+ @WrapForJNI(exceptionMode = "nsresult")
+ public synchronized void onSurfaceChanged(final int widgetId, final Surface surface)
+ throws RemoteException {
+ mManager.onSurfaceChanged(widgetId, surface);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurface.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurface.java
new file mode 100644
index 0000000000..d533d2ad39
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurface.java
@@ -0,0 +1,151 @@
+/* -*- 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.gfx;
+
+import static org.mozilla.geckoview.BuildConfig.DEBUG_BUILD;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.Surface;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public final class GeckoSurface implements Parcelable {
+ private static final String LOGTAG = "GeckoSurface";
+
+ private Surface mSurface;
+ private long mHandle;
+ private boolean mIsSingleBuffer;
+ private volatile boolean mIsAvailable;
+ private boolean mOwned = true;
+ private volatile boolean mIsReleased = false;
+
+ private int mMyPid;
+ // Locally allocated surface/texture. Do not pass it over IPC.
+ private GeckoSurface mSyncSurface;
+
+ @WrapForJNI(exceptionMode = "nsresult")
+ public GeckoSurface(final GeckoSurfaceTexture gst) {
+ mSurface = new Surface(gst);
+ mHandle = gst.getHandle();
+ mIsSingleBuffer = gst.isSingleBuffer();
+ mIsAvailable = true;
+ mMyPid = android.os.Process.myPid();
+ }
+
+ public GeckoSurface(final Parcel p) {
+ mSurface = Surface.CREATOR.createFromParcel(p);
+ mHandle = p.readLong();
+ mIsSingleBuffer = p.readByte() == 1;
+ mIsAvailable = p.readByte() == 1;
+ mMyPid = p.readInt();
+ }
+
+ public static final Parcelable.Creator<GeckoSurface> CREATOR =
+ new Parcelable.Creator<GeckoSurface>() {
+ public GeckoSurface createFromParcel(final Parcel p) {
+ return new GeckoSurface(p);
+ }
+
+ public GeckoSurface[] newArray(final int size) {
+ return new GeckoSurface[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel out, final int flags) {
+ mSurface.writeToParcel(out, flags);
+ if ((flags & Parcelable.PARCELABLE_WRITE_RETURN_VALUE) == 0) {
+ // GeckoSurface can be passed across processes as a return value or
+ // an argument, and should always tranfers its ownership (move) to
+ // the receiver of parcel. On the other hand, Surface is moved only
+ // when passed as a return value and releases itself when corresponding
+ // write flags is provided. (See Surface.writeToParcel().)
+ // The superclass method must be called here to ensure the local instance
+ // is truely forgotten.
+ mSurface.release();
+ }
+ mOwned = false;
+
+ out.writeLong(mHandle);
+ out.writeByte((byte) (mIsSingleBuffer ? 1 : 0));
+ out.writeByte((byte) (mIsAvailable ? 1 : 0));
+ out.writeInt(mMyPid);
+ }
+
+ public void release() {
+ if (mIsReleased) {
+ return;
+ }
+ mIsReleased = true;
+
+ if (mSyncSurface != null) {
+ mSyncSurface.release();
+ final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(mSyncSurface.getHandle());
+ if (gst != null) {
+ gst.decrementUse();
+ }
+ mSyncSurface = null;
+ }
+
+ if (mOwned) {
+ mSurface.release();
+ }
+ }
+
+ @WrapForJNI
+ public long getHandle() {
+ return mHandle;
+ }
+
+ @WrapForJNI
+ public Surface getSurface() {
+ return mSurface;
+ }
+
+ @WrapForJNI
+ public boolean getAvailable() {
+ return mIsAvailable;
+ }
+
+ @WrapForJNI
+ public boolean isReleased() {
+ return mIsReleased;
+ }
+
+ @WrapForJNI
+ public void setAvailable(final boolean available) {
+ mIsAvailable = available;
+ }
+
+ /* package */ boolean inProcess() {
+ return android.os.Process.myPid() == mMyPid;
+ }
+
+ /* package */ SyncConfig initSyncSurface(final int width, final int height) {
+ if (DEBUG_BUILD) {
+ if (inProcess()) {
+ throw new AssertionError("no need for sync when allocated in process");
+ }
+ }
+ if (GeckoSurfaceTexture.lookup(mHandle) != null) {
+ throw new AssertionError("texture#" + mHandle + " already in use.");
+ }
+ final GeckoSurfaceTexture texture = GeckoSurfaceTexture.acquire(true, mHandle);
+ if (texture != null) {
+ texture.setDefaultBufferSize(width, height);
+ texture.track(mHandle);
+ mSyncSurface = new GeckoSurface(texture);
+ return new SyncConfig(mHandle, mSyncSurface, width, height);
+ }
+
+ return null;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurfaceTexture.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurfaceTexture.java
new file mode 100644
index 0000000000..b063fc9c2c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoSurfaceTexture.java
@@ -0,0 +1,314 @@
+/* -*- 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.gfx;
+
+import android.graphics.SurfaceTexture;
+import android.os.Build;
+import android.util.Log;
+import android.util.LongSparseArray;
+import java.util.LinkedList;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+
+/* package */ final class GeckoSurfaceTexture extends SurfaceTexture {
+ private static final String LOGTAG = "GeckoSurfaceTexture";
+ private static final int MAX_SURFACE_TEXTURES = 200;
+ private static final LongSparseArray<GeckoSurfaceTexture> sSurfaceTextures =
+ new LongSparseArray<GeckoSurfaceTexture>();
+
+ private static LongSparseArray<LinkedList<GeckoSurfaceTexture>> sUnusedTextures =
+ new LongSparseArray<LinkedList<GeckoSurfaceTexture>>();
+
+ private long mHandle;
+ private boolean mIsSingleBuffer;
+
+ private long mAttachedContext;
+ private int mTexName;
+
+ private GeckoSurfaceTexture.Callbacks mListener;
+ private AtomicInteger mUseCount;
+ private boolean mFinalized;
+
+ private long mUpstream;
+ private NativeGLBlitHelper mBlitter;
+
+ private GeckoSurfaceTexture(final long handle) {
+ super(0);
+ init(handle, false);
+ }
+
+ private GeckoSurfaceTexture(final long handle, final boolean singleBufferMode) {
+ super(0, singleBufferMode);
+ init(handle, singleBufferMode);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ // We only want finalize() to be called once
+ if (mFinalized) {
+ return;
+ }
+
+ mFinalized = true;
+ super.finalize();
+ }
+
+ private void init(final long handle, final boolean singleBufferMode) {
+ mHandle = handle;
+ mIsSingleBuffer = singleBufferMode;
+ mUseCount = new AtomicInteger(1);
+
+ // Start off detached
+ detachFromGLContext();
+ }
+
+ @WrapForJNI
+ public long getHandle() {
+ return mHandle;
+ }
+
+ @WrapForJNI
+ public int getTexName() {
+ return mTexName;
+ }
+
+ @WrapForJNI(exceptionMode = "nsresult")
+ public synchronized void attachToGLContext(final long context, final int texName) {
+ if (context == mAttachedContext && texName == mTexName) {
+ return;
+ }
+
+ attachToGLContext(texName);
+
+ mAttachedContext = context;
+ mTexName = texName;
+ }
+
+ @Override
+ @WrapForJNI(exceptionMode = "nsresult")
+ public synchronized void detachFromGLContext() {
+ super.detachFromGLContext();
+
+ mAttachedContext = mTexName = 0;
+ }
+
+ @WrapForJNI
+ public synchronized boolean isAttachedToGLContext(final long context) {
+ return mAttachedContext == context;
+ }
+
+ @WrapForJNI
+ public boolean isSingleBuffer() {
+ return mIsSingleBuffer;
+ }
+
+ @Override
+ @WrapForJNI
+ public synchronized void updateTexImage() {
+ try {
+ if (mUpstream != 0) {
+ SurfaceAllocator.sync(mUpstream);
+ }
+ super.updateTexImage();
+ if (mListener != null) {
+ mListener.onUpdateTexImage();
+ }
+ } catch (final Exception e) {
+ Log.w(LOGTAG, "updateTexImage() failed", e);
+ }
+ }
+
+ @Override
+ public synchronized void release() {
+ mUpstream = 0;
+ if (mBlitter != null) {
+ mBlitter.close();
+ }
+ try {
+ super.release();
+ synchronized (sSurfaceTextures) {
+ sSurfaceTextures.remove(mHandle);
+ }
+ } catch (final Exception e) {
+ Log.w(LOGTAG, "release() failed", e);
+ }
+ }
+
+ @Override
+ @WrapForJNI
+ public synchronized void releaseTexImage() {
+ if (!mIsSingleBuffer) {
+ return;
+ }
+
+ try {
+ super.releaseTexImage();
+ if (mListener != null) {
+ mListener.onReleaseTexImage();
+ }
+ } catch (final Exception e) {
+ Log.w(LOGTAG, "releaseTexImage() failed", e);
+ }
+ }
+
+ public synchronized void setListener(final GeckoSurfaceTexture.Callbacks listener) {
+ mListener = listener;
+ }
+
+ @WrapForJNI
+ public synchronized void incrementUse() {
+ mUseCount.incrementAndGet();
+ }
+
+ @WrapForJNI
+ public synchronized void decrementUse() {
+ final int useCount = mUseCount.decrementAndGet();
+
+ if (useCount == 0) {
+ setListener(null);
+
+ if (mAttachedContext == 0) {
+ release();
+ synchronized (sUnusedTextures) {
+ sSurfaceTextures.remove(mHandle);
+ }
+ return;
+ }
+
+ synchronized (sUnusedTextures) {
+ LinkedList<GeckoSurfaceTexture> list = sUnusedTextures.get(mAttachedContext);
+ if (list == null) {
+ list = new LinkedList<GeckoSurfaceTexture>();
+ sUnusedTextures.put(mAttachedContext, list);
+ }
+ list.addFirst(this);
+ }
+ }
+ }
+
+ @WrapForJNI
+ public static void destroyUnused(final long context) {
+ final LinkedList<GeckoSurfaceTexture> list;
+ synchronized (sUnusedTextures) {
+ list = sUnusedTextures.get(context);
+ sUnusedTextures.delete(context);
+ }
+
+ if (list == null) {
+ return;
+ }
+
+ for (final GeckoSurfaceTexture tex : list) {
+ try {
+ if (tex.isSingleBuffer()) {
+ tex.releaseTexImage();
+ }
+
+ tex.detachFromGLContext();
+ tex.release();
+
+ // We need to manually call finalize here, otherwise we can run out
+ // of file descriptors if the GC doesn't kick in soon enough. Bug 1416015.
+ try {
+ tex.finalize();
+ } catch (final Throwable t) {
+ Log.e(LOGTAG, "Failed to finalize SurfaceTexture", t);
+ }
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Failed to destroy SurfaceTexture", e);
+ }
+ }
+ }
+
+ public static GeckoSurfaceTexture acquire(final boolean singleBufferMode, final long handle) {
+ // Attempting to create a SurfaceTexture from an isolated process on Android versions prior to
+ // 8.0 results in an indefinite hang. See bug 1706656.
+ if (GeckoAppShell.isIsolatedProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return null;
+ }
+
+ synchronized (sSurfaceTextures) {
+ // We want to limit the maximum number of SurfaceTextures at any one time.
+ // This is because they use a large number of fds, and once the process' limit
+ // is reached bad things happen. See bug 1421586.
+ if (sSurfaceTextures.size() >= MAX_SURFACE_TEXTURES) {
+ return null;
+ }
+
+ if (sSurfaceTextures.indexOfKey(handle) >= 0) {
+ throw new IllegalArgumentException("Already have a GeckoSurfaceTexture with that handle");
+ }
+
+ final GeckoSurfaceTexture gst = new GeckoSurfaceTexture(handle, singleBufferMode);
+
+ sSurfaceTextures.put(handle, gst);
+
+ return gst;
+ }
+ }
+
+ @WrapForJNI
+ public static GeckoSurfaceTexture lookup(final long handle) {
+ synchronized (sSurfaceTextures) {
+ return sSurfaceTextures.get(handle);
+ }
+ }
+
+ /* package */ synchronized void track(final long upstream) {
+ mUpstream = upstream;
+ }
+
+ /* package */ synchronized void configureSnapshot(
+ final GeckoSurface target, final int width, final int height) {
+ mBlitter = NativeGLBlitHelper.create(mHandle, target, width, height);
+ }
+
+ /* package */ synchronized void takeSnapshot() {
+ mBlitter.blit();
+ }
+
+ public interface Callbacks {
+ void onUpdateTexImage();
+
+ void onReleaseTexImage();
+ }
+
+ @WrapForJNI
+ public static final class NativeGLBlitHelper extends JNIObject {
+ public static NativeGLBlitHelper create(
+ final long textureHandle,
+ final GeckoSurface targetSurface,
+ final int width,
+ final int height) {
+ final NativeGLBlitHelper helper = nativeCreate(textureHandle, targetSurface, width, height);
+ helper.mTargetSurface = targetSurface; // Take ownership of surface.
+ return helper;
+ }
+
+ public static native NativeGLBlitHelper nativeCreate(
+ final long textureHandle,
+ final GeckoSurface targetSurface,
+ final int width,
+ final int height);
+
+ public native void blit();
+
+ public void close() {
+ disposeNative();
+ if (mTargetSurface != null) {
+ mTargetSurface.release();
+ mTargetSurface = null;
+ }
+ }
+
+ @Override
+ protected native void disposeNative();
+
+ private GeckoSurface mTargetSurface;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
new file mode 100644
index 0000000000..b8ceb74f0b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/PanningPerfAPI.java
@@ -0,0 +1,71 @@
+/* -*- 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.gfx;
+
+import android.os.SystemClock;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public final class PanningPerfAPI {
+ private static final String LOGTAG = "GeckoPanningPerfAPI";
+
+ // make this large enough to avoid having to resize the frame time
+ // list, as that may be expensive and impact the thing we're trying
+ // to measure.
+ private static final int EXPECTED_FRAME_COUNT = 2048;
+
+ private static boolean mRecordingFrames;
+ private static List<Long> mFrameTimes;
+ private static long mFrameStartTime;
+
+ private static void initialiseRecordingArrays() {
+ if (mFrameTimes == null) {
+ mFrameTimes = new ArrayList<Long>(EXPECTED_FRAME_COUNT);
+ } else {
+ mFrameTimes.clear();
+ }
+ }
+
+ @RobocopTarget
+ public static void startFrameTimeRecording() {
+ if (mRecordingFrames) {
+ Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
+ return;
+ }
+ mRecordingFrames = true;
+ initialiseRecordingArrays();
+ mFrameStartTime = SystemClock.uptimeMillis();
+ }
+
+ @RobocopTarget
+ public static List<Long> stopFrameTimeRecording() {
+ if (!mRecordingFrames) {
+ Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
+ return null;
+ }
+ mRecordingFrames = false;
+ return mFrameTimes;
+ }
+
+ public static void recordFrameTime() {
+ // this will be called often, so try to make it as quick as possible
+ if (mRecordingFrames) {
+ mFrameTimes.add(SystemClock.uptimeMillis() - mFrameStartTime);
+ }
+ }
+
+ @RobocopTarget
+ public static void startCheckerboardRecording() {
+ throw new UnsupportedOperationException();
+ }
+
+ @RobocopTarget
+ public static List<Float> stopCheckerboardRecording() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java
new file mode 100644
index 0000000000..3244519da1
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java
@@ -0,0 +1,77 @@
+/* -*- 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.gfx;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class RemoteSurfaceAllocator extends ISurfaceAllocator.Stub {
+ private static final String LOGTAG = "RemoteSurfaceAllocator";
+
+ private static RemoteSurfaceAllocator mInstance;
+
+ private final int mAllocatorId;
+ /// Monotonically increasing counter used to generate unique handles
+ /// for each SurfaceTexture by combining with mAllocatorId.
+ private static AtomicInteger sNextHandle = new AtomicInteger(1);
+
+ /**
+ * Retrieves the singleton allocator instance for this process.
+ *
+ * @param allocatorId A unique ID identifying the process this instance belongs to, which must be
+ * 0 for the parent process instance.
+ */
+ public static synchronized RemoteSurfaceAllocator getInstance(final int allocatorId) {
+ if (mInstance == null) {
+ mInstance = new RemoteSurfaceAllocator(allocatorId);
+ }
+ return mInstance;
+ }
+
+ private RemoteSurfaceAllocator(final int allocatorId) {
+ mAllocatorId = allocatorId;
+ }
+
+ @Override
+ public GeckoSurface acquireSurface(
+ final int width, final int height, final boolean singleBufferMode) {
+ final long handle = ((long) mAllocatorId << 32) | sNextHandle.getAndIncrement();
+ final GeckoSurfaceTexture gst = GeckoSurfaceTexture.acquire(singleBufferMode, handle);
+
+ if (gst == null) {
+ return null;
+ }
+
+ if (width > 0 && height > 0) {
+ gst.setDefaultBufferSize(width, height);
+ }
+
+ return new GeckoSurface(gst);
+ }
+
+ @Override
+ public void releaseSurface(final long handle) {
+ final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(handle);
+ if (gst != null) {
+ gst.decrementUse();
+ }
+ }
+
+ @Override
+ public void configureSync(final SyncConfig config) {
+ final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(config.sourceTextureHandle);
+ if (gst != null) {
+ gst.configureSnapshot(config.targetSurface, config.width, config.height);
+ }
+ }
+
+ @Override
+ public void sync(final long handle) {
+ final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(handle);
+ if (gst != null) {
+ gst.takeSnapshot();
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java
new file mode 100644
index 0000000000..f3cca81a81
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java
@@ -0,0 +1,139 @@
+/* -*- 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.gfx;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.LongSparseArray;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.process.GeckoProcessManager;
+import org.mozilla.gecko.process.GeckoServiceChildProcess;
+
+/* package */ final class SurfaceAllocator {
+ private static final String LOGTAG = "SurfaceAllocator";
+
+ private static ISurfaceAllocator sAllocator;
+
+ // Keep a reference to all allocated Surfaces, so that we can release them if we lose the
+ // connection to the allocator service.
+ private static final LongSparseArray<GeckoSurface> sSurfaces =
+ new LongSparseArray<GeckoSurface>();
+
+ private static synchronized void ensureConnection() {
+ if (sAllocator != null) {
+ return;
+ }
+
+ try {
+ if (GeckoAppShell.isParentProcess()) {
+ sAllocator = GeckoProcessManager.getInstance().getSurfaceAllocator();
+ } else {
+ sAllocator = GeckoServiceChildProcess.getSurfaceAllocator();
+ }
+
+ if (sAllocator == null) {
+ Log.w(LOGTAG, "Failed to connect to RemoteSurfaceAllocator");
+ return;
+ }
+ sAllocator
+ .asBinder()
+ .linkToDeath(
+ new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ Log.w(LOGTAG, "RemoteSurfaceAllocator died");
+ synchronized (SurfaceAllocator.class) {
+ // Our connection to the remote allocator has died, so all our surfaces are
+ // invalid. Release them all now. When their owners attempt to render in to
+ // them they can detect they have been released and allocate new ones instead.
+ for (int i = 0; i < sSurfaces.size(); i++) {
+ sSurfaces.valueAt(i).release();
+ }
+ sSurfaces.clear();
+ sAllocator = null;
+ }
+ }
+ },
+ 0);
+ } catch (final RemoteException e) {
+ Log.w(LOGTAG, "Failed to connect to RemoteSurfaceAllocator", e);
+ sAllocator = null;
+ }
+ }
+
+ @WrapForJNI
+ public static synchronized GeckoSurface acquireSurface(
+ final int width, final int height, final boolean singleBufferMode) {
+ try {
+ ensureConnection();
+
+ if (sAllocator == null) {
+ Log.w(LOGTAG, "Failed to acquire GeckoSurface: not connected");
+ return null;
+ }
+
+ final GeckoSurface surface = sAllocator.acquireSurface(width, height, singleBufferMode);
+ if (surface == null) {
+ Log.w(LOGTAG, "Failed to acquire GeckoSurface: RemoteSurfaceAllocator returned null");
+ return null;
+ }
+ sSurfaces.put(surface.getHandle(), surface);
+
+ if (!surface.inProcess()) {
+ final SyncConfig config = surface.initSyncSurface(width, height);
+ if (config != null) {
+ sAllocator.configureSync(config);
+ }
+ }
+ return surface;
+ } catch (final RemoteException e) {
+ Log.w(LOGTAG, "Failed to acquire GeckoSurface", e);
+ return null;
+ }
+ }
+
+ @WrapForJNI
+ public static synchronized void disposeSurface(final GeckoSurface surface) {
+ // If the surface has already been released (probably due to losing connection to the remote
+ // allocator) then there is nothing to do here.
+ if (surface.isReleased()) {
+ return;
+ }
+
+ sSurfaces.remove(surface.getHandle());
+
+ // Release our Surface
+ surface.release();
+
+ if (sAllocator == null) {
+ return;
+ }
+
+ // Release the SurfaceTexture on the other side. If we have lost connection then do nothing, as
+ // there is nothing on the other side to release.
+ try {
+ if (sAllocator != null) {
+ sAllocator.releaseSurface(surface.getHandle());
+ }
+ } catch (final RemoteException e) {
+ Log.w(LOGTAG, "Failed to release surface texture", e);
+ }
+ }
+
+ public static synchronized void sync(final long upstream) {
+ // Sync from the SurfaceTexture on the other side. If we have lost connection then do nothing,
+ // as there is nothing on the other side to sync from.
+ try {
+ if (sAllocator != null) {
+ sAllocator.sync(upstream);
+ }
+ } catch (final RemoteException e) {
+ Log.w(LOGTAG, "Failed to sync texture", e);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceControlManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceControlManager.java
new file mode 100644
index 0000000000..7732cc3bc9
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceControlManager.java
@@ -0,0 +1,111 @@
+/* -*- 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.gfx;
+
+import android.os.Build;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import androidx.annotation.RequiresApi;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.WeakHashMap;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+// A helper class that creates Surfaces from SurfaceControl objects, for the widget to render in to.
+// Unlike the Surfaces provided to the widget directly from the application, these are suitable for
+// use in the GPU process as well as the main process.
+//
+// The reason we must not render directly in to the Surface provided by the application from the GPU
+// process is because of a bug on Android versions 12 and later: when the GPU process dies the
+// Surface is not detached from the dead process' EGL surface, and any subsequent attempts to
+// attach another EGL surface to the Surface will fail.
+//
+// The application is therefore required to provide the SurfaceControl object to a GeckoDisplay
+// whenever rendering in to a SurfaceView. The widget will then obtain a Surface from that
+// SurfaceControl using getChildSurface(). Internally, this creates another SurfaceControl as a
+// child of the provided SurfaceControl, then creates the Surface from that child. If the GPU
+// process dies we are able to simply destroy and recreate the child SurfaceControl objects, thereby
+// avoiding the bug.
+public class SurfaceControlManager {
+ private static final String LOGTAG = "SurfaceControlManager";
+
+ private static final SurfaceControlManager sInstance = new SurfaceControlManager();
+
+ private final WeakHashMap<SurfaceControl, SurfaceControl> mChildSurfaceControls =
+ new WeakHashMap<>();
+
+ @WrapForJNI
+ public static SurfaceControlManager getInstance() {
+ return sInstance;
+ }
+
+ // Returns a Surface of the requested size that will be composited in to the specified
+ // SurfaceControl.
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ @WrapForJNI(exceptionMode = "abort")
+ public synchronized Surface getChildSurface(
+ final SurfaceControl parent, final int width, final int height) {
+ SurfaceControl child = mChildSurfaceControls.get(parent);
+ if (child == null) {
+ // We must periodically check if any of the SurfaceControls we are managing have been
+ // destroyed, as we are unable to directly listen to their SurfaceViews' surfaceDestroyed
+ // callbacks, and they may not be attached to any compositor when they are destroyed meaning
+ // we cannot perform cleanup in response to the compositor being paused.
+ // Doing so here, when we encounter a new SurfaceControl instance, is a reasonable guess as to
+ // when a previous instance may have been released.
+ final Iterator<Map.Entry<SurfaceControl, SurfaceControl>> it =
+ mChildSurfaceControls.entrySet().iterator();
+ while (it.hasNext()) {
+ final Map.Entry<SurfaceControl, SurfaceControl> entry = it.next();
+ if (!entry.getKey().isValid()) {
+ it.remove();
+ }
+ }
+
+ child = new SurfaceControl.Builder().setParent(parent).setName("GeckoSurface").build();
+ mChildSurfaceControls.put(parent, child);
+ }
+
+ final SurfaceControl.Transaction transaction =
+ new SurfaceControl.Transaction()
+ .setVisibility(child, true)
+ .setBufferSize(child, width, height);
+ transaction.apply();
+ transaction.close();
+
+ return new Surface(child);
+ }
+
+ // Removes an existing parent SurfaceControl and its corresponding child from the manager. This
+ // can be used when we require the next call to getChildSurface() for the specified parent to
+ // create a new child rather than return the existing one.
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ @WrapForJNI(exceptionMode = "abort")
+ public synchronized void removeSurface(final SurfaceControl parent) {
+ final SurfaceControl child = mChildSurfaceControls.remove(parent);
+ if (child != null) {
+ child.release();
+ }
+ }
+
+ // Must be called whenever the GPU process has died. This destroys all the child SurfaceControls
+ // that have been created, meaning subsequent calls to getChildSurface() will create new ones.
+ @RequiresApi(api = Build.VERSION_CODES.Q)
+ @WrapForJNI(exceptionMode = "abort")
+ public synchronized void onGpuProcessLoss() {
+ for (final SurfaceControl child : mChildSurfaceControls.values()) {
+ // Explicitly reparenting the old SurfaceControl to null ensures SurfaceFlinger does not hold
+ // on to it. We used to not do this in order to avoid a blank screen until we resume rendering
+ // in to a new SurfaceControl, but on some devices this was causing glitches.
+ final SurfaceControl.Transaction transaction =
+ new SurfaceControl.Transaction().reparent(child, null);
+ transaction.apply();
+ transaction.close();
+ child.release();
+ }
+ mChildSurfaceControls.clear();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java
new file mode 100644
index 0000000000..0ba79d1f42
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceTextureListener.java
@@ -0,0 +1,38 @@
+/* -*- 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.gfx;
+
+import android.graphics.SurfaceTexture;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+
+/* package */ final class SurfaceTextureListener extends JNIObject
+ implements SurfaceTexture.OnFrameAvailableListener {
+ @WrapForJNI(calledFrom = "gecko")
+ private SurfaceTextureListener() {}
+
+ @WrapForJNI(dispatchTo = "gecko")
+ @Override // JNIObject
+ protected native void disposeNative();
+
+ @Override
+ protected void finalize() {
+ disposeNative();
+ }
+
+ @WrapForJNI(stubName = "OnFrameAvailable")
+ private native void nativeOnFrameAvailable();
+
+ @Override // SurfaceTexture.OnFrameAvailableListener
+ public void onFrameAvailable(final SurfaceTexture surfaceTexture) {
+ try {
+ nativeOnFrameAvailable();
+ } catch (final NullPointerException e) {
+ // Ignore exceptions caused by a disposed object, i.e.
+ // getting a callback after this listener is no longer in use.
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SyncConfig.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SyncConfig.java
new file mode 100644
index 0000000000..d8e2099ddc
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SyncConfig.java
@@ -0,0 +1,59 @@
+/* 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.gfx;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/* package */ final class SyncConfig implements Parcelable {
+ final long sourceTextureHandle;
+ final GeckoSurface targetSurface;
+ final int width;
+ final int height;
+
+ /* package */ SyncConfig(
+ final long sourceTextureHandle,
+ final GeckoSurface targetSurface,
+ final int width,
+ final int height) {
+ this.sourceTextureHandle = sourceTextureHandle;
+ this.targetSurface = targetSurface;
+ this.width = width;
+ this.height = height;
+ }
+
+ public static final Creator<SyncConfig> CREATOR =
+ new Creator<SyncConfig>() {
+ @Override
+ public SyncConfig createFromParcel(final Parcel parcel) {
+ return new SyncConfig(parcel);
+ }
+
+ @Override
+ public SyncConfig[] newArray(final int i) {
+ return new SyncConfig[i];
+ }
+ };
+
+ private SyncConfig(final Parcel parcel) {
+ sourceTextureHandle = parcel.readLong();
+ targetSurface = GeckoSurface.CREATOR.createFromParcel(parcel);
+ width = parcel.readInt();
+ height = parcel.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel parcel, final int flags) {
+ parcel.writeLong(sourceTextureHandle);
+ targetSurface.writeToParcel(parcel, flags);
+ parcel.writeInt(width);
+ parcel.writeInt(height);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/AsyncCodec.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/AsyncCodec.java
new file mode 100644
index 0000000000..b29d488c6c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/AsyncCodec.java
@@ -0,0 +1,61 @@
+/* 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.media;
+
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CryptoInfo;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.view.Surface;
+import java.nio.ByteBuffer;
+
+// A wrapper interface that mimics the new {@link android.media.MediaCodec}
+// asynchronous mode API in Lollipop.
+public interface AsyncCodec {
+ interface Callbacks {
+ void onInputBufferAvailable(AsyncCodec codec, int index);
+
+ void onOutputBufferAvailable(AsyncCodec codec, int index, BufferInfo info);
+
+ void onError(AsyncCodec codec, int error);
+
+ void onOutputFormatChanged(AsyncCodec codec, MediaFormat format);
+ }
+
+ void setCallbacks(Callbacks callbacks, Handler handler);
+
+ void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags);
+
+ boolean isAdaptivePlaybackSupported(String mimeType);
+
+ boolean isTunneledPlaybackSupported(final String mimeType);
+
+ void start();
+
+ void stop();
+
+ void flush();
+
+ // Must be called after flush().
+ void resumeReceivingInputs();
+
+ void release();
+
+ ByteBuffer getInputBuffer(int index);
+
+ MediaFormat getInputFormat();
+
+ ByteBuffer getOutputBuffer(int index);
+
+ void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags);
+
+ void setBitrate(int bps);
+
+ void queueSecureInputBuffer(
+ int index, int offset, CryptoInfo info, long presentationTimeUs, int flags);
+
+ void releaseOutputBuffer(int index, boolean render);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/AsyncCodecFactory.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/AsyncCodecFactory.java
new file mode 100644
index 0000000000..3295919b91
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/AsyncCodecFactory.java
@@ -0,0 +1,19 @@
+/* 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.media;
+
+import android.os.Build;
+import java.io.IOException;
+
+public final class AsyncCodecFactory {
+ public static AsyncCodec create(final String name) throws IOException {
+ // A bug that getInputBuffer() could fail after flush() then start() wasn't fixed until MR1.
+ // See:
+ // https://android.googlesource.com/platform/frameworks/av/+/d9e0603a1be07dbb347c55050c7d4629ea7492e8
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1
+ ? new LollipopAsyncCodec(name)
+ : new JellyBeanAsyncCodec(name);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/BaseHlsPlayer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/BaseHlsPlayer.java
new file mode 100644
index 0000000000..467d67681c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/BaseHlsPlayer.java
@@ -0,0 +1,104 @@
+/* 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.media;
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+public interface BaseHlsPlayer {
+
+ enum TrackType {
+ UNDEFINED,
+ AUDIO,
+ VIDEO,
+ TEXT,
+ }
+
+ enum ResourceError {
+ BASE(-100),
+ UNKNOWN(-101),
+ PLAYER(-102),
+ UNSUPPORTED(-103);
+
+ private int mNumVal;
+
+ ResourceError(final int numVal) {
+ mNumVal = numVal;
+ }
+
+ public int code() {
+ return mNumVal;
+ }
+ }
+
+ enum DemuxerError {
+ BASE(-200),
+ UNKNOWN(-201),
+ PLAYER(-202),
+ UNSUPPORTED(-203);
+
+ private int mNumVal;
+
+ DemuxerError(final int numVal) {
+ mNumVal = numVal;
+ }
+
+ public int code() {
+ return mNumVal;
+ }
+ }
+
+ interface DemuxerCallbacks {
+ void onInitialized(boolean hasAudio, boolean hasVideo);
+
+ void onError(int errorCode);
+ }
+
+ interface ResourceCallbacks {
+ void onLoad(String mediaUrl);
+
+ void onDataArrived();
+
+ void onError(int errorCode);
+ }
+
+ // Used to identify player instance.
+ int getId();
+
+ // =======================================================================
+ // API for GeckoHLSResourceWrapper
+ // =======================================================================
+ void init(String url, ResourceCallbacks callback);
+
+ boolean isLiveStream();
+
+ // =======================================================================
+ // API for GeckoHLSDemuxerWrapper
+ // =======================================================================
+ void addDemuxerWrapperCallbackListener(DemuxerCallbacks callback);
+
+ ConcurrentLinkedQueue<GeckoHLSSample> getSamples(TrackType trackType, int number);
+
+ long getBufferedPosition();
+
+ int getNumberOfTracks(TrackType trackType);
+
+ GeckoVideoInfo getVideoInfo(int index);
+
+ GeckoAudioInfo getAudioInfo(int index);
+
+ boolean seek(long positionUs);
+
+ long getNextKeyFrameTime();
+
+ void suspend();
+
+ void resume();
+
+ void play();
+
+ void pause();
+
+ void release();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Codec.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Codec.java
new file mode 100644
index 0000000000..eb07f6146c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Codec.java
@@ -0,0 +1,713 @@
+/* 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.media;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.VideoCapabilities;
+import android.media.MediaCodecList;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Surface;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import org.mozilla.gecko.gfx.GeckoSurface;
+
+/* package */ final class Codec extends ICodec.Stub implements IBinder.DeathRecipient {
+ private static final String LOGTAG = "GeckoRemoteCodec";
+ private static final boolean DEBUG = false;
+ public static final String SW_CODEC_PREFIX = "OMX.google.";
+
+ public enum Error {
+ DECODE,
+ FATAL
+ }
+
+ private final class Callbacks implements AsyncCodec.Callbacks {
+ @Override
+ public void onInputBufferAvailable(final AsyncCodec codec, final int index) {
+ mInputProcessor.onBuffer(index);
+ }
+
+ @Override
+ public void onOutputBufferAvailable(
+ final AsyncCodec codec, final int index, final MediaCodec.BufferInfo info) {
+ mOutputProcessor.onBuffer(index, info);
+ }
+
+ @Override
+ public void onError(final AsyncCodec codec, final int error) {
+ reportError(Error.FATAL, new Exception("codec error:" + error));
+ }
+
+ @Override
+ public void onOutputFormatChanged(final AsyncCodec codec, final MediaFormat format) {
+ mOutputProcessor.onFormatChanged(format);
+ }
+ }
+
+ private static final class Input {
+ public final Sample sample;
+ public boolean reported;
+
+ public Input(final Sample sample) {
+ this.sample = sample;
+ }
+ }
+
+ private final class InputProcessor {
+ private boolean mHasInputCapacitySet;
+ private Queue<Integer> mAvailableInputBuffers = new LinkedList<>();
+ private Queue<Sample> mDequeuedSamples = new LinkedList<>();
+ private Queue<Input> mInputSamples = new LinkedList<>();
+ private boolean mStopped;
+
+ private synchronized Sample onAllocate(final int size) {
+ final Sample sample = mSamplePool.obtainInput(size);
+ sample.session = mSession;
+ mDequeuedSamples.add(sample);
+ return sample;
+ }
+
+ private synchronized void onSample(final Sample sample) {
+ if (sample == null) {
+ // Ignore empty input.
+ mSamplePool.recycleInput(mDequeuedSamples.remove());
+ Log.w(LOGTAG, "WARN: empty input sample");
+ return;
+ }
+
+ if (sample.isEOS()) {
+ queueSample(sample);
+ return;
+ }
+
+ if (sample.session >= mSession) {
+ final Sample dequeued = mDequeuedSamples.remove();
+ dequeued.setBufferInfo(sample.info);
+ dequeued.setCryptoInfo(sample.cryptoInfo);
+ queueSample(dequeued);
+ }
+
+ sample.dispose();
+ }
+
+ private void queueSample(final Sample sample) {
+ if (!mInputSamples.offer(new Input(sample))) {
+ reportError(Error.FATAL, new Exception("FAIL: input sample queue is full"));
+ return;
+ }
+
+ try {
+ feedSampleToBuffer();
+ } catch (final Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ }
+
+ private synchronized void onBuffer(final int index) {
+ if (mStopped || !isValidBuffer(index)) {
+ return;
+ }
+
+ if (!mHasInputCapacitySet) {
+ final int capacity = mCodec.getInputBuffer(index).capacity();
+ if (capacity > 0) {
+ mSamplePool.setInputBufferSize(capacity);
+ mHasInputCapacitySet = true;
+ }
+ }
+
+ if (mAvailableInputBuffers.offer(index)) {
+ feedSampleToBuffer();
+ } else {
+ reportError(Error.FATAL, new Exception("FAIL: input buffer queue is full"));
+ }
+ }
+
+ private boolean isValidBuffer(final int index) {
+ try {
+ return mCodec.getInputBuffer(index) != null;
+ } catch (final IllegalStateException e) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "invalid input buffer#" + index, e);
+ }
+ return false;
+ }
+ }
+
+ private void feedSampleToBuffer() {
+ while (!mAvailableInputBuffers.isEmpty() && !mInputSamples.isEmpty()) {
+ final int index = mAvailableInputBuffers.poll();
+ if (!isValidBuffer(index)) {
+ continue;
+ }
+ int len = 0;
+ final Sample sample = mInputSamples.poll().sample;
+ final long pts = sample.info.presentationTimeUs;
+ final int flags = sample.info.flags;
+ final MediaCodec.CryptoInfo cryptoInfo = sample.cryptoInfo;
+ if (!sample.isEOS() && sample.bufferId != Sample.NO_BUFFER) {
+ len = sample.info.size;
+ final ByteBuffer buf = mCodec.getInputBuffer(index);
+ try {
+ mSamplePool
+ .getInputBuffer(sample.bufferId)
+ .writeToByteBuffer(buf, sample.info.offset, len);
+ } catch (final IOException e) {
+ e.printStackTrace();
+ len = 0;
+ }
+ mSamplePool.recycleInput(sample);
+ }
+
+ try {
+ if (cryptoInfo != null && len > 0) {
+ mCodec.queueSecureInputBuffer(index, 0, cryptoInfo, pts, flags);
+ } else {
+ mCodec.queueInputBuffer(index, 0, len, pts, flags);
+ }
+ mCallbacks.onInputQueued(pts);
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ } catch (final Exception e) {
+ reportError(Error.FATAL, e);
+ return;
+ }
+ }
+ reportPendingInputs();
+ }
+
+ private void reportPendingInputs() {
+ try {
+ for (final Input i : mInputSamples) {
+ if (!i.reported) {
+ i.reported = true;
+ mCallbacks.onInputPending(i.sample.info.presentationTimeUs);
+ }
+ }
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private synchronized void reset() {
+ for (final Input i : mInputSamples) {
+ if (!i.sample.isEOS()) {
+ mSamplePool.recycleInput(i.sample);
+ }
+ }
+ mInputSamples.clear();
+
+ for (final Sample s : mDequeuedSamples) {
+ mSamplePool.recycleInput(s);
+ }
+ mDequeuedSamples.clear();
+
+ mAvailableInputBuffers.clear();
+ }
+
+ private synchronized void start() {
+ if (!mStopped) {
+ return;
+ }
+ mStopped = false;
+ }
+
+ private synchronized void stop() {
+ if (mStopped) {
+ return;
+ }
+ mStopped = true;
+ reset();
+ }
+ }
+
+ private static final class Output {
+ public final Sample sample;
+ public final int index;
+
+ public Output(final Sample sample, final int index) {
+ this.sample = sample;
+ this.index = index;
+ }
+ }
+
+ private class OutputProcessor {
+ private final boolean mRenderToSurface;
+ private boolean mHasOutputCapacitySet;
+ private Queue<Output> mSentOutputs = new LinkedList<>();
+ private boolean mStopped;
+
+ private OutputProcessor(final boolean renderToSurface) {
+ mRenderToSurface = renderToSurface;
+ }
+
+ private synchronized void onBuffer(final int index, final MediaCodec.BufferInfo info) {
+ if (mStopped || !isValidBuffer(index)) {
+ return;
+ }
+
+ try {
+ final Sample output = obtainOutputSample(index, info);
+ mSentOutputs.add(new Output(output, index));
+ output.session = mSession;
+ mCallbacks.onOutput(output);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ mCodec.releaseOutputBuffer(index, false);
+ }
+
+ final boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+ if (DEBUG && eos) {
+ Log.d(LOGTAG, "output EOS");
+ }
+ }
+
+ private boolean isValidBuffer(final int index) {
+ try {
+ return (mCodec.getOutputBuffer(index) != null) || mRenderToSurface;
+ } catch (final IllegalStateException e) {
+ if (DEBUG) {
+ Log.e(LOGTAG, "invalid buffer#" + index, e);
+ }
+ return false;
+ }
+ }
+
+ private Sample obtainOutputSample(final int index, final MediaCodec.BufferInfo info) {
+ final Sample sample = mSamplePool.obtainOutput(info);
+
+ if (mRenderToSurface) {
+ return sample;
+ }
+
+ final ByteBuffer output = mCodec.getOutputBuffer(index);
+ if (!mHasOutputCapacitySet) {
+ final int capacity = output.capacity();
+ if (capacity > 0) {
+ mSamplePool.setOutputBufferSize(capacity);
+ mHasOutputCapacitySet = true;
+ }
+ }
+
+ if (info.size > 0) {
+ try {
+ mSamplePool
+ .getOutputBuffer(sample.bufferId)
+ .readFromByteBuffer(output, info.offset, info.size);
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Fail to read output buffer:" + e.getMessage());
+ }
+ }
+
+ return sample;
+ }
+
+ private synchronized void onRelease(final Sample sample, final boolean render) {
+ final Output output = mSentOutputs.poll();
+ if (output != null) {
+ mCodec.releaseOutputBuffer(output.index, render);
+ mSamplePool.recycleOutput(output.sample);
+ } else if (DEBUG) {
+ Log.d(LOGTAG, sample + " already released");
+ }
+
+ sample.dispose();
+ }
+
+ private synchronized void onFormatChanged(final MediaFormat format) {
+ if (mStopped) {
+ return;
+ }
+ try {
+ mCallbacks.onOutputFormatChanged(new FormatParam(format));
+ } catch (final RemoteException re) {
+ // Dead recipient.
+ re.printStackTrace();
+ }
+ }
+
+ private synchronized void reset() {
+ for (final Output o : mSentOutputs) {
+ mCodec.releaseOutputBuffer(o.index, false);
+ mSamplePool.recycleOutput(o.sample);
+ }
+ mSentOutputs.clear();
+ }
+
+ private synchronized void start() {
+ if (!mStopped) {
+ return;
+ }
+ mStopped = false;
+ }
+
+ private synchronized void stop() {
+ if (mStopped) {
+ return;
+ }
+ mStopped = true;
+ reset();
+ }
+ }
+
+ private volatile ICodecCallbacks mCallbacks;
+ private GeckoSurface mSurface;
+ private AsyncCodec mCodec;
+ private InputProcessor mInputProcessor;
+ private OutputProcessor mOutputProcessor;
+ private long mSession;
+ private SamplePool mSamplePool;
+ // Values will be updated after configure called.
+ private volatile boolean mIsAdaptivePlaybackSupported = false;
+ private volatile boolean mIsHardwareAccelerated = false;
+ private boolean mIsTunneledPlaybackSupported = false;
+
+ public synchronized void setCallbacks(final ICodecCallbacks callbacks) throws RemoteException {
+ mCallbacks = callbacks;
+ callbacks.asBinder().linkToDeath(this, 0);
+ }
+
+ // IBinder.DeathRecipient
+ @Override
+ public synchronized void binderDied() {
+ Log.e(LOGTAG, "Callbacks is dead");
+ try {
+ release();
+ } catch (final RemoteException e) {
+ // Nowhere to report the error.
+ }
+ }
+
+ @Override
+ public synchronized boolean configure(
+ final FormatParam format, final GeckoSurface surface, final int flags, final String drmStubId)
+ throws RemoteException {
+ if (mCallbacks == null) {
+ Log.e(LOGTAG, "FAIL: callbacks must be set before calling configure()");
+ return false;
+ }
+
+ if (mCodec != null) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "release existing codec: " + mCodec);
+ }
+ mCodec.release();
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "configure " + this);
+ }
+
+ final MediaFormat fmt = format.asFormat();
+ final String mime = fmt.getString(MediaFormat.KEY_MIME);
+ if (mime == null || mime.isEmpty()) {
+ Log.e(LOGTAG, "invalid MIME type: " + mime);
+ return false;
+ }
+
+ final List<String> found =
+ findMatchingCodecNames(fmt, flags == MediaCodec.CONFIGURE_FLAG_ENCODE);
+ for (final String name : found) {
+ final AsyncCodec codec =
+ configureCodec(
+ name, fmt, surface != null ? surface.getSurface() : null, flags, drmStubId);
+ if (codec == null) {
+ Log.w(LOGTAG, "unable to configure " + name + ". Try next.");
+ continue;
+ }
+ mIsHardwareAccelerated = !name.startsWith(SW_CODEC_PREFIX);
+ mCodec = codec;
+ // Bug 1789846: Check if the Codec provides stride or height values to use.
+ if (flags == MediaCodec.CONFIGURE_FLAG_ENCODE && fmt.containsKey(MediaFormat.KEY_WIDTH)) {
+ final MediaFormat inputFormat = mCodec.getInputFormat();
+ if (inputFormat != null) {
+ if (inputFormat.containsKey(MediaFormat.KEY_STRIDE)) {
+ fmt.setInteger(MediaFormat.KEY_STRIDE, inputFormat.getInteger(MediaFormat.KEY_STRIDE));
+ }
+ if (inputFormat.containsKey(MediaFormat.KEY_SLICE_HEIGHT)) {
+ fmt.setInteger(
+ MediaFormat.KEY_SLICE_HEIGHT, inputFormat.getInteger(MediaFormat.KEY_SLICE_HEIGHT));
+ }
+ }
+ }
+ mInputProcessor = new InputProcessor();
+ final boolean renderToSurface = surface != null;
+ mOutputProcessor = new OutputProcessor(renderToSurface);
+ mSamplePool = new SamplePool(name, renderToSurface);
+ if (renderToSurface) {
+ mIsTunneledPlaybackSupported = mCodec.isTunneledPlaybackSupported(mime);
+ mSurface = surface; // Take ownership of surface.
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, codec.toString() + " created. Render to surface?" + renderToSurface);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private List<String> findMatchingCodecNames(final MediaFormat format, final boolean isEncoder) {
+ final String mimeType = format.getString(MediaFormat.KEY_MIME);
+ // Missing width and height value in format means audio;
+ // Video format should never has 0 width or height.
+ final int width =
+ format.containsKey(MediaFormat.KEY_WIDTH) ? format.getInteger(MediaFormat.KEY_WIDTH) : 0;
+ final int height =
+ format.containsKey(MediaFormat.KEY_HEIGHT) ? format.getInteger(MediaFormat.KEY_HEIGHT) : 0;
+
+ int numCodecs = 0;
+ final List<String> found = new ArrayList<>();
+ try {
+ numCodecs = MediaCodecList.getCodecCount();
+ } catch (final RuntimeException e) {
+ Log.e(LOGTAG, "Failed retrieving codec count finding matching codec names", e);
+ return found;
+ }
+
+ for (int i = 0; i < numCodecs; i++) {
+ final MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ if (info.isEncoder() == !isEncoder) {
+ continue;
+ }
+
+ final String[] types = info.getSupportedTypes();
+ for (final String t : types) {
+ if (!t.equalsIgnoreCase(mimeType)) {
+ continue;
+ }
+ final String name = info.getName();
+ // API 21+ provide a method to query whether supplied size is supported. For
+ // older version, just avoid software video encoders.
+ if (isEncoder && width > 0 && height > 0) {
+ final VideoCapabilities c = info.getCapabilitiesForType(mimeType).getVideoCapabilities();
+ if (c != null && !c.isSizeSupported(width, height)) {
+ if (DEBUG) {
+ Log.d(LOGTAG, name + ": " + width + "x" + height + " not supported");
+ }
+ continue;
+ }
+ }
+
+ found.add(name);
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "found " + (isEncoder ? "encoder:" : "decoder:") + name + " for mime:" + mimeType);
+ }
+ }
+ }
+ return found;
+ }
+
+ private AsyncCodec configureCodec(
+ final String name,
+ final MediaFormat format,
+ final Surface surface,
+ final int flags,
+ final String drmStubId) {
+ try {
+ final AsyncCodec codec = AsyncCodecFactory.create(name);
+ codec.setCallbacks(new Callbacks(), null);
+
+ final MediaCrypto crypto = RemoteMediaDrmBridgeStub.getMediaCrypto(drmStubId);
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "configure mediacodec with crypto(" + (crypto != null) + ") / Id :" + drmStubId);
+ }
+
+ if (surface != null) {
+ setupAdaptivePlayback(codec, format);
+ }
+
+ codec.configure(format, surface, crypto, flags);
+ return codec;
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "codec creation error", e);
+ return null;
+ }
+ }
+
+ private void setupAdaptivePlayback(final AsyncCodec codec, final MediaFormat format) {
+ // Video decoder should config with adaptive playback capability.
+ mIsAdaptivePlaybackSupported =
+ codec.isAdaptivePlaybackSupported(format.getString(MediaFormat.KEY_MIME));
+ if (mIsAdaptivePlaybackSupported) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "codec supports adaptive playback = " + mIsAdaptivePlaybackSupported);
+ }
+ // TODO: may need to find a way to not use hard code to decide the max w/h.
+ format.setInteger(MediaFormat.KEY_MAX_WIDTH, 1920);
+ format.setInteger(MediaFormat.KEY_MAX_HEIGHT, 1080);
+ }
+ }
+
+ @Override
+ public synchronized boolean isAdaptivePlaybackSupported() {
+ return mIsAdaptivePlaybackSupported;
+ }
+
+ @Override
+ public synchronized boolean isHardwareAccelerated() {
+ return mIsHardwareAccelerated;
+ }
+
+ @Override
+ public synchronized boolean isTunneledPlaybackSupported() {
+ return mIsTunneledPlaybackSupported;
+ }
+
+ @Override
+ public synchronized void start() throws RemoteException {
+ if (DEBUG) {
+ Log.d(LOGTAG, "start " + this);
+ }
+ mInputProcessor.start();
+ mOutputProcessor.start();
+ try {
+ mCodec.start();
+ } catch (final Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ }
+
+ private void reportError(final Error error, final Exception e) {
+ if (e != null) {
+ e.printStackTrace();
+ }
+ try {
+ mCallbacks.onError(error == Error.FATAL);
+ } catch (final NullPointerException ne) {
+ // mCallbacks has been disposed by release().
+ } catch (final RemoteException re) {
+ re.printStackTrace();
+ }
+ }
+
+ @Override
+ public synchronized void stop() throws RemoteException {
+ if (DEBUG) {
+ Log.d(LOGTAG, "stop " + this);
+ }
+ try {
+ mInputProcessor.stop();
+ mOutputProcessor.stop();
+
+ mCodec.stop();
+ } catch (final Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ }
+
+ @Override
+ public synchronized void flush() throws RemoteException {
+ if (DEBUG) {
+ Log.d(LOGTAG, "flush " + this);
+ }
+ try {
+ mInputProcessor.stop();
+ mOutputProcessor.stop();
+
+ mCodec.flush();
+ if (DEBUG) {
+ Log.d(LOGTAG, "flushed " + this);
+ }
+ mInputProcessor.start();
+ mOutputProcessor.start();
+ mCodec.resumeReceivingInputs();
+ mSession++;
+ } catch (final Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ }
+
+ @Override
+ public synchronized Sample dequeueInput(final int size) throws RemoteException {
+ try {
+ return mInputProcessor.onAllocate(size);
+ } catch (final Exception e) {
+ // Translate allocation error to remote exception.
+ throw new RemoteException(e.getMessage());
+ }
+ }
+
+ @Override
+ public synchronized SampleBuffer getInputBuffer(final int id) {
+ if (mSamplePool == null) {
+ return null;
+ }
+ return mSamplePool.getInputBuffer(id);
+ }
+
+ @Override
+ public synchronized SampleBuffer getOutputBuffer(final int id) {
+ if (mSamplePool == null) {
+ return null;
+ }
+ return mSamplePool.getOutputBuffer(id);
+ }
+
+ @Override
+ public synchronized void queueInput(final Sample sample) throws RemoteException {
+ try {
+ mInputProcessor.onSample(sample);
+ } catch (final Exception e) {
+ throw new RemoteException(e.getMessage());
+ }
+ }
+
+ @Override
+ public synchronized void setBitrate(final int bps) {
+ try {
+ mCodec.setBitrate(bps);
+ } catch (final Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ }
+
+ @Override
+ public synchronized void releaseOutput(final Sample sample, final boolean render) {
+ try {
+ mOutputProcessor.onRelease(sample, render);
+ } catch (final Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ }
+
+ @Override
+ public synchronized void release() throws RemoteException {
+ if (DEBUG) {
+ Log.d(LOGTAG, "release " + this);
+ }
+ try {
+ // In case Codec.stop() is not called yet.
+ mInputProcessor.stop();
+ mOutputProcessor.stop();
+
+ mCodec.release();
+ } catch (final Exception e) {
+ reportError(Error.FATAL, e);
+ }
+ mCodec = null;
+ mSamplePool.reset();
+ mSamplePool = null;
+ mCallbacks.asBinder().unlinkToDeath(this, 0);
+ mCallbacks = null;
+ if (mSurface != null) {
+ mSurface.release();
+ mSurface = null;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/CodecProxy.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/CodecProxy.java
new file mode 100644
index 0000000000..34bba3e593
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/CodecProxy.java
@@ -0,0 +1,503 @@
+/* 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.media;
+
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CryptoInfo;
+import android.media.MediaFormat;
+import android.os.Build;
+import android.os.DeadObjectException;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+import androidx.annotation.RequiresApi;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.GeckoSurface;
+import org.mozilla.gecko.mozglue.JNIObject;
+
+// Proxy class of ICodec binder.
+public final class CodecProxy {
+ private static final String LOGTAG = "GeckoRemoteCodecProxy";
+ private static final boolean DEBUG = false;
+ @WrapForJNI private static final long INVALID_SESSION = -1;
+
+ private ICodec mRemote;
+ private long mSession;
+ private boolean mIsEncoder;
+ private FormatParam mFormat;
+ private GeckoSurface mOutputSurface;
+ private CallbacksForwarder mCallbacks;
+ private String mRemoteDrmStubId;
+ private Queue<Sample> mSurfaceOutputs = new ConcurrentLinkedQueue<>();
+ private boolean mFlushed = true;
+
+ private SparseArray<SampleBuffer> mInputBuffers = new SparseArray<>();
+ private SparseArray<SampleBuffer> mOutputBuffers = new SparseArray<>();
+
+ public interface Callbacks {
+ void onInputStatus(long timestamp, boolean processed);
+
+ void onOutputFormatChanged(MediaFormat format);
+
+ void onOutput(Sample output, SampleBuffer buffer);
+
+ void onError(boolean fatal);
+ }
+
+ @WrapForJNI
+ public static class NativeCallbacks extends JNIObject implements Callbacks {
+ public native void onInputStatus(long timestamp, boolean processed);
+
+ public native void onOutputFormatChanged(MediaFormat format);
+
+ public native void onOutput(Sample output, SampleBuffer buffer);
+
+ public native void onError(boolean fatal);
+
+ @Override // JNIObject
+ protected void disposeNative() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private class CallbacksForwarder extends ICodecCallbacks.Stub {
+ private final Callbacks mCallbacks;
+ private boolean mCodecProxyReleased;
+
+ CallbacksForwarder(final Callbacks callbacks) {
+ mCallbacks = callbacks;
+ }
+
+ @Override
+ public synchronized void onInputQueued(final long timestamp) throws RemoteException {
+ if (!mCodecProxyReleased) {
+ mCallbacks.onInputStatus(timestamp, true /* processed */);
+ }
+ }
+
+ @Override
+ public synchronized void onInputPending(final long timestamp) throws RemoteException {
+ if (!mCodecProxyReleased) {
+ mCallbacks.onInputStatus(timestamp, false /* processed */);
+ }
+ }
+
+ @Override
+ public synchronized void onOutputFormatChanged(final FormatParam format)
+ throws RemoteException {
+ if (!mCodecProxyReleased) {
+ mCallbacks.onOutputFormatChanged(format.asFormat());
+ }
+ }
+
+ @Override
+ public synchronized void onOutput(final Sample sample) throws RemoteException {
+ if (mCodecProxyReleased) {
+ sample.dispose();
+ return;
+ }
+
+ final SampleBuffer buffer = CodecProxy.this.getOutputBuffer(sample.bufferId);
+ if (mOutputSurface != null) {
+ // Don't render to surface just yet. Callback will make that happen when it's time.
+ mSurfaceOutputs.offer(sample);
+ } else if (buffer == null) {
+ // Buffer with given ID has been flushed.
+ sample.dispose();
+ return;
+ }
+ mCallbacks.onOutput(sample, buffer);
+ }
+
+ @Override
+ public void onError(final boolean fatal) throws RemoteException {
+ reportError(fatal);
+ }
+
+ private synchronized void reportError(final boolean fatal) {
+ if (!mCodecProxyReleased) {
+ mCallbacks.onError(fatal);
+ }
+ }
+
+ private synchronized void setCodecProxyReleased() {
+ mCodecProxyReleased = true;
+ }
+ }
+
+ @WrapForJNI
+ public int GetInputFormatStride() {
+ if (mFormat.asFormat().containsKey(MediaFormat.KEY_STRIDE)) {
+ return mFormat.asFormat().getInteger(MediaFormat.KEY_STRIDE);
+ }
+ return 0;
+ }
+
+ @WrapForJNI
+ public int GetInputFormatYPlaneHeight() {
+ if (mFormat.asFormat().containsKey(MediaFormat.KEY_SLICE_HEIGHT)) {
+ return mFormat.asFormat().getInteger(MediaFormat.KEY_SLICE_HEIGHT);
+ }
+ return 0;
+ }
+
+ @WrapForJNI
+ public static CodecProxy create(
+ final boolean isEncoder,
+ final MediaFormat format,
+ final GeckoSurface surface,
+ final Callbacks callbacks,
+ final String drmStubId) {
+ return RemoteManager.getInstance()
+ .createCodec(isEncoder, format, surface, callbacks, drmStubId);
+ }
+
+ public static CodecProxy createCodecProxy(
+ final boolean isEncoder,
+ final MediaFormat format,
+ final GeckoSurface surface,
+ final Callbacks callbacks,
+ final String drmStubId) {
+ return new CodecProxy(isEncoder, format, surface, callbacks, drmStubId);
+ }
+
+ private CodecProxy(
+ final boolean isEncoder,
+ final MediaFormat format,
+ final GeckoSurface surface,
+ final Callbacks callbacks,
+ final String drmStubId) {
+ mIsEncoder = isEncoder;
+ mFormat = new FormatParam(format);
+ mOutputSurface = surface;
+ mRemoteDrmStubId = drmStubId;
+ mCallbacks = new CallbacksForwarder(callbacks);
+ }
+
+ boolean init(final ICodec remote) {
+ try {
+ remote.setCallbacks(mCallbacks);
+ if (!remote.configure(
+ mFormat,
+ mOutputSurface,
+ mIsEncoder ? MediaCodec.CONFIGURE_FLAG_ENCODE : 0,
+ mRemoteDrmStubId)) {
+ return false;
+ }
+ remote.start();
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ mRemote = remote;
+ return true;
+ }
+
+ boolean deinit() {
+ try {
+ mRemote.stop();
+ mRemote.release();
+ mRemote = null;
+ return true;
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @WrapForJNI
+ public synchronized boolean isAdaptivePlaybackSupported() {
+ if (mRemote == null) {
+ Log.e(LOGTAG, "cannot check isAdaptivePlaybackSupported with an ended codec");
+ return false;
+ }
+ try {
+ return mRemote.isAdaptivePlaybackSupported();
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @WrapForJNI
+ public synchronized boolean isHardwareAccelerated() {
+ if (mRemote == null) {
+ Log.e(LOGTAG, "cannot check isHardwareAccelerated with an ended codec");
+ return false;
+ }
+ try {
+ return mRemote.isHardwareAccelerated();
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @WrapForJNI
+ public synchronized boolean isTunneledPlaybackSupported() {
+ if (mRemote == null) {
+ Log.e(LOGTAG, "cannot check isTunneledPlaybackSupported with an ended codec");
+ return false;
+ }
+ try {
+ return mRemote.isTunneledPlaybackSupported();
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @WrapForJNI
+ public synchronized long input(
+ final ByteBuffer bytes, final BufferInfo info, final CryptoInfo cryptoInfo) {
+ if (mRemote == null) {
+ Log.e(LOGTAG, "cannot send input to an ended codec");
+ return INVALID_SESSION;
+ }
+
+ final boolean eos = info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+
+ if (eos) {
+ return sendInput(Sample.EOS);
+ }
+
+ try {
+ final Sample s = mRemote.dequeueInput(info.size);
+ fillInputBuffer(s.bufferId, bytes, info.offset, info.size);
+ mSession = s.session;
+ return sendInput(s.set(info, cryptoInfo));
+ } catch (final RemoteException | NullPointerException e) {
+ Log.e(LOGTAG, "fail to dequeue input buffer", e);
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "fail to copy input data.", e);
+ // Balance dequeue/queue.
+ sendInput(null);
+ }
+ return INVALID_SESSION;
+ }
+
+ private void fillInputBuffer(
+ final int bufferId, final ByteBuffer bytes, final int offset, final int size)
+ throws RemoteException, IOException {
+ if (bytes == null || size == 0) {
+ Log.w(LOGTAG, "empty input");
+ return;
+ }
+
+ SampleBuffer buffer = mInputBuffers.get(bufferId);
+ if (buffer == null) {
+ buffer = mRemote.getInputBuffer(bufferId);
+ if (buffer != null) {
+ mInputBuffers.put(bufferId, buffer);
+ }
+ }
+
+ if (buffer.capacity() < size) {
+ final IOException e =
+ new IOException("data larger than capacity: " + size + " > " + buffer.capacity());
+ Log.e(LOGTAG, "cannot fill input.", e);
+ throw e;
+ }
+
+ buffer.readFromByteBuffer(bytes, offset, size);
+ }
+
+ private long sendInput(final Sample sample) {
+ try {
+ mRemote.queueInput(sample);
+ if (sample != null) {
+ sample.dispose();
+ mFlushed = false;
+ }
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "fail to queue input:" + sample, e);
+ return INVALID_SESSION;
+ }
+ return mSession;
+ }
+
+ @WrapForJNI
+ public synchronized boolean flush() {
+ if (mFlushed) {
+ return true;
+ }
+ if (mRemote == null) {
+ Log.e(LOGTAG, "cannot flush an ended codec");
+ return false;
+ }
+ try {
+ if (DEBUG) {
+ Log.d(LOGTAG, "flush " + this);
+ }
+ resetBuffers();
+ mRemote.flush();
+ mFlushed = true;
+ } catch (final DeadObjectException e) {
+ return false;
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+
+ private void resetBuffers() {
+ for (int i = 0; i < mInputBuffers.size(); ++i) {
+ mInputBuffers.valueAt(i).dispose();
+ }
+ mInputBuffers.clear();
+ for (int i = 0; i < mOutputBuffers.size(); ++i) {
+ mOutputBuffers.valueAt(i).dispose();
+ }
+ mOutputBuffers.clear();
+ }
+
+ @WrapForJNI
+ public boolean release() {
+ mCallbacks.setCodecProxyReleased();
+ synchronized (this) {
+ if (mRemote == null) {
+ Log.w(LOGTAG, "codec already ended");
+ return true;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "release " + this);
+ }
+
+ if (!mSurfaceOutputs.isEmpty()) {
+ // Flushing output buffers to surface may cause some frames to be skipped and
+ // should not happen unless caller release codec before processing all buffers.
+ Log.w(LOGTAG, "release codec when " + mSurfaceOutputs.size() + " output buffers unhandled");
+ try {
+ for (final Sample s : mSurfaceOutputs) {
+ mRemote.releaseOutput(s, true);
+ }
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ }
+ mSurfaceOutputs.clear();
+ }
+
+ resetBuffers();
+
+ try {
+ RemoteManager.getInstance().releaseCodec(this);
+ } catch (final DeadObjectException e) {
+ return false;
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+ }
+
+ @WrapForJNI
+ public synchronized boolean setBitrate(final int bps) {
+ if (!mIsEncoder) {
+ Log.w(LOGTAG, "this api is encoder-only");
+ return false;
+ }
+
+ if (mRemote == null) {
+ Log.w(LOGTAG, "codec already ended");
+ return true;
+ }
+
+ try {
+ mRemote.setBitrate(bps);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "remote fail to set rates:" + bps);
+ e.printStackTrace();
+ }
+ return true;
+ }
+
+ @WrapForJNI
+ public synchronized boolean releaseOutput(final Sample sample, final boolean render) {
+ if (mOutputSurface != null) {
+ if (!mSurfaceOutputs.remove(sample)) {
+ if (mRemote != null) Log.w(LOGTAG, "already released: " + sample);
+ return true;
+ }
+
+ if (DEBUG && !render) {
+ Log.d(LOGTAG, "drop output:" + sample.info.presentationTimeUs);
+ }
+ }
+
+ if (mRemote == null) {
+ Log.w(LOGTAG, "codec already ended");
+ sample.dispose();
+ return true;
+ }
+
+ try {
+ mRemote.releaseOutput(sample, render);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "remote fail to release output:" + sample.info.presentationTimeUs);
+ e.printStackTrace();
+ }
+ sample.dispose();
+
+ return true;
+ }
+
+ /* package */ void reportError(final boolean fatal) {
+ mCallbacks.reportError(fatal);
+ }
+
+ private synchronized SampleBuffer getOutputBuffer(final int id) {
+ if (mRemote == null) {
+ Log.e(LOGTAG, "cannot get buffer#" + id + " from an ended codec");
+ return null;
+ }
+
+ if (mOutputSurface != null || id == Sample.NO_BUFFER) {
+ return null;
+ }
+
+ SampleBuffer buffer = mOutputBuffers.get(id);
+ if (buffer != null) {
+ return buffer;
+ }
+
+ try {
+ buffer = mRemote.getOutputBuffer(id);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "cannot get buffer#" + id, e);
+ return null;
+ }
+ if (buffer != null) {
+ mOutputBuffers.put(id, buffer);
+ }
+
+ return buffer;
+ }
+
+ @WrapForJNI
+ public static boolean supportsCBCS() {
+ // Android N/API-24 supports CBCS but there seems to be a bug.
+ // See https://github.com/google/ExoPlayer/issues/4022
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N_MR1)
+ @WrapForJNI
+ public static boolean setCryptoPatternIfNeeded(
+ final CryptoInfo info, final int blocksToEncrypt, final int blocksToSkip) {
+ if (supportsCBCS() && (blocksToEncrypt > 0 || blocksToSkip > 0)) {
+ info.setPattern(new CryptoInfo.Pattern(blocksToEncrypt, blocksToSkip));
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/FormatParam.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/FormatParam.java
new file mode 100644
index 0000000000..99287974f5
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/FormatParam.java
@@ -0,0 +1,199 @@
+/* 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.media;
+
+import android.media.MediaFormat;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import java.nio.ByteBuffer;
+
+/**
+ * A wrapper to make {@link MediaFormat} parcelable. Supports following keys:
+ *
+ * <ul>
+ * <li>{@link MediaFormat#KEY_MIME}
+ * <li>{@link MediaFormat#KEY_WIDTH}
+ * <li>{@link MediaFormat#KEY_HEIGHT}
+ * <li>{@link MediaFormat#KEY_CHANNEL_COUNT}
+ * <li>{@link MediaFormat#KEY_SAMPLE_RATE}
+ * <li>{@link MediaFormat#KEY_BIT_RATE}
+ * <li>{@link MediaFormat#KEY_BITRATE_MODE}
+ * <li>{@link MediaFormat#KEY_COLOR_FORMAT}
+ * <li>{@link MediaFormat#KEY_FRAME_RATE}
+ * <li>{@link MediaFormat#KEY_I_FRAME_INTERVAL}
+ * <li>{@link MediaFormat#KEY_STRIDE}
+ * <li>{@link MediaFormat#KEY_SLICE_HEIGHT}
+ * <li>{@link MediaFormat#KEY_COLOR_RANGE
+ * <li>{@link MediaFormat#KEY_COLOR_STANDARD}
+ * <li>"csd-0"
+ * <li>"csd-1"
+ * </ul>
+ */
+public final class FormatParam implements Parcelable {
+ // Keys for codec specific config bits not exposed in {@link MediaFormat}.
+ private static final String KEY_CONFIG_0 = "csd-0";
+ private static final String KEY_CONFIG_1 = "csd-1";
+
+ private MediaFormat mFormat;
+
+ public MediaFormat asFormat() {
+ return mFormat;
+ }
+
+ public FormatParam(final MediaFormat format) {
+ mFormat = format;
+ }
+
+ protected FormatParam(final Parcel in) {
+ mFormat = new MediaFormat();
+ readFromParcel(in);
+ }
+
+ public static final Creator<FormatParam> CREATOR =
+ new Creator<FormatParam>() {
+ @Override
+ public FormatParam createFromParcel(final Parcel in) {
+ return new FormatParam(in);
+ }
+
+ @Override
+ public FormatParam[] newArray(final int size) {
+ return new FormatParam[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public void readFromParcel(final Parcel in) {
+ final Bundle bundle = in.readBundle();
+ fromBundle(bundle);
+ }
+
+ private void fromBundle(final Bundle bundle) {
+ if (bundle.containsKey(MediaFormat.KEY_MIME)) {
+ mFormat.setString(MediaFormat.KEY_MIME, bundle.getString(MediaFormat.KEY_MIME));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_WIDTH)) {
+ mFormat.setInteger(MediaFormat.KEY_WIDTH, bundle.getInt(MediaFormat.KEY_WIDTH));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_HEIGHT)) {
+ mFormat.setInteger(MediaFormat.KEY_HEIGHT, bundle.getInt(MediaFormat.KEY_HEIGHT));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
+ mFormat.setInteger(
+ MediaFormat.KEY_CHANNEL_COUNT, bundle.getInt(MediaFormat.KEY_CHANNEL_COUNT));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
+ mFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, bundle.getInt(MediaFormat.KEY_SAMPLE_RATE));
+ }
+ if (bundle.containsKey(KEY_CONFIG_0)) {
+ mFormat.setByteBuffer(KEY_CONFIG_0, ByteBuffer.wrap(bundle.getByteArray(KEY_CONFIG_0)));
+ }
+ if (bundle.containsKey(KEY_CONFIG_1)) {
+ mFormat.setByteBuffer(KEY_CONFIG_1, ByteBuffer.wrap(bundle.getByteArray((KEY_CONFIG_1))));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_BIT_RATE)) {
+ mFormat.setInteger(MediaFormat.KEY_BIT_RATE, bundle.getInt(MediaFormat.KEY_BIT_RATE));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_BITRATE_MODE)) {
+ mFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, bundle.getInt(MediaFormat.KEY_BITRATE_MODE));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
+ mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, bundle.getInt(MediaFormat.KEY_COLOR_FORMAT));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_FRAME_RATE)) {
+ mFormat.setInteger(MediaFormat.KEY_FRAME_RATE, bundle.getInt(MediaFormat.KEY_FRAME_RATE));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_I_FRAME_INTERVAL)) {
+ mFormat.setInteger(
+ MediaFormat.KEY_I_FRAME_INTERVAL, bundle.getInt(MediaFormat.KEY_I_FRAME_INTERVAL));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_STRIDE)) {
+ mFormat.setInteger(MediaFormat.KEY_STRIDE, bundle.getInt(MediaFormat.KEY_STRIDE));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_SLICE_HEIGHT)) {
+ mFormat.setInteger(MediaFormat.KEY_SLICE_HEIGHT, bundle.getInt(MediaFormat.KEY_SLICE_HEIGHT));
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ if (bundle.containsKey(MediaFormat.KEY_COLOR_RANGE)) {
+ mFormat.setInteger(MediaFormat.KEY_COLOR_RANGE, bundle.getInt(MediaFormat.KEY_COLOR_RANGE));
+ }
+ if (bundle.containsKey(MediaFormat.KEY_COLOR_STANDARD)) {
+ mFormat.setInteger(
+ MediaFormat.KEY_COLOR_STANDARD, bundle.getInt(MediaFormat.KEY_COLOR_STANDARD));
+ }
+ }
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int flags) {
+ dest.writeBundle(toBundle());
+ }
+
+ private Bundle toBundle() {
+ final Bundle bundle = new Bundle();
+ if (mFormat.containsKey(MediaFormat.KEY_MIME)) {
+ bundle.putString(MediaFormat.KEY_MIME, mFormat.getString(MediaFormat.KEY_MIME));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_WIDTH)) {
+ bundle.putInt(MediaFormat.KEY_WIDTH, mFormat.getInteger(MediaFormat.KEY_WIDTH));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
+ bundle.putInt(MediaFormat.KEY_HEIGHT, mFormat.getInteger(MediaFormat.KEY_HEIGHT));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
+ bundle.putInt(
+ MediaFormat.KEY_CHANNEL_COUNT, mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
+ bundle.putInt(MediaFormat.KEY_SAMPLE_RATE, mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE));
+ }
+ if (mFormat.containsKey(KEY_CONFIG_0)) {
+ final ByteBuffer bytes = mFormat.getByteBuffer(KEY_CONFIG_0);
+ bundle.putByteArray(KEY_CONFIG_0, Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity()));
+ }
+ if (mFormat.containsKey(KEY_CONFIG_1)) {
+ final ByteBuffer bytes = mFormat.getByteBuffer(KEY_CONFIG_1);
+ bundle.putByteArray(KEY_CONFIG_1, Sample.byteArrayFromBuffer(bytes, 0, bytes.capacity()));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_BIT_RATE)) {
+ bundle.putInt(MediaFormat.KEY_BIT_RATE, mFormat.getInteger(MediaFormat.KEY_BIT_RATE));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_BITRATE_MODE)) {
+ bundle.putInt(MediaFormat.KEY_BITRATE_MODE, mFormat.getInteger(MediaFormat.KEY_BITRATE_MODE));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
+ bundle.putInt(MediaFormat.KEY_COLOR_FORMAT, mFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
+ bundle.putInt(MediaFormat.KEY_FRAME_RATE, mFormat.getInteger(MediaFormat.KEY_FRAME_RATE));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_I_FRAME_INTERVAL)) {
+ bundle.putInt(
+ MediaFormat.KEY_I_FRAME_INTERVAL, mFormat.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_STRIDE)) {
+ bundle.putInt(MediaFormat.KEY_STRIDE, mFormat.getInteger(MediaFormat.KEY_STRIDE));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_SLICE_HEIGHT)) {
+ bundle.putInt(MediaFormat.KEY_SLICE_HEIGHT, mFormat.getInteger(MediaFormat.KEY_SLICE_HEIGHT));
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ if (mFormat.containsKey(MediaFormat.KEY_COLOR_RANGE)) {
+ bundle.putInt(MediaFormat.KEY_COLOR_RANGE, mFormat.getInteger(MediaFormat.KEY_COLOR_RANGE));
+ }
+ if (mFormat.containsKey(MediaFormat.KEY_COLOR_STANDARD)) {
+ bundle.putInt(
+ MediaFormat.KEY_COLOR_STANDARD, mFormat.getInteger(MediaFormat.KEY_COLOR_STANDARD));
+ }
+ }
+ return bundle;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoAudioInfo.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoAudioInfo.java
new file mode 100644
index 0000000000..6418375a57
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoAudioInfo.java
@@ -0,0 +1,36 @@
+/* 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.media;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+// A subset of the class AudioInfo in dom/media/MediaInfo.h
+@WrapForJNI
+public final class GeckoAudioInfo {
+ public final byte[] codecSpecificData;
+ public final int rate;
+ public final int channels;
+ public final int bitDepth;
+ public final int profile;
+ public final long duration;
+ public final String mimeType;
+
+ public GeckoAudioInfo(
+ final int rate,
+ final int channels,
+ final int bitDepth,
+ final int profile,
+ final long duration,
+ final String mimeType,
+ final byte[] codecSpecificData) {
+ this.rate = rate;
+ this.channels = channels;
+ this.bitDepth = bitDepth;
+ this.profile = profile;
+ this.duration = duration;
+ this.mimeType = mimeType;
+ this.codecSpecificData = codecSpecificData;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSDemuxerWrapper.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSDemuxerWrapper.java
new file mode 100644
index 0000000000..36c714ba72
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSDemuxerWrapper.java
@@ -0,0 +1,164 @@
+/* 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.media;
+
+import android.util.Log;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.geckoview.BuildConfig;
+
+public final class GeckoHLSDemuxerWrapper {
+ private static final String LOGTAG = "GeckoHLSDemuxerWrapper";
+ private static final boolean DEBUG = !BuildConfig.MOZILLA_OFFICIAL;
+
+ // NOTE : These TRACK definitions should be synced with Gecko.
+ public enum TrackType {
+ UNDEFINED(0),
+ AUDIO(1),
+ VIDEO(2),
+ TEXT(3);
+ private int mType;
+
+ TrackType(final int type) {
+ mType = type;
+ }
+
+ public int value() {
+ return mType;
+ }
+ }
+
+ private BaseHlsPlayer mPlayer = null;
+
+ public static class Callbacks extends JNIObject implements BaseHlsPlayer.DemuxerCallbacks {
+ @WrapForJNI(calledFrom = "gecko")
+ Callbacks() {}
+
+ @Override
+ @WrapForJNI
+ public native void onInitialized(boolean hasAudio, boolean hasVideo);
+
+ @Override
+ @WrapForJNI
+ public native void onError(int errorCode);
+
+ @Override // JNIObject
+ protected void disposeNative() {
+ throw new UnsupportedOperationException();
+ }
+ } // Callbacks
+
+ private static void assertTrue(final boolean condition) {
+ if (DEBUG && !condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ private BaseHlsPlayer.TrackType getPlayerTrackType(final int trackType) {
+ if (trackType == TrackType.AUDIO.value()) {
+ return BaseHlsPlayer.TrackType.AUDIO;
+ } else if (trackType == TrackType.VIDEO.value()) {
+ return BaseHlsPlayer.TrackType.VIDEO;
+ } else if (trackType == TrackType.TEXT.value()) {
+ return BaseHlsPlayer.TrackType.TEXT;
+ }
+ return BaseHlsPlayer.TrackType.UNDEFINED;
+ }
+
+ @WrapForJNI
+ public long getBuffered() {
+ assertTrue(mPlayer != null);
+ return mPlayer.getBufferedPosition();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static GeckoHLSDemuxerWrapper create(
+ final int id, final BaseHlsPlayer.DemuxerCallbacks callback) {
+ return new GeckoHLSDemuxerWrapper(id, callback);
+ }
+
+ @WrapForJNI
+ public int getNumberOfTracks(final int trackType) {
+ assertTrue(mPlayer != null);
+ final int tracks = mPlayer.getNumberOfTracks(getPlayerTrackType(trackType));
+ if (DEBUG) Log.d(LOGTAG, "[GetNumberOfTracks] type : " + trackType + ", num = " + tracks);
+ return tracks;
+ }
+
+ @WrapForJNI
+ public GeckoAudioInfo getAudioInfo(final int index) {
+ assertTrue(mPlayer != null);
+ if (DEBUG) Log.d(LOGTAG, "[getAudioInfo] formatIndex : " + index);
+ return mPlayer.getAudioInfo(index);
+ }
+
+ @WrapForJNI
+ public GeckoVideoInfo getVideoInfo(final int index) {
+ assertTrue(mPlayer != null);
+ if (DEBUG) Log.d(LOGTAG, "[getVideoInfo] formatIndex : " + index);
+ return mPlayer.getVideoInfo(index);
+ }
+
+ @WrapForJNI
+ public boolean seek(final long seekTime) {
+ // seekTime : microseconds.
+ assertTrue(mPlayer != null);
+ if (DEBUG) Log.d(LOGTAG, "seek : " + seekTime + " (Us)");
+ return mPlayer.seek(seekTime);
+ }
+
+ GeckoHLSDemuxerWrapper(final int id, final BaseHlsPlayer.DemuxerCallbacks callback) {
+ if (DEBUG) Log.d(LOGTAG, "Constructing GeckoHLSDemuxerWrapper ...");
+ assertTrue(callback != null);
+ try {
+ mPlayer = GeckoPlayerFactory.getPlayer(id);
+ if (mPlayer != null) {
+ mPlayer.addDemuxerWrapperCallbackListener(callback);
+ }
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Constructing GeckoHLSDemuxerWrapper ... error", e);
+ callback.onError(BaseHlsPlayer.DemuxerError.UNKNOWN.code());
+ }
+ }
+
+ @WrapForJNI
+ private GeckoHLSSample[] getSamples(final int mediaType, final int number) {
+ assertTrue(mPlayer != null);
+ ConcurrentLinkedQueue<GeckoHLSSample> samples = null;
+ // getA/VSamples will always return a non-null instance.
+ samples = mPlayer.getSamples(getPlayerTrackType(mediaType), number);
+ assertTrue(samples.size() <= number);
+ return samples.toArray(new GeckoHLSSample[samples.size()]);
+ }
+
+ @WrapForJNI
+ private long getNextKeyFrameTime() {
+ assertTrue(mPlayer != null);
+ return mPlayer.getNextKeyFrameTime();
+ }
+
+ @WrapForJNI
+ private boolean isLiveStream() {
+ assertTrue(mPlayer != null);
+ return mPlayer.isLiveStream();
+ }
+
+ @WrapForJNI // Called when native object is destroyed.
+ private void destroy() {
+ if (DEBUG) Log.d(LOGTAG, "destroy!! Native object is destroyed.");
+ if (mPlayer != null) {
+ release();
+ }
+ }
+
+ private void release() {
+ assertTrue(mPlayer != null);
+ if (DEBUG) Log.d(LOGTAG, "release BaseHlsPlayer...");
+ GeckoPlayerFactory.removePlayer(mPlayer);
+ mPlayer.release();
+ mPlayer = null;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSResourceWrapper.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSResourceWrapper.java
new file mode 100644
index 0000000000..c21789fdd0
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSResourceWrapper.java
@@ -0,0 +1,119 @@
+/* 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.media;
+
+import android.util.Log;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.geckoview.BuildConfig;
+
+public class GeckoHLSResourceWrapper {
+ private static final String LOGTAG = "GeckoHLSResourceWrapper";
+ private static final boolean DEBUG = !BuildConfig.MOZILLA_OFFICIAL;
+ private BaseHlsPlayer mPlayer = null;
+ private boolean mDestroy = false;
+
+ public static class Callbacks extends JNIObject implements BaseHlsPlayer.ResourceCallbacks {
+ @WrapForJNI(calledFrom = "gecko")
+ Callbacks() {}
+
+ @Override
+ @WrapForJNI
+ public native void onLoad(String mediaUrl);
+
+ @Override
+ @WrapForJNI
+ public native void onDataArrived();
+
+ @Override
+ @WrapForJNI
+ public native void onError(int errorCode);
+
+ @Override // JNIObject
+ protected void disposeNative() {
+ throw new UnsupportedOperationException();
+ }
+ } // Callbacks
+
+ private GeckoHLSResourceWrapper(
+ final String url, final BaseHlsPlayer.ResourceCallbacks callback) {
+ if (DEBUG) Log.d(LOGTAG, "GeckoHLSResourceWrapper created with url = " + url);
+ assertTrue(callback != null);
+
+ mPlayer = GeckoPlayerFactory.getPlayer();
+ try {
+ mPlayer.init(url, callback);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Failed to create GeckoHlsResourceWrapper !", e);
+ callback.onError(BaseHlsPlayer.ResourceError.UNKNOWN.code());
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static GeckoHLSResourceWrapper create(
+ final String url, final BaseHlsPlayer.ResourceCallbacks callback) {
+ return new GeckoHLSResourceWrapper(url, callback);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public int getPlayerId() {
+ // GeckoHLSResourceWrapper should always be created before others
+ assertTrue(!mDestroy);
+ assertTrue(mPlayer != null);
+ return mPlayer.getId();
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public void suspend() {
+ if (DEBUG) Log.d(LOGTAG, "GeckoHLSResourceWrapper suspend");
+ if (mPlayer != null) {
+ mPlayer.suspend();
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public void resume() {
+ if (DEBUG) Log.d(LOGTAG, "GeckoHLSResourceWrapper resume");
+ if (mPlayer != null) {
+ mPlayer.resume();
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public void play() {
+ if (DEBUG) Log.d(LOGTAG, "GeckoHLSResourceWrapper mediaelement played");
+ if (mPlayer != null) {
+ mPlayer.play();
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public void pause() {
+ if (DEBUG) Log.d(LOGTAG, "GeckoHLSResourceWrapper mediaelement paused");
+ if (mPlayer != null) {
+ mPlayer.pause();
+ }
+ }
+
+ private static void assertTrue(final boolean condition) {
+ if (DEBUG && !condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ @WrapForJNI // Called when native object is mDestroy.
+ private void destroy() {
+ if (DEBUG) Log.d(LOGTAG, "destroy!! Native object is destroyed.");
+ if (mDestroy) {
+ return;
+ }
+ mDestroy = true;
+ if (mPlayer != null) {
+ GeckoPlayerFactory.removePlayer(mPlayer);
+ mPlayer.release();
+ mPlayer = null;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSSample.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSSample.java
new file mode 100644
index 0000000000..d2ab76a13d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHLSSample.java
@@ -0,0 +1,93 @@
+/* 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.media;
+
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CryptoInfo;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public final class GeckoHLSSample {
+ public static final GeckoHLSSample EOS;
+
+ static {
+ final BufferInfo eosInfo = new BufferInfo();
+ eosInfo.set(0, 0, Long.MIN_VALUE, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ EOS = new GeckoHLSSample(null, eosInfo, null, 0);
+ }
+
+ // Indicate the index of format which is used by this sample.
+ @WrapForJNI public final int formatIndex;
+
+ @WrapForJNI public long duration;
+
+ @WrapForJNI public final BufferInfo info;
+
+ @WrapForJNI public final CryptoInfo cryptoInfo;
+
+ private ByteBuffer mBuffer = null;
+
+ @WrapForJNI
+ public void writeToByteBuffer(final ByteBuffer dest) throws IOException {
+ if (mBuffer != null && dest != null && info.size > 0) {
+ dest.put(mBuffer);
+ }
+ }
+
+ @WrapForJNI
+ public boolean isEOS() {
+ return (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+ }
+
+ @WrapForJNI
+ public boolean isKeyFrame() {
+ return (info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
+ }
+
+ public static GeckoHLSSample create(
+ final ByteBuffer src,
+ final BufferInfo info,
+ final CryptoInfo cryptoInfo,
+ final int formatIndex) {
+ return new GeckoHLSSample(src, info, cryptoInfo, formatIndex);
+ }
+
+ private GeckoHLSSample(
+ final ByteBuffer buffer,
+ final BufferInfo info,
+ final CryptoInfo cryptoInfo,
+ final int formatIndex) {
+ this.formatIndex = formatIndex;
+ duration = Long.MAX_VALUE;
+ this.mBuffer = buffer;
+ this.info = info;
+ this.cryptoInfo = cryptoInfo;
+ }
+
+ @Override
+ public String toString() {
+ if (isEOS()) {
+ return "EOS GeckoHLSSample";
+ }
+
+ final StringBuilder str = new StringBuilder();
+ str.append("{ info=")
+ .append("{ offset=")
+ .append(info.offset)
+ .append(", size=")
+ .append(info.size)
+ .append(", pts=")
+ .append(info.presentationTimeUs)
+ .append(", duration=")
+ .append(duration)
+ .append(", flags=")
+ .append(Integer.toHexString(info.flags))
+ .append(" }")
+ .append(" }");
+ return str.toString();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsAudioRenderer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsAudioRenderer.java
new file mode 100644
index 0000000000..d60f7c1ccd
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsAudioRenderer.java
@@ -0,0 +1,167 @@
+/* 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.media;
+
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CryptoInfo;
+import android.util.Log;
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.mozilla.geckoview.BuildConfig;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+
+public class GeckoHlsAudioRenderer extends GeckoHlsRendererBase {
+ public GeckoHlsAudioRenderer(final GeckoHlsPlayer.ComponentEventDispatcher eventDispatcher) {
+ super(C.TRACK_TYPE_AUDIO, eventDispatcher);
+ LOGTAG = getClass().getSimpleName();
+ DEBUG = !BuildConfig.MOZILLA_OFFICIAL;
+ }
+
+ @Override
+ public final int supportsFormat(final Format format) {
+ /*
+ * FORMAT_EXCEEDS_CAPABILITIES : The Renderer is capable of rendering
+ * formats with the same mime type, but
+ * the properties of the format exceed
+ * the renderer's capability.
+ * FORMAT_UNSUPPORTED_SUBTYPE : The Renderer is a general purpose
+ * renderer for formats of the same
+ * top-level type, but is not capable of
+ * rendering the format or any other format
+ * with the same mime type because the
+ * sub-type is not supported.
+ * FORMAT_UNSUPPORTED_TYPE : The Renderer is not capable of rendering
+ * the format, either because it does not support
+ * the format's top-level type, or because it's
+ * a specialized renderer for a different mime type.
+ * ADAPTIVE_NOT_SEAMLESS : The Renderer can adapt between formats,
+ * but may suffer a brief discontinuity (~50-100ms)
+ * when adaptation occurs.
+ */
+ final String mimeType = format.sampleMimeType;
+ if (!MimeTypes.isAudio(mimeType)) {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
+ }
+ List<MediaCodecInfo> decoderInfos = null;
+ try {
+ final MediaCodecSelector mediaCodecSelector = MediaCodecSelector.DEFAULT;
+ decoderInfos = mediaCodecSelector.getDecoderInfos(mimeType, false, false);
+ } catch (final MediaCodecUtil.DecoderQueryException e) {
+ Log.e(LOGTAG, e.getMessage());
+ }
+ if (decoderInfos == null || decoderInfos.isEmpty()) {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);
+ }
+ final MediaCodecInfo info = decoderInfos.get(0);
+ /*
+ * Note : If the code can make it to this place, ExoPlayer assumes
+ * support for unknown sampleRate and channelCount when
+ * SDK version is less than 21, otherwise, further check is needed
+ * if there's no sampleRate/channelCount in format.
+ */
+ final boolean decoderCapable =
+ ((format.sampleRate == Format.NO_VALUE
+ || info.isAudioSampleRateSupportedV21(format.sampleRate))
+ && (format.channelCount == Format.NO_VALUE
+ || info.isAudioChannelCountSupportedV21(format.channelCount)));
+ return RendererCapabilities.create(
+ decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES,
+ ADAPTIVE_NOT_SEAMLESS,
+ TUNNELING_NOT_SUPPORTED);
+ }
+
+ @Override
+ protected final void createInputBuffer() {
+ // We're not able to estimate the size for audio from format. So we rely
+ // on the dynamic allocation mechanism provided in DecoderInputBuffer.
+ mInputBuffer = null;
+ }
+
+ @Override
+ protected void resetRenderer() {
+ mInputBuffer = null;
+ mInitialized = false;
+ }
+
+ @Override
+ protected void handleReconfiguration(final DecoderInputBuffer bufferForRead) {
+ // Do nothing
+ }
+
+ @Override
+ protected void handleFormatRead(final DecoderInputBuffer bufferForRead)
+ throws ExoPlaybackException {
+ onInputFormatChanged(mFormatHolder.format);
+ }
+
+ @Override
+ protected void handleEndOfStream(final DecoderInputBuffer bufferForRead) {
+ mInputStreamEnded = true;
+ mDemuxedInputSamples.offer(GeckoHLSSample.EOS);
+ }
+
+ @Override
+ protected void handleSamplePreparation(final DecoderInputBuffer bufferForRead) {
+ final int size = bufferForRead.data.limit();
+ final byte[] realData = new byte[size];
+ bufferForRead.data.get(realData, 0, size);
+ final ByteBuffer buffer = ByteBuffer.wrap(realData);
+ mInputBuffer = bufferForRead.data;
+ mInputBuffer.clear();
+
+ final CryptoInfo cryptoInfo =
+ bufferForRead.isEncrypted() ? bufferForRead.cryptoInfo.getFrameworkCryptoInfoV16() : null;
+ final BufferInfo bufferInfo = new BufferInfo();
+ // Flags in DecoderInputBuffer are synced with MediaCodec Buffer flags.
+ int flags = 0;
+ flags |= bufferForRead.isKeyFrame() ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0;
+ flags |= bufferForRead.isEndOfStream() ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0;
+ bufferInfo.set(0, size, bufferForRead.timeUs, flags);
+
+ assertTrue(mFormats.size() >= 0);
+ // We add a new format in the list once format changes, so the formatIndex
+ // should indicate to the last(latest) format.
+ final GeckoHLSSample sample =
+ GeckoHLSSample.create(buffer, bufferInfo, cryptoInfo, mFormats.size() - 1);
+
+ mDemuxedInputSamples.offer(sample);
+
+ if (BuildConfig.DEBUG_BUILD) {
+ Log.d(
+ LOGTAG,
+ "Demuxed sample PTS : "
+ + sample.info.presentationTimeUs
+ + ", duration :"
+ + sample.duration
+ + ", formatIndex("
+ + sample.formatIndex
+ + "), queue size : "
+ + mDemuxedInputSamples.size());
+ }
+ }
+
+ @Override
+ protected boolean clearInputSamplesQueue() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "clearInputSamplesQueue");
+ }
+ mDemuxedInputSamples.clear();
+ return true;
+ }
+
+ @Override
+ protected void notifyPlayerInputFormatChanged(final Format newFormat) {
+ mPlayerEventDispatcher.onAudioInputFormatChanged(newFormat);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsPlayer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsPlayer.java
new file mode 100644
index 0000000000..4fe5064072
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsPlayer.java
@@ -0,0 +1,1107 @@
+/* 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.media;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.mozilla.geckoview.BuildConfig;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.DefaultLoadControl;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSourceEventListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultAllocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+
+@ReflectionTarget
+public class GeckoHlsPlayer implements BaseHlsPlayer, ExoPlayer.EventListener {
+ private static final String LOGTAG = "GeckoHlsPlayer";
+ private static final DefaultBandwidthMeter BANDWIDTH_METER =
+ new DefaultBandwidthMeter.Builder(null).build();
+ private static final int MAX_TIMELINE_ITEM_LINES = 3;
+ private static final boolean DEBUG = !BuildConfig.MOZILLA_OFFICIAL;
+
+ private static final AtomicInteger sPlayerId = new AtomicInteger(0);
+ /*
+ * Because we treat GeckoHlsPlayer as a source data provider.
+ * It will be created and initialized with a URL by HLSResource in
+ * Gecko media pipleine (in cpp). Once HLSDemuxer is created later, we
+ * need to bridge this HLSResource to the created demuxer. And they share
+ * the same GeckoHlsPlayer.
+ * mPlayerId is a token used for Gecko media pipeline to obtain corresponding player.
+ */
+ private final int mPlayerId;
+ // Accessed only in GeckoHlsPlayerThread.
+ private boolean mExoplayerSuspended = false;
+
+ private static final int DEFAULT_MIN_BUFFER_MS = 5 * 1000;
+ private static final int DEFAULT_MAX_BUFFER_MS = 10 * 1000;
+
+ private enum MediaDecoderPlayState {
+ PLAY_STATE_PREPARING,
+ PLAY_STATE_PAUSED,
+ PLAY_STATE_PLAYING
+ }
+
+ // Default value is PLAY_STATE_PREPARING and it will be set to PLAY_STATE_PLAYING
+ // once HTMLMediaElement calls PlayInternal().
+ // Accessed only in GeckoHlsPlayerThread.
+ private MediaDecoderPlayState mMediaDecoderPlayState = MediaDecoderPlayState.PLAY_STATE_PREPARING;
+
+ private Handler mMainHandler;
+ private HandlerThread mThread;
+ private ExoPlayer mPlayer;
+ private GeckoHlsRendererBase[] mRenderers;
+ private DefaultTrackSelector mTrackSelector;
+ private MediaSource mMediaSource;
+ private SourceEventListener mSourceEventListener;
+ private ComponentListener mComponentListener;
+ private ComponentEventDispatcher mComponentEventDispatcher;
+
+ private volatile boolean mIsTimelineStatic = false;
+ private long mDurationUs;
+
+ private GeckoHlsVideoRenderer mVRenderer = null;
+ private GeckoHlsAudioRenderer mARenderer = null;
+
+ // Able to control if we only want V/A/V+A tracks from bitstream.
+ private class RendererController {
+ private final boolean mEnableV;
+ private final boolean mEnableA;
+
+ RendererController(final boolean enableVideoRenderer, final boolean enableAudioRenderer) {
+ this.mEnableV = enableVideoRenderer;
+ this.mEnableA = enableAudioRenderer;
+ }
+
+ boolean isVideoRendererEnabled() {
+ return mEnableV;
+ }
+
+ boolean isAudioRendererEnabled() {
+ return mEnableA;
+ }
+ }
+
+ private RendererController mRendererController = new RendererController(true, true);
+
+ // Provide statistical information of tracks.
+ private class HlsMediaTracksInfo {
+ private int mNumVideoTracks = 0;
+ private int mNumAudioTracks = 0;
+ private boolean mVideoInfoUpdated = false;
+ private boolean mAudioInfoUpdated = false;
+ private boolean mVideoDataArrived = false;
+ private boolean mAudioDataArrived = false;
+
+ HlsMediaTracksInfo() {}
+
+ public void reset() {
+ mNumVideoTracks = 0;
+ mNumAudioTracks = 0;
+ mVideoInfoUpdated = false;
+ mAudioInfoUpdated = false;
+ mVideoDataArrived = false;
+ mAudioDataArrived = false;
+ }
+
+ public void updateNumOfVideoTracks(final int numOfTracks) {
+ mNumVideoTracks = numOfTracks;
+ }
+
+ public void updateNumOfAudioTracks(final int numOfTracks) {
+ mNumAudioTracks = numOfTracks;
+ }
+
+ public boolean hasVideo() {
+ return mNumVideoTracks > 0;
+ }
+
+ public boolean hasAudio() {
+ return mNumAudioTracks > 0;
+ }
+
+ public int getNumOfVideoTracks() {
+ return mNumVideoTracks;
+ }
+
+ public int getNumOfAudioTracks() {
+ return mNumAudioTracks;
+ }
+
+ public void onVideoInfoUpdated() {
+ mVideoInfoUpdated = true;
+ }
+
+ public void onAudioInfoUpdated() {
+ mAudioInfoUpdated = true;
+ }
+
+ public void onDataArrived(final int trackType) {
+ if (trackType == C.TRACK_TYPE_VIDEO) {
+ mVideoDataArrived = true;
+ } else if (trackType == C.TRACK_TYPE_AUDIO) {
+ mAudioDataArrived = true;
+ }
+ }
+
+ public boolean videoReady() {
+ return !hasVideo() || (mVideoInfoUpdated && mVideoDataArrived);
+ }
+
+ public boolean audioReady() {
+ return !hasAudio() || (mAudioInfoUpdated && mAudioDataArrived);
+ }
+ }
+
+ private HlsMediaTracksInfo mTracksInfo = new HlsMediaTracksInfo();
+
+ // Used only in GeckoHlsPlayerThread.
+ private boolean mIsPlayerInitDone = false;
+ private boolean mIsDemuxerInitDone = false;
+ private BaseHlsPlayer.DemuxerCallbacks mDemuxerCallbacks;
+ private BaseHlsPlayer.ResourceCallbacks mResourceCallbacks;
+
+ private boolean mReleasing = false; // Used only in Gecko Main thread.
+
+ private static void assertTrue(final boolean condition) {
+ if (DEBUG && !condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ protected void checkInitDone() {
+ if (mIsDemuxerInitDone) {
+ return;
+ }
+ assertTrue(mDemuxerCallbacks != null);
+
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "[checkInitDone] VReady:"
+ + mTracksInfo.videoReady()
+ + ",AReady:"
+ + mTracksInfo.audioReady()
+ + ",hasV:"
+ + mTracksInfo.hasVideo()
+ + ",hasA:"
+ + mTracksInfo.hasAudio());
+ }
+ if (mTracksInfo.videoReady() && mTracksInfo.audioReady()) {
+ if (mDemuxerCallbacks != null) {
+ mDemuxerCallbacks.onInitialized(mTracksInfo.hasAudio(), mTracksInfo.hasVideo());
+ }
+ mIsDemuxerInitDone = true;
+ }
+ }
+
+ private final class SourceEventListener implements MediaSourceEventListener {
+ public void onLoadStarted(
+ final int windowIndex,
+ final MediaSource.MediaPeriodId mediaPeriodId,
+ final LoadEventInfo loadEventInfo,
+ final MediaLoadData mediaLoadData) {
+ assertTrue(isPlayerThread());
+
+ synchronized (GeckoHlsPlayer.this) {
+ if (mediaLoadData.dataType != C.DATA_TYPE_MEDIA) {
+ // Don't report non-media URLs.
+ return;
+ }
+ if (mResourceCallbacks == null || loadEventInfo.uri == null || mReleasing) {
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "on-load: url=" + loadEventInfo.uri);
+ }
+ mResourceCallbacks.onLoad(loadEventInfo.uri.toString());
+ }
+ }
+ }
+
+ public final class ComponentEventDispatcher {
+ // Called from GeckoHls{Audio,Video}Renderer/ExoPlayer internal playback thread
+ // or GeckoHlsPlayerThread.
+ public void onDataArrived(final int trackType) {
+ assertTrue(mComponentListener != null);
+
+ if (mComponentListener != null) {
+ runOnPlayerThread(() -> mComponentListener.onDataArrived(trackType));
+ }
+ }
+
+ // Called from GeckoHls{Audio,Video}Renderer internal playback thread.
+ public void onVideoInputFormatChanged(final Format format) {
+ assertTrue(mComponentListener != null);
+
+ if (mComponentListener != null) {
+ runOnPlayerThread(() -> mComponentListener.onVideoInputFormatChanged(format));
+ }
+ }
+
+ // Called from GeckoHls{Audio,Video}Renderer internal playback thread.
+ public void onAudioInputFormatChanged(final Format format) {
+ assertTrue(mComponentListener != null);
+
+ if (mComponentListener != null) {
+ runOnPlayerThread(() -> mComponentListener.onAudioInputFormatChanged(format));
+ }
+ }
+ }
+
+ public final class ComponentListener {
+
+ // General purpose implementation
+ // Called on GeckoHlsPlayerThread
+ public void onDataArrived(final int trackType) {
+ assertTrue(isPlayerThread());
+
+ synchronized (GeckoHlsPlayer.this) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "[CB][onDataArrived] id " + mPlayerId);
+ }
+ if (!mIsPlayerInitDone) {
+ return;
+ }
+
+ mTracksInfo.onDataArrived(trackType);
+ if (!mReleasing) {
+ mResourceCallbacks.onDataArrived();
+ }
+ checkInitDone();
+ }
+ }
+
+ // Called on GeckoHlsPlayerThread
+ public void onVideoInputFormatChanged(final Format format) {
+ assertTrue(isPlayerThread());
+
+ synchronized (GeckoHlsPlayer.this) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "[CB] onVideoInputFormatChanged [" + format + "]");
+ Log.d(
+ LOGTAG,
+ "[CB] SampleMIMEType ["
+ + format.sampleMimeType
+ + "], ContainerMIMEType ["
+ + format.containerMimeType
+ + "], id : "
+ + mPlayerId);
+ }
+ if (!mIsPlayerInitDone) {
+ return;
+ }
+ mTracksInfo.onVideoInfoUpdated();
+ checkInitDone();
+ }
+ }
+
+ // Called on GeckoHlsPlayerThread
+ public void onAudioInputFormatChanged(final Format format) {
+ assertTrue(isPlayerThread());
+
+ synchronized (GeckoHlsPlayer.this) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "[CB] onAudioInputFormatChanged [" + format + "], mPlayerId :" + mPlayerId);
+ }
+ if (!mIsPlayerInitDone) {
+ return;
+ }
+ mTracksInfo.onAudioInfoUpdated();
+ checkInitDone();
+ }
+ }
+ }
+
+ private HlsMediaSource.Factory buildDataSourceFactory(
+ final Context ctx, final DefaultBandwidthMeter bandwidthMeter) {
+ return new HlsMediaSource.Factory(
+ new DefaultDataSourceFactory(
+ ctx, bandwidthMeter, buildHttpDataSourceFactory(bandwidthMeter)));
+ }
+
+ private HttpDataSource.Factory buildHttpDataSourceFactory(
+ final DefaultBandwidthMeter bandwidthMeter) {
+ return new DefaultHttpDataSourceFactory(
+ BuildConfig.USER_AGENT_GECKOVIEW_MOBILE,
+ bandwidthMeter /* listener */,
+ DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
+ true /* allowCrossProtocolRedirects */);
+ }
+
+ private long getDuration() {
+ return awaitPlayerThread(
+ () -> {
+ long duration = 0L;
+ // Value returned by getDuration() is in milliseconds.
+ if (mPlayer != null && !isLiveStream()) {
+ duration = Math.max(0L, mPlayer.getDuration() * 1000L);
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "getDuration : " + duration + "(Us)");
+ }
+ return duration;
+ });
+ }
+
+ // To make sure that each player has a unique id, GeckoHlsPlayer should be
+ // created only from synchronized APIs in GeckoPlayerFactory.
+ public GeckoHlsPlayer() {
+ mPlayerId = sPlayerId.incrementAndGet();
+ if (DEBUG) {
+ Log.d(LOGTAG, " construct player with id(" + mPlayerId + ")");
+ }
+ }
+
+ // Should be only called by GeckoPlayerFactory and GeckoHLSResourceWrapper.
+ // The mPlayerId is used to make sure that the same GeckoHlsPlayer is used by
+ // corresponding HLSResource and HLSDemuxer for each media playback.
+ // Called on Gecko's main thread
+ @Override
+ public int getId() {
+ return mPlayerId;
+ }
+
+ // Called on Gecko's main thread
+ @Override
+ public synchronized void addDemuxerWrapperCallbackListener(
+ final BaseHlsPlayer.DemuxerCallbacks callback) {
+ if (DEBUG) {
+ Log.d(LOGTAG, " addDemuxerWrapperCallbackListener ...");
+ }
+ mDemuxerCallbacks = callback;
+ }
+
+ // Called on GeckoHlsPlayerThread from ExoPlayer
+ @Override
+ public synchronized void onLoadingChanged(final boolean isLoading) {
+ assertTrue(isPlayerThread());
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "loading [" + isLoading + "]");
+ }
+ if (!isLoading) {
+ if (mMediaDecoderPlayState != MediaDecoderPlayState.PLAY_STATE_PLAYING) {
+ suspendExoplayer();
+ }
+ // To update buffered position.
+ mComponentEventDispatcher.onDataArrived(C.TRACK_TYPE_DEFAULT);
+ }
+ }
+
+ // Called on GeckoHlsPlayerThread from ExoPlayer
+ @Override
+ public synchronized void onPlayerStateChanged(final boolean playWhenReady, final int state) {
+ assertTrue(isPlayerThread());
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "state [" + playWhenReady + ", " + getStateString(state) + "]");
+ }
+ if (state == ExoPlayer.STATE_READY
+ && !mExoplayerSuspended
+ && mMediaDecoderPlayState == MediaDecoderPlayState.PLAY_STATE_PLAYING) {
+ resumeExoplayer();
+ }
+ }
+
+ // Called on GeckoHlsPlayerThread from ExoPlayer
+ @Override
+ public void onPositionDiscontinuity(final int reason) {
+ assertTrue(isPlayerThread());
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "positionDiscontinuity: reason=" + reason);
+ }
+ }
+
+ // Called on GeckoHlsPlayerThread from ExoPlayer
+ @Override
+ public void onPlaybackParametersChanged(final PlaybackParameters playbackParameters) {
+ assertTrue(isPlayerThread());
+
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "playbackParameters "
+ + String.format(
+ "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch));
+ }
+ }
+
+ // Called on GeckoHlsPlayerThread from ExoPlayer
+ @Override
+ public synchronized void onPlayerError(final ExoPlaybackException e) {
+ assertTrue(isPlayerThread());
+
+ if (DEBUG) {
+ Log.e(LOGTAG, "playerFailed", e);
+ }
+ mIsPlayerInitDone = false;
+ if (mReleasing) {
+ return;
+ }
+ if (mResourceCallbacks != null) {
+ mResourceCallbacks.onError(ResourceError.PLAYER.code());
+ }
+ if (mDemuxerCallbacks != null) {
+ mDemuxerCallbacks.onError(DemuxerError.PLAYER.code());
+ }
+ }
+
+ // Called on GeckoHlsPlayerThread from ExoPlayer
+ @Override
+ public synchronized void onTracksChanged(
+ final TrackGroupArray ignored, final TrackSelectionArray trackSelections) {
+ assertTrue(isPlayerThread());
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "onTracksChanged : TGA[" + ignored + "], TSA[" + trackSelections + "]");
+
+ final MappedTrackInfo mappedTrackInfo = mTrackSelector.getCurrentMappedTrackInfo();
+ if (mappedTrackInfo == null) {
+ Log.d(LOGTAG, "Tracks []");
+ return;
+ }
+ Log.d(LOGTAG, "Tracks [");
+ // Log tracks associated to renderers.
+ for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) {
+ final TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
+ final TrackSelection trackSelection = trackSelections.get(rendererIndex);
+ if (rendererTrackGroups.length > 0) {
+ Log.d(LOGTAG, " Renderer:" + rendererIndex + " [");
+ for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) {
+ final TrackGroup trackGroup = rendererTrackGroups.get(groupIndex);
+ final String adaptiveSupport =
+ getAdaptiveSupportString(
+ trackGroup.length,
+ mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false));
+ Log.d(
+ LOGTAG,
+ " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " [");
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ final String status = getTrackStatusString(trackSelection, trackGroup, trackIndex);
+ final String formatSupport =
+ getFormatSupportString(
+ mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
+ Log.d(
+ LOGTAG,
+ " "
+ + status
+ + " Track:"
+ + trackIndex
+ + ", "
+ + Format.toLogString(trackGroup.getFormat(trackIndex))
+ + ", supported="
+ + formatSupport);
+ }
+ Log.d(LOGTAG, " ]");
+ }
+ Log.d(LOGTAG, " ]");
+ }
+ }
+ // Log tracks not associated with a renderer.
+ final TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups();
+ if (unassociatedTrackGroups.length > 0) {
+ Log.d(LOGTAG, " Renderer:None [");
+ for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) {
+ Log.d(LOGTAG, " Group:" + groupIndex + " [");
+ final TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex);
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ final String status = getTrackStatusString(false);
+ final String formatSupport =
+ getFormatSupportString(RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
+ Log.d(
+ LOGTAG,
+ " "
+ + status
+ + " Track:"
+ + trackIndex
+ + ", "
+ + Format.toLogString(trackGroup.getFormat(trackIndex))
+ + ", supported="
+ + formatSupport);
+ }
+ Log.d(LOGTAG, " ]");
+ }
+ Log.d(LOGTAG, " ]");
+ }
+ Log.d(LOGTAG, "]");
+ }
+ mTracksInfo.reset();
+ int numVideoTracks = 0;
+ int numAudioTracks = 0;
+ for (int j = 0; j < ignored.length; j++) {
+ final TrackGroup tg = ignored.get(j);
+ for (int i = 0; i < tg.length; i++) {
+ final Format fmt = tg.getFormat(i);
+ if (fmt.sampleMimeType != null) {
+ if (mRendererController.isVideoRendererEnabled()
+ && fmt.sampleMimeType.startsWith(new String("video"))) {
+ numVideoTracks++;
+ } else if (mRendererController.isAudioRendererEnabled()
+ && fmt.sampleMimeType.startsWith(new String("audio"))) {
+ numAudioTracks++;
+ }
+ }
+ }
+ }
+ mTracksInfo.updateNumOfVideoTracks(numVideoTracks);
+ mTracksInfo.updateNumOfAudioTracks(numAudioTracks);
+ }
+
+ // Called on GeckoHlsPlayerThread from ExoPlayer
+ @Override
+ public synchronized void onTimelineChanged(final Timeline timeline, final int reason) {
+ assertTrue(isPlayerThread());
+
+ // For now, we use the interface ExoPlayer.getDuration() for gecko,
+ // so here we create local variable 'window' & 'peroid' to obtain
+ // the dynamic duration.
+ // See.
+ // http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Timeline.html
+ // for further information.
+ final Timeline.Window window = new Timeline.Window();
+ mIsTimelineStatic =
+ !timeline.isEmpty() && !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
+
+ final int periodCount = timeline.getPeriodCount();
+ final int windowCount = timeline.getWindowCount();
+ if (DEBUG) {
+ Log.d(LOGTAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);
+ }
+ final Timeline.Period period = new Timeline.Period();
+ for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) {
+ timeline.getPeriod(i, period);
+ if (mDurationUs < period.getDurationUs()) {
+ mDurationUs = period.getDurationUs();
+ }
+ }
+ for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) {
+ timeline.getWindow(i, window);
+ if (mDurationUs < window.getDurationUs()) {
+ mDurationUs = window.getDurationUs();
+ }
+ }
+ // TODO : Need to check if the duration from play.getDuration is different
+ // with the one calculated from multi-timelines/windows.
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "Media duration (from Timeline) = "
+ + mDurationUs
+ + "(us)"
+ + " player.getDuration() = "
+ + mPlayer.getDuration()
+ + "(ms)");
+ }
+ }
+
+ private static String getStateString(final int state) {
+ switch (state) {
+ case ExoPlayer.STATE_BUFFERING:
+ return "B";
+ case ExoPlayer.STATE_ENDED:
+ return "E";
+ case ExoPlayer.STATE_IDLE:
+ return "I";
+ case ExoPlayer.STATE_READY:
+ return "R";
+ default:
+ return "?";
+ }
+ }
+
+ private static String getFormatSupportString(final int formatSupport) {
+ switch (formatSupport) {
+ case RendererCapabilities.FORMAT_HANDLED:
+ return "YES";
+ case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
+ return "NO_EXCEEDS_CAPABILITIES";
+ case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE:
+ return "NO_UNSUPPORTED_TYPE";
+ case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE:
+ return "NO";
+ default:
+ return "?";
+ }
+ }
+
+ private static String getAdaptiveSupportString(final int trackCount, final int adaptiveSupport) {
+ if (trackCount < 2) {
+ return "N/A";
+ }
+ switch (adaptiveSupport) {
+ case RendererCapabilities.ADAPTIVE_SEAMLESS:
+ return "YES";
+ case RendererCapabilities.ADAPTIVE_NOT_SEAMLESS:
+ return "YES_NOT_SEAMLESS";
+ case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED:
+ return "NO";
+ default:
+ return "?";
+ }
+ }
+
+ private static String getTrackStatusString(
+ final TrackSelection selection, final TrackGroup group, final int trackIndex) {
+ return getTrackStatusString(
+ selection != null
+ && selection.getTrackGroup() == group
+ && selection.indexOf(trackIndex) != C.INDEX_UNSET);
+ }
+
+ private static String getTrackStatusString(final boolean enabled) {
+ return enabled ? "[X]" : "[ ]";
+ }
+
+ // Called on GeckoHlsPlayerThread
+ private void createExoPlayer(final String url) {
+ assertTrue(isPlayerThread());
+
+ final Context ctx = GeckoAppShell.getApplicationContext();
+ mComponentListener = new ComponentListener();
+ mComponentEventDispatcher = new ComponentEventDispatcher();
+ mDurationUs = 0;
+
+ // Prepare trackSelector
+ final TrackSelection.Factory videoTrackSelectionFactory =
+ new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
+ mTrackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
+
+ // Prepare customized renderer
+ mRenderers = new GeckoHlsRendererBase[2];
+ mVRenderer = new GeckoHlsVideoRenderer(mComponentEventDispatcher);
+ mARenderer = new GeckoHlsAudioRenderer(mComponentEventDispatcher);
+ mRenderers[0] = mVRenderer;
+ mRenderers[1] = mARenderer;
+
+ final DefaultLoadControl dlc =
+ new DefaultLoadControl.Builder()
+ .setAllocator(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE))
+ .setBufferDurationsMs(
+ DEFAULT_MIN_BUFFER_MS,
+ DEFAULT_MAX_BUFFER_MS,
+ DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
+ DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
+ .createDefaultLoadControl();
+ // Create ExoPlayer instance with specific components.
+ mPlayer =
+ new ExoPlayer.Builder(ctx, mRenderers)
+ .setTrackSelector(mTrackSelector)
+ .setLoadControl(dlc)
+ .build();
+ mPlayer.addListener(this);
+
+ final Uri uri = Uri.parse(url);
+ mMediaSource = buildDataSourceFactory(ctx, BANDWIDTH_METER).createMediaSource(uri);
+ mSourceEventListener = new SourceEventListener();
+ mMediaSource.addEventListener(mMainHandler, mSourceEventListener);
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "Uri is " + uri + ", ContentType is " + Util.inferContentType(uri.getLastPathSegment()));
+ }
+ mPlayer.setPlayWhenReady(false);
+ mPlayer.prepare(mMediaSource);
+ mIsPlayerInitDone = true;
+ }
+
+ // =======================================================================
+ // API for GeckoHLSResourceWrapper
+ // =======================================================================
+ // Called on Gecko Main Thread
+ @Override
+ public synchronized void init(final String url, final BaseHlsPlayer.ResourceCallbacks callback) {
+ if (DEBUG) {
+ Log.d(LOGTAG, " init");
+ }
+ assertTrue(callback != null);
+ assertTrue(!mIsPlayerInitDone);
+
+ mThread = new HandlerThread("GeckoHlsPlayerThread");
+ mThread.start();
+ mMainHandler = new Handler(mThread.getLooper());
+
+ mMainHandler.post(
+ () -> {
+ mResourceCallbacks = callback;
+ createExoPlayer(url);
+ });
+ }
+
+ // Called on MDSM's TaskQueue
+ @Override
+ public boolean isLiveStream() {
+ return !mIsTimelineStatic;
+ }
+
+ // =======================================================================
+ // API for GeckoHLSDemuxerWrapper
+ // =======================================================================
+ // Called on HLSDemuxer's TaskQueue
+ @Override
+ public synchronized ConcurrentLinkedQueue<GeckoHLSSample> getSamples(
+ final TrackType trackType, final int number) {
+ if (trackType == TrackType.VIDEO) {
+ return mVRenderer != null
+ ? mVRenderer.getQueuedSamples(number)
+ : new ConcurrentLinkedQueue<GeckoHLSSample>();
+ } else if (trackType == TrackType.AUDIO) {
+ return mARenderer != null
+ ? mARenderer.getQueuedSamples(number)
+ : new ConcurrentLinkedQueue<GeckoHLSSample>();
+ } else {
+ return new ConcurrentLinkedQueue<GeckoHLSSample>();
+ }
+ }
+
+ // Called on MFR's TaskQueue
+ @Override
+ public long getBufferedPosition() {
+ return awaitPlayerThread(
+ () -> {
+ // Value returned by getBufferedPosition() is in milliseconds.
+ final long bufferedPos =
+ mPlayer == null ? 0L : Math.max(0L, mPlayer.getBufferedPosition() * 1000L);
+ if (DEBUG) {
+ Log.d(LOGTAG, "getBufferedPosition : " + bufferedPos + "(Us)");
+ }
+ return bufferedPos;
+ });
+ }
+
+ // Called on MFR's TaskQueue
+ @Override
+ public synchronized int getNumberOfTracks(final TrackType trackType) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "getNumberOfTracks : type " + trackType);
+ }
+ if (trackType == TrackType.VIDEO) {
+ return mTracksInfo.getNumOfVideoTracks();
+ } else if (trackType == TrackType.AUDIO) {
+ return mTracksInfo.getNumOfAudioTracks();
+ }
+ return 0;
+ }
+
+ // Called on MFR's TaskQueue
+ @Override
+ public GeckoVideoInfo getVideoInfo(final int index) {
+ final Format fmt;
+ synchronized (this) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "getVideoInfo");
+ }
+ if (mVRenderer == null) {
+ Log.e(LOGTAG, "no render to get video info from. Index : " + index);
+ return null;
+ }
+ if (!mTracksInfo.hasVideo()) {
+ return null;
+ }
+ fmt = mVRenderer.getFormat(index);
+ if (fmt == null) {
+ return null;
+ }
+ }
+ return new GeckoVideoInfo(
+ fmt.width,
+ fmt.height,
+ fmt.width,
+ fmt.height,
+ fmt.rotationDegrees,
+ fmt.stereoMode,
+ getDuration(),
+ fmt.sampleMimeType,
+ null,
+ null);
+ }
+
+ // Called on MFR's TaskQueue
+ @Override
+ public GeckoAudioInfo getAudioInfo(final int index) {
+ final Format fmt;
+ synchronized (this) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "getAudioInfo");
+ }
+ if (mARenderer == null) {
+ Log.e(LOGTAG, "no render to get audio info from. Index : " + index);
+ return null;
+ }
+ if (!mTracksInfo.hasAudio()) {
+ return null;
+ }
+ fmt = mARenderer.getFormat(index);
+ if (fmt == null) {
+ return null;
+ }
+ }
+ /* According to https://github.com/google/ExoPlayer/blob
+ * /d979469659861f7fe1d39d153b90bdff1ab479cc/library/core/src/main
+ * /java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java#L221-L224,
+ * if the input audio format is not raw, exoplayer would assure that
+ * the sample's pcm encoding bitdepth is 16.
+ * For HLS content, it should always be 16.
+ */
+ assertTrue(!MimeTypes.AUDIO_RAW.equals(fmt.sampleMimeType));
+ // For HLS content, csd-0 is enough.
+ final byte[] csd = fmt.initializationData.isEmpty() ? null : fmt.initializationData.get(0);
+ return new GeckoAudioInfo(
+ fmt.sampleRate, fmt.channelCount, 16, 0, getDuration(), fmt.sampleMimeType, csd);
+ }
+
+ // Called on HLSDemuxer's TaskQueue
+ @Override
+ public boolean seek(final long positionUs) {
+ synchronized (this) {
+ if (mPlayer == null) {
+ Log.d(LOGTAG, "Seek operation won't be performed as no player exists!");
+ return false;
+ }
+ }
+ return awaitPlayerThread(
+ () -> {
+ // Need to temporarily resume Exoplayer to download the chunks for getting the demuxed
+ // keyframe sample when HTMLMediaElement is paused. Suspend Exoplayer when collecting
+ // enough
+ // samples in onLoadingChanged.
+ if (mExoplayerSuspended) {
+ resumeExoplayer();
+ }
+ // positionUs : microseconds.
+ // NOTE : 1) It's not possible to seek media by tracktype via ExoPlayer Interface.
+ // 2) positionUs is samples PTS from MFR, we need to re-adjust it
+ // for ExoPlayer by subtracting sample start time.
+ // 3) Time unit for ExoPlayer.seek() is milliseconds.
+ try {
+ // TODO : Gather Timeline Period / Window information to develop
+ // complete timeline, and seekTime should be inside the duration.
+ Long startTime = Long.MAX_VALUE;
+ for (final GeckoHlsRendererBase r : mRenderers) {
+ if (r == mVRenderer
+ && mRendererController.isVideoRendererEnabled()
+ && mTracksInfo.hasVideo()
+ || r == mARenderer
+ && mRendererController.isAudioRendererEnabled()
+ && mTracksInfo.hasAudio()) {
+ // Find the min value of the start time
+ startTime = Math.min(startTime, r.getFirstSamplePTS());
+ }
+ }
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "seeking : "
+ + positionUs / 1000
+ + " (ms); startTime : "
+ + startTime / 1000
+ + " (ms)");
+ }
+ assertTrue(startTime != Long.MAX_VALUE && startTime != Long.MIN_VALUE);
+ mPlayer.seekTo(positionUs / 1000 - startTime / 1000);
+ } catch (final Exception e) {
+ if (mReleasing) {
+ return false;
+ }
+ if (mDemuxerCallbacks != null) {
+ mDemuxerCallbacks.onError(DemuxerError.UNKNOWN.code());
+ }
+ return false;
+ }
+ return true;
+ });
+ }
+
+ // Called on HLSDemuxer's TaskQueue
+ @Override
+ public synchronized long getNextKeyFrameTime() {
+ return mVRenderer != null ? mVRenderer.getNextKeyFrameTime() : Long.MAX_VALUE;
+ }
+
+ // Called on Gecko's main thread.
+ @Override
+ public synchronized void suspend() {
+ runOnPlayerThread(
+ () -> {
+ if (mExoplayerSuspended) {
+ return;
+ }
+ if (mMediaDecoderPlayState != MediaDecoderPlayState.PLAY_STATE_PLAYING) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "suspend player id : " + mPlayerId);
+ }
+ suspendExoplayer();
+ }
+ });
+ }
+
+ // Called on Gecko's main thread.
+ @Override
+ public synchronized void resume() {
+ runOnPlayerThread(
+ () -> {
+ if (!mExoplayerSuspended) {
+ return;
+ }
+ if (mMediaDecoderPlayState == MediaDecoderPlayState.PLAY_STATE_PLAYING) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "resume player id : " + mPlayerId);
+ }
+ resumeExoplayer();
+ }
+ });
+ }
+
+ // Called on Gecko's main thread.
+ @Override
+ public synchronized void play() {
+ runOnPlayerThread(
+ () -> {
+ if (mMediaDecoderPlayState == MediaDecoderPlayState.PLAY_STATE_PLAYING) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "MediaDecoder played.");
+ }
+ mMediaDecoderPlayState = MediaDecoderPlayState.PLAY_STATE_PLAYING;
+ resumeExoplayer();
+ });
+ }
+
+ // Called on Gecko's main thread.
+ @Override
+ public synchronized void pause() {
+ runOnPlayerThread(
+ () -> {
+ if (mMediaDecoderPlayState != MediaDecoderPlayState.PLAY_STATE_PLAYING) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "MediaDecoder paused.");
+ }
+ mMediaDecoderPlayState = MediaDecoderPlayState.PLAY_STATE_PAUSED;
+ suspendExoplayer();
+ });
+ }
+
+ private void suspendExoplayer() {
+ assertTrue(isPlayerThread());
+
+ if (mPlayer == null) {
+ return;
+ }
+ mExoplayerSuspended = true;
+ if (DEBUG) {
+ Log.d(LOGTAG, "suspend Exoplayer");
+ }
+ mPlayer.setPlayWhenReady(false);
+ }
+
+ private void resumeExoplayer() {
+ assertTrue(isPlayerThread());
+
+ if (mPlayer == null) {
+ return;
+ }
+ mExoplayerSuspended = false;
+ if (DEBUG) {
+ Log.d(LOGTAG, "resume Exoplayer");
+ }
+ mPlayer.setPlayWhenReady(true);
+ }
+
+ // Called on Gecko's main thread, when HLSDemuxer or HLSResource destructs.
+ @Override
+ public void release() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "releasing ... id : " + mPlayerId);
+ }
+
+ synchronized (this) {
+ if (mReleasing) {
+ return;
+ } else {
+ mReleasing = true;
+ }
+ }
+
+ runOnPlayerThread(
+ () -> {
+ if (mPlayer != null) {
+ mPlayer.removeListener(this);
+ mPlayer.stop();
+ mPlayer.release();
+ mVRenderer = null;
+ mARenderer = null;
+ mPlayer = null;
+ }
+ if (mThread != null) {
+ mThread.quit();
+ mThread = null;
+ }
+ mDemuxerCallbacks = null;
+ mResourceCallbacks = null;
+ mIsPlayerInitDone = false;
+ mIsDemuxerInitDone = false;
+ });
+ }
+
+ private void runOnPlayerThread(final Runnable task) {
+ assertTrue(mMainHandler != null);
+ if (isPlayerThread()) {
+ task.run();
+ } else {
+ mMainHandler.post(task);
+ }
+ }
+
+ private boolean isPlayerThread() {
+ return Thread.currentThread() == mMainHandler.getLooper().getThread();
+ }
+
+ private <T> T awaitPlayerThread(final Callable<T> task) {
+ assertTrue(!isPlayerThread());
+
+ try {
+ final FutureTask<T> wait = new FutureTask<T>(task);
+ mMainHandler.post(wait);
+ return wait.get();
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsRendererBase.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsRendererBase.java
new file mode 100644
index 0000000000..ecb7b93d61
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsRendererBase.java
@@ -0,0 +1,340 @@
+/* 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.media;
+
+import android.util.Log;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.mozilla.geckoview.BuildConfig;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+
+public abstract class GeckoHlsRendererBase extends BaseRenderer {
+ protected static final int QUEUED_INPUT_SAMPLE_DURATION_THRESHOLD = 1000000; // 1sec
+ protected final FormatHolder mFormatHolder = new FormatHolder();
+ /*
+ * DEBUG/LOGTAG will be set in the 2 subclass GeckoHlsAudioRenderer and
+ * GeckoHlsVideoRenderer, and we still wants to log message in the base class
+ * GeckoHlsRendererBase, so neither 'static' nor 'final' are applied to them.
+ */
+ protected boolean DEBUG;
+ protected String LOGTAG;
+ // Notify GeckoHlsPlayer about renderer's status, i.e. data has arrived.
+ protected GeckoHlsPlayer.ComponentEventDispatcher mPlayerEventDispatcher;
+
+ protected ConcurrentLinkedQueue<GeckoHLSSample> mDemuxedInputSamples =
+ new ConcurrentLinkedQueue<>();
+
+ protected ByteBuffer mInputBuffer = null;
+ protected ArrayList<Format> mFormats = new ArrayList<Format>();
+ protected boolean mInitialized = false;
+ protected boolean mWaitingForData = true;
+ protected boolean mInputStreamEnded = false;
+ protected long mFirstSampleStartTime = Long.MIN_VALUE;
+
+ protected abstract void createInputBuffer() throws ExoPlaybackException;
+
+ protected abstract void handleReconfiguration(DecoderInputBuffer bufferForRead);
+
+ protected abstract void handleFormatRead(DecoderInputBuffer bufferForRead)
+ throws ExoPlaybackException;
+
+ protected abstract void handleEndOfStream(DecoderInputBuffer bufferForRead);
+
+ protected abstract void handleSamplePreparation(DecoderInputBuffer bufferForRead);
+
+ protected abstract void resetRenderer();
+
+ protected abstract boolean clearInputSamplesQueue();
+
+ protected abstract void notifyPlayerInputFormatChanged(Format newFormat);
+
+ private DecoderInputBuffer mBufferForRead =
+ new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ private final DecoderInputBuffer mFlagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
+
+ protected void assertTrue(final boolean condition) {
+ if (DEBUG && !condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ public GeckoHlsRendererBase(
+ final int trackType, final GeckoHlsPlayer.ComponentEventDispatcher eventDispatcher) {
+ super(trackType);
+ mPlayerEventDispatcher = eventDispatcher;
+ }
+
+ private boolean isQueuedEnoughData() {
+ if (mDemuxedInputSamples.isEmpty()) {
+ return false;
+ }
+
+ final Iterator<GeckoHLSSample> iter = mDemuxedInputSamples.iterator();
+ long firstPTS = 0;
+ if (iter.hasNext()) {
+ final GeckoHLSSample sample = iter.next();
+ firstPTS = sample.info.presentationTimeUs;
+ }
+ long lastPTS = firstPTS;
+ while (iter.hasNext()) {
+ final GeckoHLSSample sample = iter.next();
+ lastPTS = sample.info.presentationTimeUs;
+ }
+ return Math.abs(lastPTS - firstPTS) > QUEUED_INPUT_SAMPLE_DURATION_THRESHOLD;
+ }
+
+ public Format getFormat(final int index) {
+ assertTrue(index >= 0);
+ final Format fmt = index < mFormats.size() ? mFormats.get(index) : null;
+ if (DEBUG) {
+ Log.d(LOGTAG, "getFormat : index = " + index + ", format : " + fmt);
+ }
+ return fmt;
+ }
+
+ public synchronized long getFirstSamplePTS() {
+ return mFirstSampleStartTime;
+ }
+
+ public synchronized ConcurrentLinkedQueue<GeckoHLSSample> getQueuedSamples(final int number) {
+ final ConcurrentLinkedQueue<GeckoHLSSample> samples =
+ new ConcurrentLinkedQueue<GeckoHLSSample>();
+
+ GeckoHLSSample sample = null;
+ final int queuedSize = mDemuxedInputSamples.size();
+ for (int i = 0; i < queuedSize; i++) {
+ if (i >= number) {
+ break;
+ }
+ sample = mDemuxedInputSamples.poll();
+ samples.offer(sample);
+ }
+
+ sample = samples.isEmpty() ? null : samples.peek();
+ if (sample == null) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "getQueuedSamples isEmpty, mWaitingForData = true !");
+ }
+ mWaitingForData = true;
+ } else if (mFirstSampleStartTime == Long.MIN_VALUE) {
+ mFirstSampleStartTime = sample.info.presentationTimeUs;
+ if (DEBUG) {
+ Log.d(LOGTAG, "mFirstSampleStartTime = " + mFirstSampleStartTime);
+ }
+ }
+ return samples;
+ }
+
+ protected void handleDrmInitChanged(final Format oldFormat, final Format newFormat) {
+ final Object oldDrmInit = oldFormat == null ? null : oldFormat.drmInitData;
+ final Object newDrnInit = newFormat.drmInitData;
+
+ // TODO: Notify MFR if the content is encrypted or not.
+ if (newDrnInit != oldDrmInit) {
+ if (newDrnInit != null) {
+ } else {
+ }
+ }
+ }
+
+ protected boolean canReconfigure(final Format oldFormat, final Format newFormat) {
+ // Referring to ExoPlayer's MediaCodecBaseRenderer, the default is set
+ // to false. Only override it in video renderer subclass.
+ return false;
+ }
+
+ protected void prepareReconfiguration() {
+ // Referring to ExoPlayer's MediaCodec related renderers, only video
+ // renderer handles this.
+ }
+
+ protected void updateCSDInfo(final Format format) {
+ // do nothing.
+ }
+
+ protected void onInputFormatChanged(final Format newFormat) throws ExoPlaybackException {
+ Format oldFormat;
+ try {
+ oldFormat = mFormats.get(mFormats.size() - 1);
+ } catch (final IndexOutOfBoundsException e) {
+ oldFormat = null;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "[onInputFormatChanged] old : " + oldFormat + " => new : " + newFormat);
+ }
+ mFormats.add(newFormat);
+ handleDrmInitChanged(oldFormat, newFormat);
+
+ if (mInitialized && canReconfigure(oldFormat, newFormat)) {
+ prepareReconfiguration();
+ } else {
+ resetRenderer();
+ maybeInitRenderer();
+ }
+
+ updateCSDInfo(newFormat);
+ notifyPlayerInputFormatChanged(newFormat);
+ }
+
+ protected void maybeInitRenderer() throws ExoPlaybackException {
+ if (mInitialized || mFormats.size() == 0) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "Initializing ... ");
+ }
+ try {
+ createInputBuffer();
+ mInitialized = true;
+ } catch (final OutOfMemoryError e) {
+ throw ExoPlaybackException.createForRenderer(
+ new RuntimeException(e),
+ getIndex(),
+ mFormats.isEmpty() ? null : getFormat(mFormats.size() - 1),
+ RendererCapabilities.FORMAT_HANDLED);
+ }
+ }
+
+ /*
+ * The place we get demuxed data from HlsMediaSource(ExoPlayer).
+ * The data will then be converted to GeckoHLSSample and deliver to
+ * GeckoHlsDemuxerWrapper for further use.
+ * If the return value is ture, that means a GeckoHLSSample is queued
+ * successfully. We can try to feed more samples into queue.
+ * If the return value is false, that means we might encounter following
+ * situation 1) not initialized 2) input stream is ended 3) queue is full.
+ * 4) format changed. 5) exception happened.
+ */
+ protected synchronized boolean feedInputBuffersQueue() throws ExoPlaybackException {
+ if (!mInitialized || mInputStreamEnded || isQueuedEnoughData()) {
+ // Need to reinitialize the renderer or the input stream has ended
+ // or we just reached the maximum queue size.
+ return false;
+ }
+
+ mBufferForRead.data = mInputBuffer;
+ if (mBufferForRead.data != null) {
+ mBufferForRead.clear();
+ }
+
+ handleReconfiguration(mBufferForRead);
+
+ // Read data from HlsMediaSource
+ int result = C.RESULT_NOTHING_READ;
+ try {
+ result = readSource(mFormatHolder, mBufferForRead, false);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "[feedInput] Exception when readSource :", e);
+ return false;
+ }
+
+ if (result == C.RESULT_NOTHING_READ) {
+ return false;
+ }
+
+ if (result == C.RESULT_FORMAT_READ) {
+ handleFormatRead(mBufferForRead);
+ return true;
+ }
+
+ // We've read a buffer.
+ if (mBufferForRead.isEndOfStream()) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "Now we're at the End Of Stream.");
+ }
+ handleEndOfStream(mBufferForRead);
+ return false;
+ }
+
+ mBufferForRead.flip();
+
+ handleSamplePreparation(mBufferForRead);
+
+ maybeNotifyDataArrived();
+ return true;
+ }
+
+ private void maybeNotifyDataArrived() {
+ if (mWaitingForData && isQueuedEnoughData()) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onDataArrived");
+ }
+ mPlayerEventDispatcher.onDataArrived(getTrackType());
+ mWaitingForData = false;
+ }
+ }
+
+ private void readFormat() throws ExoPlaybackException {
+ mFlagsOnlyBuffer.clear();
+ final int result = readSource(mFormatHolder, mFlagsOnlyBuffer, true);
+ if (result == C.RESULT_FORMAT_READ) {
+ onInputFormatChanged(mFormatHolder.format);
+ }
+ }
+
+ @Override
+ protected void onEnabled(final boolean joining) {
+ // Do nothing.
+ }
+
+ @Override
+ protected void onDisabled() {
+ mFormats.clear();
+ resetRenderer();
+ }
+
+ @Override
+ public boolean isReady() {
+ return mFormats.size() != 0;
+ }
+
+ @Override
+ public boolean isEnded() {
+ return mInputStreamEnded;
+ }
+
+ @Override
+ protected synchronized void onPositionReset(final long positionUs, final boolean joining) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "onPositionReset : positionUs = " + positionUs);
+ }
+ mInputStreamEnded = false;
+ if (mInitialized) {
+ clearInputSamplesQueue();
+ }
+ }
+
+ /*
+ * This is called by ExoPlayerImplInternal.java.
+ * ExoPlayer checks the status of renderer, i.e. isReady() / isEnded(), and
+ * calls renderer.render by passing its wall clock time.
+ */
+ @Override
+ public void render(final long positionUs, final long elapsedRealtimeUs)
+ throws ExoPlaybackException {
+ if (BuildConfig.DEBUG_BUILD) {
+ Log.d(LOGTAG, "positionUs = " + positionUs + ", mInputStreamEnded = " + mInputStreamEnded);
+ }
+ if (mInputStreamEnded) {
+ return;
+ }
+ if (mFormats.size() == 0) {
+ readFormat();
+ }
+
+ maybeInitRenderer();
+ while (feedInputBuffersQueue()) {
+ // Do nothing
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsVideoRenderer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsVideoRenderer.java
new file mode 100644
index 0000000000..28f7bad5cf
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoHlsVideoRenderer.java
@@ -0,0 +1,502 @@
+/* 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.media;
+
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CryptoInfo;
+import android.util.Log;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import org.mozilla.geckoview.BuildConfig;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+
+public class GeckoHlsVideoRenderer extends GeckoHlsRendererBase {
+ /*
+ * By configuring these states, initialization data is provided for
+ * ExoPlayer's HlsMediaSource to parse HLS bitstream and then provide samples
+ * starting with an Access Unit Delimiter including SPS/PPS for TS,
+ * and provide samples starting with an AUD without SPS/PPS for FMP4.
+ */
+ private enum RECONFIGURATION_STATE {
+ NONE,
+ WRITE_PENDING,
+ QUEUE_PENDING
+ }
+
+ private boolean mRendererReconfigured;
+ private RECONFIGURATION_STATE mRendererReconfigurationState = RECONFIGURATION_STATE.NONE;
+
+ // A list of the formats which may be included in the bitstream.
+ private Format[] mStreamFormats;
+ // The max width/height/inputBufferSize for specific codec format.
+ private CodecMaxValues mCodecMaxValues;
+ // A temporary queue for samples whose duration is not calculated yet.
+ private ConcurrentLinkedQueue<GeckoHLSSample> mDemuxedNoDurationSamples =
+ new ConcurrentLinkedQueue<>();
+
+ // Contain CSD-0(SPS)/CSD-1(PPS) information (in AnnexB format) for
+ // prepending each keyframe. When video format changes, this information
+ // changes accordingly.
+ private byte[] mCSDInfo = null;
+
+ public GeckoHlsVideoRenderer(final GeckoHlsPlayer.ComponentEventDispatcher eventDispatcher) {
+ super(C.TRACK_TYPE_VIDEO, eventDispatcher);
+ LOGTAG = getClass().getSimpleName();
+ DEBUG = !BuildConfig.MOZILLA_OFFICIAL;
+ }
+
+ @Override
+ public final int supportsMixedMimeTypeAdaptation() {
+ return ADAPTIVE_NOT_SEAMLESS;
+ }
+
+ @Override
+ public final int supportsFormat(final Format format) {
+ /*
+ * FORMAT_EXCEEDS_CAPABILITIES : The Renderer is capable of rendering
+ * formats with the same mime type, but
+ * the properties of the format exceed
+ * the renderer's capability.
+ * FORMAT_UNSUPPORTED_SUBTYPE : The Renderer is a general purpose
+ * renderer for formats of the same
+ * top-level type, but is not capable of
+ * rendering the format or any other format
+ * with the same mime type because the
+ * sub-type is not supported.
+ * FORMAT_UNSUPPORTED_TYPE : The Renderer is not capable of rendering
+ * the format, either because it does not support
+ * the format's top-level type, or because it's
+ * a specialized renderer for a different mime type.
+ * ADAPTIVE_NOT_SEAMLESS : The Renderer can adapt between formats,
+ * but may suffer a brief discontinuity (~50-100ms)
+ * when adaptation occurs.
+ * ADAPTIVE_SEAMLESS : The Renderer can seamlessly adapt between formats.
+ */
+ final String mimeType = format.sampleMimeType;
+ if (!MimeTypes.isVideo(mimeType)) {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
+ }
+
+ List<MediaCodecInfo> decoderInfos = null;
+ try {
+ final MediaCodecSelector mediaCodecSelector = MediaCodecSelector.DEFAULT;
+ decoderInfos = mediaCodecSelector.getDecoderInfos(mimeType, false, false);
+ } catch (final MediaCodecUtil.DecoderQueryException e) {
+ Log.e(LOGTAG, e.getMessage());
+ }
+ if (decoderInfos == null || decoderInfos.isEmpty()) {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);
+ }
+
+ boolean decoderCapable = false;
+ MediaCodecInfo info = null;
+ for (final MediaCodecInfo i : decoderInfos) {
+ if (i.isCodecSupported(format)) {
+ decoderCapable = true;
+ info = i;
+ }
+ }
+ if (decoderCapable && format.width > 0 && format.height > 0) {
+ decoderCapable =
+ info.isVideoSizeAndRateSupportedV21(format.width, format.height, format.frameRate);
+ }
+
+ return RendererCapabilities.create(
+ decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES,
+ info != null && info.adaptive ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS,
+ TUNNELING_NOT_SUPPORTED);
+ }
+
+ @Override
+ protected final void createInputBuffer() throws ExoPlaybackException {
+ assertTrue(mFormats.size() > 0);
+ // Calculate maximum size which might be used for target format.
+ final Format currentFormat = mFormats.get(mFormats.size() - 1);
+ mCodecMaxValues = getCodecMaxValues(currentFormat, mStreamFormats);
+ // Create a buffer with maximal size for reading source.
+ // Note : Though we are able to dynamically enlarge buffer size by
+ // creating DecoderInputBuffer with specific BufferReplacementMode, we
+ // still allocate a calculated max size buffer for it at first to reduce
+ // runtime overhead.
+ try {
+ mInputBuffer = ByteBuffer.wrap(new byte[mCodecMaxValues.inputSize]);
+ } catch (final OutOfMemoryError e) {
+ Log.e(LOGTAG, "cannot allocate input buffer of size " + mCodecMaxValues.inputSize, e);
+ throw ExoPlaybackException.createForRenderer(
+ new Exception(e),
+ getIndex(),
+ mFormats.isEmpty() ? null : getFormat(mFormats.size() - 1),
+ RendererCapabilities.FORMAT_HANDLED);
+ }
+ }
+
+ @Override
+ protected void resetRenderer() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "[resetRenderer] mInitialized = " + mInitialized);
+ }
+ if (mInitialized) {
+ mRendererReconfigured = false;
+ mRendererReconfigurationState = RECONFIGURATION_STATE.NONE;
+ mInputBuffer = null;
+ mCSDInfo = null;
+ mInitialized = false;
+ }
+ }
+
+ @Override
+ protected void handleReconfiguration(final DecoderInputBuffer bufferForRead) {
+ // For adaptive reconfiguration OMX decoders expect all reconfiguration
+ // data to be supplied at the start of the buffer that also contains
+ // the first frame in the new format.
+ assertTrue(mFormats.size() > 0);
+ if (mRendererReconfigurationState == RECONFIGURATION_STATE.WRITE_PENDING) {
+ if (bufferForRead.data == null) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "[feedInput][WRITE_PENDING] bufferForRead.data is not initialized.");
+ }
+ return;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "[feedInput][WRITE_PENDING] put initialization data");
+ }
+ final Format currentFormat = mFormats.get(mFormats.size() - 1);
+ for (int i = 0; i < currentFormat.initializationData.size(); i++) {
+ final byte[] data = currentFormat.initializationData.get(i);
+ bufferForRead.data.put(data);
+ }
+ mRendererReconfigurationState = RECONFIGURATION_STATE.QUEUE_PENDING;
+ }
+ }
+
+ @Override
+ protected void handleFormatRead(final DecoderInputBuffer bufferForRead)
+ throws ExoPlaybackException {
+ if (mRendererReconfigurationState == RECONFIGURATION_STATE.QUEUE_PENDING) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "[feedInput][QUEUE_PENDING] 2 formats in a row.");
+ }
+ // We received two formats in a row. Clear the current buffer of any reconfiguration data
+ // associated with the first format.
+ bufferForRead.clear();
+ mRendererReconfigurationState = RECONFIGURATION_STATE.WRITE_PENDING;
+ }
+ onInputFormatChanged(mFormatHolder.format);
+ }
+
+ @Override
+ protected void handleEndOfStream(final DecoderInputBuffer bufferForRead) {
+ if (mRendererReconfigurationState == RECONFIGURATION_STATE.QUEUE_PENDING) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "[feedInput][QUEUE_PENDING] isEndOfStream.");
+ }
+ // We received a new format immediately before the end of the stream. We need to clear
+ // the corresponding reconfiguration data from the current buffer, but re-write it into
+ // a subsequent buffer if there are any (e.g. if the user seeks backwards).
+ bufferForRead.clear();
+ mRendererReconfigurationState = RECONFIGURATION_STATE.WRITE_PENDING;
+ }
+ mInputStreamEnded = true;
+ final GeckoHLSSample sample = GeckoHLSSample.EOS;
+ calculatDuration(sample);
+ }
+
+ @Override
+ protected void handleSamplePreparation(final DecoderInputBuffer bufferForRead) {
+ final int csdInfoSize = mCSDInfo != null ? mCSDInfo.length : 0;
+ final int dataSize = bufferForRead.data.limit();
+ final int size = bufferForRead.isKeyFrame() ? csdInfoSize + dataSize : dataSize;
+ final byte[] realData = new byte[size];
+ if (bufferForRead.isKeyFrame()) {
+ // Prepend the CSD information to the sample if it's a key frame.
+ System.arraycopy(mCSDInfo, 0, realData, 0, csdInfoSize);
+ bufferForRead.data.get(realData, csdInfoSize, dataSize);
+ } else {
+ bufferForRead.data.get(realData, 0, dataSize);
+ }
+ final ByteBuffer buffer = ByteBuffer.wrap(realData);
+ mInputBuffer = bufferForRead.data;
+ mInputBuffer.clear();
+
+ final CryptoInfo cryptoInfo =
+ bufferForRead.isEncrypted() ? bufferForRead.cryptoInfo.getFrameworkCryptoInfoV16() : null;
+ final BufferInfo bufferInfo = new BufferInfo();
+ // Flags in DecoderInputBuffer are synced with MediaCodec Buffer flags.
+ int flags = 0;
+ flags |= bufferForRead.isKeyFrame() ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0;
+ flags |= bufferForRead.isEndOfStream() ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0;
+ bufferInfo.set(0, size, bufferForRead.timeUs, flags);
+
+ assertTrue(mFormats.size() > 0);
+ // We add a new format in the list once format changes, so the formatIndex
+ // should indicate to the last(latest) format.
+ final GeckoHLSSample sample =
+ GeckoHLSSample.create(buffer, bufferInfo, cryptoInfo, mFormats.size() - 1);
+
+ // There's no duration information from the ExoPlayer's sample, we need
+ // to calculate it.
+ calculatDuration(sample);
+ mRendererReconfigurationState = RECONFIGURATION_STATE.NONE;
+ }
+
+ @Override
+ protected void onPositionReset(final long positionUs, final boolean joining) {
+ super.onPositionReset(positionUs, joining);
+ if (mInitialized && mRendererReconfigured && mFormats.size() != 0) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "[onPositionReset] WRITE_PENDING");
+ }
+ // Any reconfiguration data that we put shortly before the reset
+ // may be invalid. We avoid this issue by sending reconfiguration
+ // data following every position reset.
+ mRendererReconfigurationState = RECONFIGURATION_STATE.WRITE_PENDING;
+ }
+ }
+
+ @Override
+ protected boolean clearInputSamplesQueue() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "clearInputSamplesQueue");
+ }
+ mDemuxedInputSamples.clear();
+ mDemuxedNoDurationSamples.clear();
+ return true;
+ }
+
+ @Override
+ protected boolean canReconfigure(final Format oldFormat, final Format newFormat) {
+ final boolean canReconfig =
+ areAdaptationCompatible(oldFormat, newFormat)
+ && newFormat.width <= mCodecMaxValues.width
+ && newFormat.height <= mCodecMaxValues.height
+ && newFormat.maxInputSize <= mCodecMaxValues.inputSize;
+ if (DEBUG) {
+ Log.d(LOGTAG, "[canReconfigure] : " + canReconfig);
+ }
+ return canReconfig;
+ }
+
+ @Override
+ protected void prepareReconfiguration() {
+ if (DEBUG) {
+ Log.d(LOGTAG, "[onInputFormatChanged] starting reconfiguration !");
+ }
+ mRendererReconfigured = true;
+ mRendererReconfigurationState = RECONFIGURATION_STATE.WRITE_PENDING;
+ }
+
+ @Override
+ protected void updateCSDInfo(final Format format) {
+ int size = 0;
+ for (int i = 0; i < format.initializationData.size(); i++) {
+ size += format.initializationData.get(i).length;
+ }
+ int startPos = 0;
+ mCSDInfo = new byte[size];
+ for (int i = 0; i < format.initializationData.size(); i++) {
+ final byte[] data = format.initializationData.get(i);
+ System.arraycopy(data, 0, mCSDInfo, startPos, data.length);
+ startPos += data.length;
+ }
+ if (DEBUG) {
+ Log.d(LOGTAG, "mCSDInfo [" + Utils.bytesToHex(mCSDInfo) + "]");
+ }
+ }
+
+ @Override
+ protected void notifyPlayerInputFormatChanged(final Format newFormat) {
+ mPlayerEventDispatcher.onVideoInputFormatChanged(newFormat);
+ }
+
+ private void calculateSamplesWithin(final GeckoHLSSample[] samples, final int range) {
+ // Calculate the first 'range' elements.
+ for (int i = 0; i < range; i++) {
+ // Comparing among samples in the window.
+ for (int j = -2; j < 14; j++) {
+ if (i + j >= 0
+ && i + j < range
+ && samples[i + j].info.presentationTimeUs > samples[i].info.presentationTimeUs) {
+ samples[i].duration =
+ Math.min(
+ samples[i].duration,
+ samples[i + j].info.presentationTimeUs - samples[i].info.presentationTimeUs);
+ }
+ }
+ }
+ }
+
+ private void calculatDuration(final GeckoHLSSample inputSample) {
+ /*
+ * NOTE :
+ * Since we customized renderer as a demuxer. Here we're not able to
+ * obtain duration from the DecoderInputBuffer as there's no duration inside.
+ * So we calcualte it by referring to nearby samples' timestamp.
+ * A temporary queue |mDemuxedNoDurationSamples| is used to queue demuxed
+ * samples from HlsMediaSource which have no duration information at first.
+ * We're choosing 16 as the comparing window size, because it's commonly
+ * used as a GOP size.
+ * Considering there're 16 demuxed samples in the _no duration_ queue already,
+ * e.g. |-2|-1|0|1|2|3|4|5|6|...|13|
+ * Once a new demuxed(No duration) sample X (17th) is put into the
+ * temporary queue,
+ * e.g. |-2|-1|0|1|2|3|4|5|6|...|13|X|
+ * we are able to calculate the correct duration for sample 0 by finding
+ * the closest but greater pts than sample 0 among these 16 samples,
+ * here, let's say sample -2 to 13.
+ */
+ if (inputSample != null) {
+ mDemuxedNoDurationSamples.offer(inputSample);
+ }
+ final int sizeOfNoDura = mDemuxedNoDurationSamples.size();
+ // A calculation window we've ever found suitable for both HLS TS & FMP4.
+ final int range = sizeOfNoDura >= 17 ? 17 : sizeOfNoDura;
+ final GeckoHLSSample[] inputArray =
+ mDemuxedNoDurationSamples.toArray(new GeckoHLSSample[sizeOfNoDura]);
+ if (range >= 17 && !mInputStreamEnded) {
+ calculateSamplesWithin(inputArray, range);
+
+ final GeckoHLSSample toQueue = mDemuxedNoDurationSamples.poll();
+ mDemuxedInputSamples.offer(toQueue);
+ if (BuildConfig.DEBUG_BUILD) {
+ Log.d(
+ LOGTAG,
+ "Demuxed sample PTS : "
+ + toQueue.info.presentationTimeUs
+ + ", duration :"
+ + toQueue.duration
+ + ", isKeyFrame("
+ + toQueue.isKeyFrame()
+ + ", formatIndex("
+ + toQueue.formatIndex
+ + "), queue size : "
+ + mDemuxedInputSamples.size()
+ + ", NoDuQueue size : "
+ + mDemuxedNoDurationSamples.size());
+ }
+ } else if (mInputStreamEnded) {
+ calculateSamplesWithin(inputArray, sizeOfNoDura);
+
+ // NOTE : We're not able to calculate the duration for the last sample.
+ // A workaround here is to assign a close duration to it.
+ long prevDuration = 33333;
+ GeckoHLSSample sample = null;
+ for (sample = mDemuxedNoDurationSamples.poll();
+ sample != null;
+ sample = mDemuxedNoDurationSamples.poll()) {
+ if (sample.duration == Long.MAX_VALUE) {
+ sample.duration = prevDuration;
+ if (DEBUG) {
+ Log.d(LOGTAG, "Adjust the PTS of the last sample to " + sample.duration + " (us)");
+ }
+ }
+ prevDuration = sample.duration;
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "last loop to offer samples - PTS : "
+ + sample.info.presentationTimeUs
+ + ", Duration : "
+ + sample.duration
+ + ", isEOS : "
+ + sample.isEOS());
+ }
+ mDemuxedInputSamples.offer(sample);
+ }
+ }
+ }
+
+ // Return the time of first keyframe sample in the queue.
+ // If there's no key frame in the queue, return the MAX_VALUE so
+ // MFR won't mistake for that which the decode is getting slow.
+ public long getNextKeyFrameTime() {
+ long nextKeyFrameTime = Long.MAX_VALUE;
+ for (final GeckoHLSSample sample : mDemuxedInputSamples) {
+ if (sample != null && (sample.info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
+ nextKeyFrameTime = sample.info.presentationTimeUs;
+ break;
+ }
+ }
+ return nextKeyFrameTime;
+ }
+
+ @Override
+ protected void onStreamChanged(final Format[] formats, final long offsetUs) {
+ mStreamFormats = formats;
+ }
+
+ private static CodecMaxValues getCodecMaxValues(
+ final Format format, final Format[] streamFormats) {
+ int maxWidth = format.width;
+ int maxHeight = format.height;
+ int maxInputSize = getMaxInputSize(format);
+ for (final Format streamFormat : streamFormats) {
+ if (areAdaptationCompatible(format, streamFormat)) {
+ maxWidth = Math.max(maxWidth, streamFormat.width);
+ maxHeight = Math.max(maxHeight, streamFormat.height);
+ maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat));
+ }
+ }
+ return new CodecMaxValues(maxWidth, maxHeight, maxInputSize);
+ }
+
+ private static int getMaxInputSize(final Format format) {
+ if (format.maxInputSize != Format.NO_VALUE) {
+ // The format defines an explicit maximum input size.
+ return format.maxInputSize;
+ }
+
+ if (format.width == Format.NO_VALUE || format.height == Format.NO_VALUE) {
+ // We can't infer a maximum input size without video dimensions.
+ return Format.NO_VALUE;
+ }
+
+ // Attempt to infer a maximum input size from the format.
+ final int maxPixels;
+ final int minCompressionRatio;
+ switch (format.sampleMimeType) {
+ case MimeTypes.VIDEO_H264:
+ // Round up width/height to an integer number of macroblocks.
+ maxPixels = ((format.width + 15) / 16) * ((format.height + 15) / 16) * 16 * 16;
+ minCompressionRatio = 2;
+ break;
+ default:
+ // Leave the default max input size.
+ return Format.NO_VALUE;
+ }
+ // Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames.
+ return (maxPixels * 3) / (2 * minCompressionRatio);
+ }
+
+ private static boolean areAdaptationCompatible(final Format first, final Format second) {
+ return first.sampleMimeType.equals(second.sampleMimeType)
+ && getRotationDegrees(first) == getRotationDegrees(second);
+ }
+
+ private static int getRotationDegrees(final Format format) {
+ return format.rotationDegrees == Format.NO_VALUE ? 0 : format.rotationDegrees;
+ }
+
+ private static final class CodecMaxValues {
+ public final int width;
+ public final int height;
+ public final int inputSize;
+
+ public CodecMaxValues(final int width, final int height, final int inputSize) {
+ this.width = width;
+ this.height = height;
+ this.inputSize = inputSize;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrm.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrm.java
new file mode 100644
index 0000000000..75dc7b2a80
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrm.java
@@ -0,0 +1,40 @@
+/* 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.media;
+
+import android.media.MediaCrypto;
+
+public interface GeckoMediaDrm {
+ interface Callbacks {
+ void onSessionCreated(int createSessionToken, int promiseId, byte[] sessionId, byte[] request);
+
+ void onSessionUpdated(int promiseId, byte[] sessionId);
+
+ void onSessionClosed(int promiseId, byte[] sessionId);
+
+ void onSessionMessage(byte[] sessionId, int sessionMessageType, byte[] request);
+
+ void onSessionError(byte[] sessionId, String message);
+
+ void onSessionBatchedKeyChanged(byte[] sessionId, SessionKeyInfo[] keyInfos);
+
+ // All failure cases should go through this function.
+ void onRejectPromise(int promiseId, String message);
+ }
+
+ void setCallbacks(Callbacks callbacks);
+
+ void createSession(int createSessionToken, int promiseId, String initDataType, byte[] initData);
+
+ void updateSession(int promiseId, String sessionId, byte[] response);
+
+ void closeSession(int promiseId, String sessionId);
+
+ void release();
+
+ MediaCrypto getMediaCrypto();
+
+ void setServerCertificate(final byte[] cert);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
new file mode 100644
index 0000000000..9d098a303f
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
@@ -0,0 +1,766 @@
+/* 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.media;
+
+import android.annotation.SuppressLint;
+import android.media.DeniedByServerException;
+import android.media.MediaCrypto;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.UUID;
+import org.mozilla.gecko.util.ProxySelector;
+
+public class GeckoMediaDrmBridgeV21 implements GeckoMediaDrm {
+ protected final String LOGTAG;
+ private static final String INVALID_SESSION_ID = "Invalid";
+ private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha";
+ private static final boolean DEBUG = false;
+ private static final UUID WIDEVINE_SCHEME_UUID =
+ new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
+ private static final int MAX_PROMISE_ID = Integer.MAX_VALUE;
+ // MediaDrm.KeyStatus information listener is supported on M+, adding a
+ // dummy key id to report key status.
+ private static final byte[] DUMMY_KEY_ID = new byte[] {0};
+
+ public static final Charset UTF_8 = Charset.forName("UTF-8");
+
+ private UUID mSchemeUUID;
+ private Handler mHandler;
+ PostRequestTask mProvisionTask;
+ private HandlerThread mHandlerThread;
+ private ByteBuffer mCryptoSessionId;
+
+ // mProvisioningPromiseId is great than 0 only during provisioning.
+ private int mProvisioningPromiseId;
+ private HashSet<ByteBuffer> mSessionIds;
+ private HashMap<ByteBuffer, String> mSessionMIMETypes;
+ private ArrayDeque<PendingCreateSessionData> mPendingCreateSessionDataQueue;
+ private PendingKeyRequest mPendingKeyRequest;
+ private GeckoMediaDrm.Callbacks mCallbacks;
+
+ private MediaCrypto mCrypto;
+ protected MediaDrm mDrm;
+
+ public static final int LICENSE_REQUEST_INITIAL = 0; /*MediaKeyMessageType::License_request*/
+ public static final int LICENSE_REQUEST_RENEWAL = 1; /*MediaKeyMessageType::License_renewal*/
+ public static final int LICENSE_REQUEST_RELEASE = 2; /*MediaKeyMessageType::License_release*/
+
+ // Store session data while provisioning
+ private static class PendingCreateSessionData {
+ public final int mToken;
+ public final int mPromiseId;
+ public final byte[] mInitData;
+ public final String mMimeType;
+
+ private PendingCreateSessionData(
+ final int token, final int promiseId, final byte[] initData, final String mimeType) {
+ mToken = token;
+ mPromiseId = promiseId;
+ mInitData = initData;
+ mMimeType = mimeType;
+ }
+ }
+
+ private static class PendingKeyRequest {
+ public final ByteBuffer mSession;
+ public final byte[] mData;
+ public final String mMimeType;
+
+ private PendingKeyRequest(final ByteBuffer session, final byte[] data, final String mimeType) {
+ mSession = session;
+ mData = data;
+ mMimeType = mimeType;
+ }
+ }
+
+ public boolean isSecureDecoderComonentRequired(final String mimeType) {
+ if (mCrypto != null) {
+ return mCrypto.requiresSecureDecoderComponent(mimeType);
+ }
+ return false;
+ }
+
+ private static void assertTrue(final boolean condition) {
+ if (DEBUG && !condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ @SuppressLint("WrongConstant")
+ private void configureVendorSpecificProperty() {
+ assertTrue(mDrm != null);
+ if (mDrm == null) {
+ return;
+ }
+ // Support L3 for now
+ mDrm.setPropertyString("securityLevel", "L3");
+ // Refer to chromium, set multi-session mode for Widevine.
+ if (mSchemeUUID.equals(WIDEVINE_SCHEME_UUID)) {
+ mDrm.setPropertyString("privacyMode", "enable");
+ mDrm.setPropertyString("sessionSharing", "enable");
+ }
+ }
+
+ GeckoMediaDrmBridgeV21(final String keySystem) throws Exception {
+ LOGTAG = getClass().getSimpleName();
+ if (DEBUG) Log.d(LOGTAG, "GeckoMediaDrmBridgeV21 ctor");
+
+ mProvisioningPromiseId = 0;
+ mSessionIds = new HashSet<ByteBuffer>();
+ mSessionMIMETypes = new HashMap<ByteBuffer, String>();
+ mPendingCreateSessionDataQueue = new ArrayDeque<PendingCreateSessionData>();
+
+ mSchemeUUID = convertKeySystemToSchemeUUID(keySystem);
+ mCryptoSessionId = null;
+
+ if (DEBUG) Log.d(LOGTAG, "mSchemeUUID : " + mSchemeUUID.toString());
+
+ // The caller of GeckoMediaDrmBridgeV21 ctor should handle exceptions
+ // threw by the following steps.
+ mDrm = new MediaDrm(mSchemeUUID);
+ configureVendorSpecificProperty();
+ mDrm.setOnEventListener(new MediaDrmListener());
+ try {
+ // ensureMediaCryptoCreated may cause NotProvisionedException for the first time use.
+ // Need to start provisioning with a dummy promise id.
+ ensureMediaCryptoCreated();
+ } catch (final android.media.NotProvisionedException e) {
+ if (DEBUG) Log.d(LOGTAG, "Device not provisioned:" + e.getMessage());
+ startProvisioning(MAX_PROMISE_ID);
+ }
+ }
+
+ @Override
+ public void setCallbacks(final GeckoMediaDrm.Callbacks callbacks) {
+ assertTrue(callbacks != null);
+ mCallbacks = callbacks;
+ }
+
+ @Override
+ public void createSession(
+ final int createSessionToken,
+ final int promiseId,
+ final String initDataType,
+ final byte[] initData) {
+ if (DEBUG) Log.d(LOGTAG, "createSession()");
+ if (mDrm == null) {
+ onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
+ return;
+ }
+
+ if (mProvisioningPromiseId > 0 && mCrypto == null) {
+ if (DEBUG) Log.d(LOGTAG, "Pending createSession because it's provisioning !");
+ savePendingCreateSessionData(
+ createSessionToken, promiseId,
+ initData, initDataType);
+ return;
+ }
+
+ ByteBuffer sessionId = null;
+ try {
+ final boolean hasMediaCrypto = ensureMediaCryptoCreated();
+ if (!hasMediaCrypto) {
+ onRejectPromise(promiseId, "MediaCrypto intance is not created !");
+ return;
+ }
+
+ sessionId = openSession();
+ if (sessionId == null) {
+ onRejectPromise(promiseId, "Cannot get a session id from MediaDrm !");
+ return;
+ }
+
+ final MediaDrm.KeyRequest request = getKeyRequest(sessionId, initData, initDataType);
+ if (request == null) {
+ mDrm.closeSession(sessionId.array());
+ onRejectPromise(promiseId, "Cannot get a key request from MediaDrm !");
+ return;
+ }
+ onSessionCreated(createSessionToken, promiseId, sessionId.array(), request.getData());
+ onSessionMessage(sessionId.array(), LICENSE_REQUEST_INITIAL, request.getData());
+ mSessionMIMETypes.put(sessionId, initDataType);
+ mSessionIds.add(sessionId);
+ if (DEBUG)
+ Log.d(
+ LOGTAG,
+ " StringID : " + new String(sessionId.array(), UTF_8) + " is put into mSessionIds ");
+ } catch (final android.media.NotProvisionedException e) {
+ if (DEBUG) Log.d(LOGTAG, "Device not provisioned:" + e.getMessage());
+ if (sessionId != null) {
+ // The promise of this createSession will be either resolved
+ // or rejected after provisioning.
+ mDrm.closeSession(sessionId.array());
+ }
+ savePendingCreateSessionData(
+ createSessionToken, promiseId,
+ initData, initDataType);
+ startProvisioning(promiseId);
+ }
+ }
+
+ @Override
+ public void updateSession(final int promiseId, final String sessionId, final byte[] response) {
+ if (DEBUG) Log.d(LOGTAG, "updateSession(), sessionId = " + sessionId);
+ if (mDrm == null) {
+ onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
+ return;
+ }
+
+ final ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes(UTF_8));
+ if (!sessionExists(session)) {
+ onRejectPromise(promiseId, "Invalid session during updateSession.");
+ return;
+ }
+
+ try {
+ final byte[] keySetId = mDrm.provideKeyResponse(session.array(), response);
+ if (DEBUG) {
+ final HashMap<String, String> infoMap = mDrm.queryKeyStatus(session.array());
+ for (final String strKey : infoMap.keySet()) {
+ final String strValue = infoMap.get(strKey);
+ Log.d(LOGTAG, "InfoMap : key(" + strKey + ")/value(" + strValue + ")");
+ }
+ }
+ HandleKeyStatusChangeByDummyKey(sessionId);
+ onSessionUpdated(promiseId, session.array());
+ return;
+ } catch (final NotProvisionedException | DeniedByServerException | IllegalStateException e) {
+ if (DEBUG) Log.d(LOGTAG, "Failed to provide key response:", e);
+ onSessionError(session.array(), "Got exception during updateSession.");
+ onRejectPromise(promiseId, "Got exception during updateSession.");
+ }
+ release();
+ return;
+ }
+
+ @Override
+ public void closeSession(final int promiseId, final String sessionId) {
+ if (DEBUG) Log.d(LOGTAG, "closeSession()");
+ if (mDrm == null) {
+ onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
+ return;
+ }
+
+ final ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes(UTF_8));
+ mSessionIds.remove(session);
+ mDrm.closeSession(session.array());
+ onSessionClosed(promiseId, session.array());
+ }
+
+ @Override
+ public void release() {
+ if (DEBUG) Log.d(LOGTAG, "release()");
+ if (mProvisionTask != null) {
+ mProvisionTask.cancel(true);
+ mProvisionTask = null;
+ }
+ if (mProvisioningPromiseId > 0) {
+ onRejectPromise(mProvisioningPromiseId, "Releasing ... reject provisioning session.");
+ mProvisioningPromiseId = 0;
+ }
+ if (mPendingKeyRequest != null) {
+ mPendingKeyRequest = null;
+ }
+ while (!mPendingCreateSessionDataQueue.isEmpty()) {
+ final PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll();
+ if (pendingData != null) {
+ onRejectPromise(pendingData.mPromiseId, "Releasing ... reject all pending sessions.");
+ }
+ }
+ mPendingCreateSessionDataQueue = null;
+
+ if (mDrm != null) {
+ for (final ByteBuffer session : mSessionIds) {
+ mDrm.closeSession(session.array());
+ }
+ mDrm.release();
+ mDrm = null;
+ }
+ mSessionIds.clear();
+ mSessionIds = null;
+ mSessionMIMETypes.clear();
+ mSessionMIMETypes = null;
+
+ mCryptoSessionId = null;
+ if (mCrypto != null) {
+ mCrypto.release();
+ mCrypto = null;
+ }
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread = null;
+ }
+ mHandler = null;
+ }
+
+ @Override
+ public MediaCrypto getMediaCrypto() {
+ if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
+ return mCrypto;
+ }
+
+ @SuppressLint("WrongConstant")
+ @Override
+ public void setServerCertificate(final byte[] cert) {
+ if (DEBUG) Log.d(LOGTAG, "setServerCertificate()");
+ if (mDrm == null) {
+ throw new IllegalStateException("MediaDrm instance doesn't exist !!");
+ }
+ mDrm.setPropertyByteArray("serviceCertificate", cert);
+ return;
+ }
+
+ protected void HandleKeyStatusChangeByDummyKey(final String sessionId) {
+ final SessionKeyInfo[] keyInfos = new SessionKeyInfo[1];
+ keyInfos[0] = new SessionKeyInfo(DUMMY_KEY_ID, MediaDrm.KeyStatus.STATUS_USABLE);
+ onSessionBatchedKeyChanged(sessionId.getBytes(), keyInfos);
+ if (DEBUG) Log.d(LOGTAG, "Key successfully added for session " + sessionId);
+ }
+
+ protected void onSessionCreated(
+ final int createSessionToken,
+ final int promiseId,
+ final byte[] sessionId,
+ final byte[] request) {
+ assertTrue(mCallbacks != null);
+ if (mCallbacks != null) {
+ mCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request);
+ }
+ }
+
+ protected void onSessionUpdated(final int promiseId, final byte[] sessionId) {
+ assertTrue(mCallbacks != null);
+ if (mCallbacks != null) {
+ mCallbacks.onSessionUpdated(promiseId, sessionId);
+ }
+ }
+
+ protected void onSessionClosed(final int promiseId, final byte[] sessionId) {
+ assertTrue(mCallbacks != null);
+ if (mCallbacks != null) {
+ mCallbacks.onSessionClosed(promiseId, sessionId);
+ }
+ }
+
+ protected void onSessionMessage(
+ final byte[] sessionId, final int sessionMessageType, final byte[] request) {
+ assertTrue(mCallbacks != null);
+ if (mCallbacks != null) {
+ mCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
+ }
+ }
+
+ protected void onSessionError(final byte[] sessionId, final String message) {
+ assertTrue(mCallbacks != null);
+ if (mCallbacks != null) {
+ mCallbacks.onSessionError(sessionId, message);
+ }
+ }
+
+ protected void onSessionBatchedKeyChanged(
+ final byte[] sessionId, final SessionKeyInfo[] keyInfos) {
+ assertTrue(mCallbacks != null);
+ if (mCallbacks != null) {
+ mCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
+ }
+ }
+
+ protected void onRejectPromise(final int promiseId, final String message) {
+ assertTrue(mCallbacks != null);
+ if (mCallbacks != null) {
+ mCallbacks.onRejectPromise(promiseId, message);
+ }
+ }
+
+ private MediaDrm.KeyRequest getKeyRequest(
+ final ByteBuffer aSession, final byte[] data, final String mimeType)
+ throws android.media.NotProvisionedException {
+ if (mProvisioningPromiseId > 0) {
+ if (DEBUG) Log.d(LOGTAG, "Now provisioning");
+ return null;
+ }
+
+ try {
+ final HashMap<String, String> optionalParameters = new HashMap<String, String>();
+ return mDrm.getKeyRequest(
+ aSession.array(), data, mimeType, MediaDrm.KEY_TYPE_STREAMING, optionalParameters);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Got excpetion during MediaDrm.getKeyRequest", e);
+ }
+ return null;
+ }
+
+ private class MediaDrmListener implements MediaDrm.OnEventListener {
+ @Override
+ public void onEvent(
+ final MediaDrm mediaDrm,
+ final byte[] sessionArray,
+ final int event,
+ final int extra,
+ final byte[] data) {
+ if (DEBUG) Log.d(LOGTAG, "MediaDrmListener.onEvent()");
+ if (sessionArray == null) {
+ if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Null session.");
+ return;
+ }
+ final ByteBuffer session = ByteBuffer.wrap(sessionArray);
+ if (!sessionExists(session)) {
+ if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Invalid session.");
+ return;
+ }
+ // On L, these events are treated as exceptions and handled correspondingly.
+ // Leaving this code block for logging message.
+ switch (event) {
+ case MediaDrm.EVENT_PROVISION_REQUIRED:
+ if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_PROVISION_REQUIRED");
+ break;
+ case MediaDrm.EVENT_KEY_REQUIRED:
+ if (DEBUG)
+ Log.d(
+ LOGTAG,
+ "MediaDrm.EVENT_KEY_REQUIRED, sessionId=" + new String(session.array(), UTF_8));
+ final String mimeType = mSessionMIMETypes.get(session);
+ MediaDrm.KeyRequest request = null;
+ try {
+ request = getKeyRequest(session, data, mimeType);
+ } catch (final android.media.NotProvisionedException e) {
+ Log.w(LOGTAG, "MediaDrm.EVENT_KEY_REQUIRED, Device not provisioned.", e);
+ startProvisioning(MAX_PROMISE_ID);
+ mPendingKeyRequest = new PendingKeyRequest(session, data, mimeType);
+ return;
+ }
+ requestLicense(sessionArray, request);
+ break;
+ case MediaDrm.EVENT_KEY_EXPIRED:
+ if (DEBUG)
+ Log.d(
+ LOGTAG,
+ "MediaDrm.EVENT_KEY_EXPIRED, sessionId=" + new String(session.array(), UTF_8));
+ break;
+ case MediaDrm.EVENT_VENDOR_DEFINED:
+ if (DEBUG)
+ Log.d(
+ LOGTAG,
+ "MediaDrm.EVENT_VENDOR_DEFINED, sessionId=" + new String(session.array(), UTF_8));
+ break;
+ case MediaDrm.EVENT_SESSION_RECLAIMED:
+ if (DEBUG)
+ Log.d(
+ LOGTAG,
+ "MediaDrm.EVENT_SESSION_RECLAIMED, sessionId="
+ + new String(session.array(), UTF_8));
+ break;
+ default:
+ if (DEBUG) Log.d(LOGTAG, "Invalid DRM event " + event);
+ return;
+ }
+ }
+ }
+
+ private ByteBuffer openSession() throws android.media.NotProvisionedException {
+ try {
+ final byte[] sessionId = mDrm.openSession();
+ // ByteBuffer.wrap() is backed by the byte[]. Make a clone here in
+ // case the underlying byte[] is modified.
+ return ByteBuffer.wrap(sessionId.clone());
+ } catch (final android.media.NotProvisionedException e) {
+ // Throw NotProvisionedException so that we can startProvisioning().
+ throw e;
+ } catch (final java.lang.RuntimeException e) {
+ if (DEBUG) Log.d(LOGTAG, "Cannot open a new session:" + e.getMessage());
+ release();
+ return null;
+ } catch (final android.media.MediaDrmException e) {
+ // Other MediaDrmExceptions (e.g. ResourceBusyException) are not
+ // recoverable.
+ release();
+ return null;
+ }
+ }
+
+ protected boolean sessionExists(final ByteBuffer session) {
+ if (mCryptoSessionId == null) {
+ if (DEBUG)
+ Log.d(LOGTAG, "Session doesn't exist because media crypto session is not created.");
+ return false;
+ }
+ if (session == null) {
+ if (DEBUG) Log.d(LOGTAG, "Session is null, not in map !");
+ return false;
+ }
+ return !session.equals(mCryptoSessionId) && mSessionIds.contains(session);
+ }
+
+ private class PostRequestTask extends AsyncTask<Void, Void, Void> {
+ private static final String LOGTAG = "PostRequestTask";
+
+ private int mPromiseId;
+ private String mURL;
+ private byte[] mDrmRequest;
+ private byte[] mResponseBody;
+
+ PostRequestTask(final int promiseId, final String url, final byte[] drmRequest) {
+ this.mPromiseId = promiseId;
+ this.mURL = url;
+ this.mDrmRequest = drmRequest;
+ }
+
+ @Override
+ protected Void doInBackground(final Void... params) {
+ HttpURLConnection urlConnection = null;
+ BufferedReader in = null;
+ try {
+ final URI finalURI =
+ new URI(mURL + "&signedRequest=" + URLEncoder.encode(new String(mDrmRequest), "UTF-8"));
+ urlConnection = (HttpURLConnection) ProxySelector.openConnectionWithProxy(finalURI);
+ urlConnection.setRequestMethod("POST");
+ if (DEBUG) Log.d(LOGTAG, "Provisioning, posting url =" + finalURI.toString());
+
+ // Add data
+ urlConnection.setRequestProperty("Accept", "*/*");
+ urlConnection.setRequestProperty("User-Agent", getCDMUserAgent());
+ urlConnection.setRequestProperty("Content-Type", "application/json");
+
+ // Execute HTTP Post Request
+ urlConnection.connect();
+
+ final int responseCode = urlConnection.getResponseCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), UTF_8));
+ String inputLine;
+ final StringBuffer response = new StringBuffer();
+
+ while ((inputLine = in.readLine()) != null) {
+ response.append(inputLine);
+ }
+ in.close();
+ mResponseBody = String.valueOf(response).getBytes(UTF_8);
+ if (DEBUG) Log.d(LOGTAG, "Provisioning, response received.");
+ if (mResponseBody != null) Log.d(LOGTAG, "response length=" + mResponseBody.length);
+ } else {
+ Log.d(LOGTAG, "Provisioning, server returned HTTP error code :" + responseCode);
+ }
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Got exception during posting provisioning request ...", e);
+ } catch (final URISyntaxException e) {
+ Log.e(LOGTAG, "Got exception during creating uri ...", e);
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ try {
+ if (in != null) {
+ in.close();
+ }
+ } catch (final IOException e) {
+ Log.e(LOGTAG, "Exception during closing in ...", e);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(final Void v) {
+ onProvisionResponse(mPromiseId, mResponseBody);
+ }
+ }
+
+ private boolean provideProvisionResponse(final byte[] response) {
+ if (response == null || response.length == 0) {
+ if (DEBUG) Log.d(LOGTAG, "Invalid provision response.");
+ return false;
+ }
+
+ try {
+ mDrm.provideProvisionResponse(response);
+ return true;
+ } catch (final android.media.DeniedByServerException e) {
+ if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage());
+ } catch (final java.lang.IllegalStateException e) {
+ if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage());
+ }
+ return false;
+ }
+
+ private void savePendingCreateSessionData(
+ final int token, final int promiseId, final byte[] initData, final String mime) {
+ if (DEBUG) Log.d(LOGTAG, "savePendingCreateSessionData, promiseId : " + promiseId);
+ mPendingCreateSessionDataQueue.offer(
+ new PendingCreateSessionData(token, promiseId, initData, mime));
+ }
+
+ private void processPendingCreateSessionData() {
+ if (DEBUG) Log.d(LOGTAG, "processPendingCreateSessionData ... ");
+
+ assertTrue(mProvisioningPromiseId == 0);
+ try {
+ while (!mPendingCreateSessionDataQueue.isEmpty()) {
+ final PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll();
+ if (pendingData == null) {
+ return;
+ }
+ if (DEBUG)
+ Log.d(LOGTAG, "processPendingCreateSessionData, promiseId : " + pendingData.mPromiseId);
+
+ createSession(
+ pendingData.mToken,
+ pendingData.mPromiseId,
+ pendingData.mMimeType,
+ pendingData.mInitData);
+ }
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Got excpetion during processPendingCreateSessionData ...", e);
+ }
+ }
+
+ private void resumePendingOperations() {
+ if (mHandlerThread == null) {
+ mHandlerThread = new HandlerThread("PendingSessionOpsThread");
+ mHandlerThread.start();
+ }
+ if (mHandler == null) {
+ mHandler = new Handler(mHandlerThread.getLooper());
+ }
+ mHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mPendingKeyRequest != null) {
+ MediaDrm.KeyRequest request = null;
+ try {
+ request =
+ getKeyRequest(
+ mPendingKeyRequest.mSession,
+ mPendingKeyRequest.mData,
+ mPendingKeyRequest.mMimeType);
+ } catch (final NotProvisionedException e) {
+ Log.e(LOGTAG, "Cannot get key request after provisioning!");
+ return;
+ } finally {
+ mPendingKeyRequest = null;
+ }
+ requestLicense(mPendingKeyRequest.mSession.array(), request);
+ } else {
+ processPendingCreateSessionData();
+ }
+ }
+ });
+ }
+
+ private void requestLicense(final byte[] session, final MediaDrm.KeyRequest request) {
+ if (request == null) {
+ Log.e(LOGTAG, "null key request when requesting license");
+ return;
+ }
+ // The EME spec says the messageType is only for optimization and optional.
+ // Send 'License_request' as default when it's not available.
+ int requestType = LICENSE_REQUEST_INITIAL;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ requestType = request.getRequestType();
+ }
+ onSessionMessage(session, requestType, request.getData());
+ }
+
+ // Only triggered when failed on {openSession, getKeyRequest}
+ private void startProvisioning(final int promiseId) {
+ if (DEBUG) Log.d(LOGTAG, "startProvisioning()");
+ if (mProvisioningPromiseId > 0) {
+ // Already in provisioning.
+ return;
+ }
+ try {
+ mProvisioningPromiseId = promiseId;
+ final MediaDrm.ProvisionRequest request = mDrm.getProvisionRequest();
+ mProvisionTask = new PostRequestTask(promiseId, request.getDefaultUrl(), request.getData());
+ mProvisionTask.execute();
+ } catch (final Exception e) {
+ onRejectPromise(promiseId, "Exception happened in startProvisioning !");
+ mProvisioningPromiseId = 0;
+ }
+ }
+
+ private void onProvisionResponse(final int promiseId, final byte[] response) {
+ if (DEBUG) Log.d(LOGTAG, "onProvisionResponse()");
+ mProvisionTask = null;
+ mProvisioningPromiseId = 0;
+ final boolean success = provideProvisionResponse(response);
+ if (success) {
+ // Promise will either be resovled / rejected in createSession during
+ // resuming operations.
+ resumePendingOperations();
+ } else {
+ onRejectPromise(promiseId, "Failed to provide provision response.");
+ }
+ }
+
+ private boolean ensureMediaCryptoCreated() throws android.media.NotProvisionedException {
+ if (mCrypto != null) {
+ return true;
+ }
+ try {
+ mCryptoSessionId = openSession();
+ if (mCryptoSessionId == null) {
+ if (DEBUG) Log.d(LOGTAG, "Cannot open session for MediaCrypto");
+ return false;
+ }
+
+ if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
+ final byte[] cryptoSessionId = mCryptoSessionId.array();
+ mCrypto = new MediaCrypto(mSchemeUUID, cryptoSessionId);
+ mSessionIds.add(mCryptoSessionId);
+ if (DEBUG)
+ Log.d(
+ LOGTAG,
+ "MediaCrypto successfully created! - SId "
+ + INVALID_SESSION_ID
+ + ", "
+ + new String(cryptoSessionId, UTF_8));
+ return true;
+ } else {
+ if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto for unsupported scheme.");
+ return false;
+ }
+ } catch (final android.media.MediaCryptoException e) {
+ if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto:" + e.getMessage());
+ release();
+ return false;
+ } catch (final android.media.NotProvisionedException e) {
+ if (DEBUG)
+ Log.d(LOGTAG, "ensureMediaCryptoCreated::Device not provisioned:" + e.getMessage());
+ throw e;
+ }
+ }
+
+ private UUID convertKeySystemToSchemeUUID(final String keySystem) {
+ if (WIDEVINE_KEY_SYSTEM.equals(keySystem)) {
+ return WIDEVINE_SCHEME_UUID;
+ }
+ if (DEBUG) Log.d(LOGTAG, "Cannot convert unsupported key system : " + keySystem);
+ return new UUID(0L, 0L);
+ }
+
+ private String getCDMUserAgent() {
+ // This user agent is found and hard-coded in Android(L) source code and
+ // Chromium project. Not sure if it's gonna change in the future.
+ return "Widevine CDM v1.0";
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java
new file mode 100644
index 0000000000..bee2635a81
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV23.java
@@ -0,0 +1,50 @@
+/* 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.media;
+
+import static android.os.Build.VERSION_CODES.M;
+
+import android.annotation.TargetApi;
+import android.media.MediaDrm;
+import android.util.Log;
+import java.util.List;
+
+@TargetApi(M)
+public class GeckoMediaDrmBridgeV23 extends GeckoMediaDrmBridgeV21 {
+ private static final boolean DEBUG = false;
+
+ GeckoMediaDrmBridgeV23(final String keySystem) throws Exception {
+ super(keySystem);
+ if (DEBUG) Log.d(LOGTAG, "GeckoMediaDrmBridgeV23 ctor");
+ mDrm.setOnKeyStatusChangeListener(new KeyStatusChangeListener(), null);
+ }
+
+ private class KeyStatusChangeListener implements MediaDrm.OnKeyStatusChangeListener {
+ @Override
+ public void onKeyStatusChange(
+ final MediaDrm mediaDrm,
+ final byte[] sessionId,
+ final List<MediaDrm.KeyStatus> keyInformation,
+ final boolean hasNewUsableKey) {
+ if (DEBUG) Log.d(LOGTAG, "[onKeyStatusChange] hasNewUsableKey = " + hasNewUsableKey);
+ if (keyInformation.size() == 0) {
+ return;
+ }
+ final SessionKeyInfo[] keyInfos = new SessionKeyInfo[keyInformation.size()];
+ for (int i = 0; i < keyInformation.size(); i++) {
+ final MediaDrm.KeyStatus keyStatus = keyInformation.get(i);
+ keyInfos[i] = new SessionKeyInfo(keyStatus.getKeyId(), keyStatus.getStatusCode());
+ }
+ onSessionBatchedKeyChanged(sessionId, keyInfos);
+ if (DEBUG) Log.d(LOGTAG, "Key successfully added for session " + new String(sessionId));
+ }
+ }
+
+ @Override
+ protected void HandleKeyStatusChangeByDummyKey(final String sessionId) {
+ // MediaDrm.KeyStatus information listener is supported on M+, there is no need to use
+ // dummy key id to report key status anymore.
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoPlayerFactory.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoPlayerFactory.java
new file mode 100644
index 0000000000..47278115d3
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoPlayerFactory.java
@@ -0,0 +1,43 @@
+/* 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.media;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import java.util.ArrayList;
+
+public final class GeckoPlayerFactory {
+ public static final ArrayList<BaseHlsPlayer> sPlayerList = new ArrayList<BaseHlsPlayer>();
+
+ static synchronized BaseHlsPlayer getPlayer() {
+ try {
+ final Class<?> cls = Class.forName("org.mozilla.gecko.media.GeckoHlsPlayer");
+ final BaseHlsPlayer player = (BaseHlsPlayer) cls.newInstance();
+ sPlayerList.add(player);
+ return player;
+ } catch (final Exception e) {
+ Log.e("GeckoPlayerFactory", "Class GeckoHlsPlayer not found or failed to create", e);
+ }
+ return null;
+ }
+
+ static synchronized BaseHlsPlayer getPlayer(final int id) {
+ for (final BaseHlsPlayer player : sPlayerList) {
+ if (player.getId() == id) {
+ return player;
+ }
+ }
+ Log.w("GeckoPlayerFactory", "No player found with id : " + id);
+ return null;
+ }
+
+ static synchronized void removePlayer(final @NonNull BaseHlsPlayer player) {
+ final int index = sPlayerList.indexOf(player);
+ if (index >= 0) {
+ sPlayerList.remove(player);
+ Log.d("GeckoPlayerFactory", "HlsPlayer with id(" + player.getId() + ") is removed.");
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoVideoInfo.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoVideoInfo.java
new file mode 100644
index 0000000000..c641c58354
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/GeckoVideoInfo.java
@@ -0,0 +1,45 @@
+/* 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.media;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+// A subset of the class VideoInfo in dom/media/MediaInfo.h
+@WrapForJNI
+public final class GeckoVideoInfo {
+ public final byte[] codecSpecificData;
+ public final byte[] extraData;
+ public final int displayWidth;
+ public final int displayHeight;
+ public final int pictureWidth;
+ public final int pictureHeight;
+ public final int rotation;
+ public final int stereoMode;
+ public final long duration;
+ public final String mimeType;
+
+ public GeckoVideoInfo(
+ final int displayWidth,
+ final int displayHeight,
+ final int pictureWidth,
+ final int pictureHeight,
+ final int rotation,
+ final int stereoMode,
+ final long duration,
+ final String mimeType,
+ final byte[] extraData,
+ final byte[] codecSpecificData) {
+ this.displayWidth = displayWidth;
+ this.displayHeight = displayHeight;
+ this.pictureWidth = pictureWidth;
+ this.pictureHeight = pictureHeight;
+ this.rotation = rotation;
+ this.stereoMode = stereoMode;
+ this.duration = duration;
+ this.mimeType = mimeType;
+ this.extraData = extraData;
+ this.codecSpecificData = codecSpecificData;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
new file mode 100644
index 0000000000..3b055f0bca
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/JellyBeanAsyncCodec.java
@@ -0,0 +1,481 @@
+/* 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.media;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.view.Surface;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
+
+// Implement async API using MediaCodec sync mode (API v16).
+// This class uses internal worker thread/handler (mBufferPoller) to poll
+// input and output buffer and notifies the client through callbacks.
+final class JellyBeanAsyncCodec implements AsyncCodec {
+ private static final String LOGTAG = "GeckoAsyncCodecAPIv16";
+ private static final boolean DEBUG = false;
+
+ private static final int ERROR_CODEC = -10000;
+
+ private abstract class CancelableHandler extends Handler {
+ private static final int MSG_CANCELLATION = 0x434E434C; // 'CNCL'
+
+ protected CancelableHandler(final Looper looper) {
+ super(looper);
+ }
+
+ protected void cancel() {
+ removeCallbacksAndMessages(null);
+ sendEmptyMessage(MSG_CANCELLATION);
+ // Wait until handleMessageLocked() is done.
+ synchronized (this) {
+ }
+ }
+
+ protected boolean isCanceled() {
+ return hasMessages(MSG_CANCELLATION);
+ }
+
+ // Subclass should implement this and return true if it handles msg.
+ // Warning: Never, ever call super.handleMessage() in this method!
+ protected abstract boolean handleMessageLocked(Message msg);
+
+ public final void handleMessage(final Message msg) {
+ // Block cancel() during handleMessageLocked().
+ synchronized (this) {
+ if (isCanceled() || handleMessageLocked(msg)) {
+ return;
+ }
+ }
+
+ switch (msg.what) {
+ case MSG_CANCELLATION:
+ // Just a marker. Nothing to do here.
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "handler " + this + " done cancellation, codec=" + JellyBeanAsyncCodec.this);
+ }
+ break;
+ default:
+ super.handleMessage(msg);
+ break;
+ }
+ }
+ }
+
+ // A handler to invoke AsyncCodec.Callbacks methods.
+ private final class CallbackSender extends CancelableHandler {
+ private static final int MSG_INPUT_BUFFER_AVAILABLE = 1;
+ private static final int MSG_OUTPUT_BUFFER_AVAILABLE = 2;
+ private static final int MSG_OUTPUT_FORMAT_CHANGE = 3;
+ private static final int MSG_ERROR = 4;
+ private Callbacks mCallbacks;
+
+ private CallbackSender(final Looper looper, final Callbacks callbacks) {
+ super(looper);
+ mCallbacks = callbacks;
+ }
+
+ public void notifyInputBuffer(final int index) {
+ if (isCanceled()) {
+ return;
+ }
+
+ final Message msg = obtainMessage(MSG_INPUT_BUFFER_AVAILABLE);
+ msg.arg1 = index;
+ processMessage(msg);
+ }
+
+ private void processMessage(final Message msg) {
+ if (Looper.myLooper() == getLooper()) {
+ handleMessage(msg);
+ } else {
+ sendMessage(msg);
+ }
+ }
+
+ public void notifyOutputBuffer(final int index, final MediaCodec.BufferInfo info) {
+ if (isCanceled()) {
+ return;
+ }
+
+ final Message msg = obtainMessage(MSG_OUTPUT_BUFFER_AVAILABLE, info);
+ msg.arg1 = index;
+ processMessage(msg);
+ }
+
+ public void notifyOutputFormat(final MediaFormat format) {
+ if (isCanceled()) {
+ return;
+ }
+ processMessage(obtainMessage(MSG_OUTPUT_FORMAT_CHANGE, format));
+ }
+
+ public void notifyError(final int result) {
+ Log.e(LOGTAG, "codec error:" + result);
+ processMessage(obtainMessage(MSG_ERROR, result, 0));
+ }
+
+ protected boolean handleMessageLocked(final Message msg) {
+ switch (msg.what) {
+ case MSG_INPUT_BUFFER_AVAILABLE: // arg1: buffer index.
+ mCallbacks.onInputBufferAvailable(JellyBeanAsyncCodec.this, msg.arg1);
+ break;
+ case MSG_OUTPUT_BUFFER_AVAILABLE: // arg1: buffer index, obj: info.
+ mCallbacks.onOutputBufferAvailable(
+ JellyBeanAsyncCodec.this, msg.arg1, (MediaCodec.BufferInfo) msg.obj);
+ break;
+ case MSG_OUTPUT_FORMAT_CHANGE: // obj: output format.
+ mCallbacks.onOutputFormatChanged(JellyBeanAsyncCodec.this, (MediaFormat) msg.obj);
+ break;
+ case MSG_ERROR: // arg1: error code.
+ mCallbacks.onError(JellyBeanAsyncCodec.this, msg.arg1);
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ // Handler to poll input and output buffers using dequeue(Input|Output)Buffer(),
+ // with 10ms time-out. Once triggered and successfully gets a buffer, it
+ // will schedule next polling until EOS or failure. To prevent it from
+ // automatically polling more buffer, use cancel() it inherits from
+ // CancelableHandler.
+ private final class BufferPoller extends CancelableHandler {
+ private static final int MSG_POLL_INPUT_BUFFERS = 1;
+ private static final int MSG_POLL_OUTPUT_BUFFERS = 2;
+
+ private static final long DEQUEUE_TIMEOUT_US = 10000;
+
+ public BufferPoller(final Looper looper) {
+ super(looper);
+ }
+
+ private void schedulePollingIfNotCanceled(final int what) {
+ if (isCanceled()) {
+ return;
+ }
+
+ schedulePolling(what);
+ }
+
+ private void schedulePolling(final int what) {
+ if (needsBuffer(what)) {
+ sendEmptyMessage(what);
+ }
+ }
+
+ private boolean needsBuffer(final int what) {
+ if (mOutputEnded && (what == MSG_POLL_OUTPUT_BUFFERS)) {
+ return false;
+ }
+
+ return !(mInputEnded && (what == MSG_POLL_INPUT_BUFFERS));
+ }
+
+ protected boolean handleMessageLocked(final Message msg) {
+ try {
+ switch (msg.what) {
+ case MSG_POLL_INPUT_BUFFERS:
+ pollInputBuffer();
+ break;
+ case MSG_POLL_OUTPUT_BUFFERS:
+ pollOutputBuffer();
+ break;
+ default:
+ return false;
+ }
+ } catch (final IllegalStateException e) {
+ e.printStackTrace();
+ mCallbackSender.notifyError(ERROR_CODEC);
+ }
+
+ return true;
+ }
+
+ private void pollInputBuffer() {
+ final int result = mCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US);
+ if (result >= 0) {
+ mCallbackSender.notifyInputBuffer(result);
+ } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ mBufferPoller.schedulePollingIfNotCanceled(BufferPoller.MSG_POLL_INPUT_BUFFERS);
+ } else {
+ mCallbackSender.notifyError(result);
+ }
+ }
+
+ private void pollOutputBuffer() {
+ boolean dequeueMoreBuffer = true;
+ final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+ final int result = mCodec.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT_US);
+ if (result >= 0) {
+ if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ mOutputEnded = true;
+ }
+ mCallbackSender.notifyOutputBuffer(result, info);
+ } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+ mOutputBuffers = mCodec.getOutputBuffers();
+ } else if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ mOutputBuffers = mCodec.getOutputBuffers();
+ mCallbackSender.notifyOutputFormat(mCodec.getOutputFormat());
+ } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ // When input ended, keep polling remaining output buffer until EOS.
+ dequeueMoreBuffer = mInputEnded;
+ } else {
+ mCallbackSender.notifyError(result);
+ dequeueMoreBuffer = false;
+ }
+
+ if (dequeueMoreBuffer) {
+ schedulePollingIfNotCanceled(MSG_POLL_OUTPUT_BUFFERS);
+ }
+ }
+ }
+
+ private MediaCodec mCodec;
+ private ByteBuffer[] mInputBuffers;
+ private ByteBuffer[] mOutputBuffers;
+ private AsyncCodec.Callbacks mCallbacks;
+ private CallbackSender mCallbackSender;
+
+ private BufferPoller mBufferPoller;
+ private volatile boolean mInputEnded;
+ private volatile boolean mOutputEnded;
+
+ // Must be called on a thread with looper.
+ /* package */ JellyBeanAsyncCodec(final String name) throws IOException {
+ mCodec = MediaCodec.createByCodecName(name);
+ initBufferPoller(name + " buffer poller");
+ }
+
+ private void initBufferPoller(final String name) {
+ if (mBufferPoller != null) {
+ Log.e(LOGTAG, "poller already initialized");
+ return;
+ }
+ final HandlerThread thread = new HandlerThread(name);
+ thread.start();
+ mBufferPoller = new BufferPoller(thread.getLooper());
+ if (DEBUG) {
+ Log.d(LOGTAG, "start poller for codec:" + this + ", thread=" + thread.getThreadId());
+ }
+ }
+
+ @Override
+ public void setCallbacks(final AsyncCodec.Callbacks callbacks, final Handler handler) {
+ if (callbacks == null) {
+ return;
+ }
+
+ Looper looper = (handler == null) ? null : handler.getLooper();
+ if (looper == null) {
+ // Use this thread if no handler supplied.
+ looper = Looper.myLooper();
+ }
+ if (looper == null) {
+ // This thread has no looper. Use poller thread.
+ looper = mBufferPoller.getLooper();
+ }
+ mCallbackSender = new CallbackSender(looper, callbacks);
+ if (DEBUG) {
+ Log.d(LOGTAG, "setCallbacks(): sender=" + mCallbackSender);
+ }
+ }
+
+ @Override
+ public void configure(
+ final MediaFormat format, final Surface surface, final MediaCrypto crypto, final int flags) {
+ assertCallbacks();
+
+ mCodec.configure(format, surface, crypto, flags);
+ }
+
+ @Override
+ public boolean isAdaptivePlaybackSupported(final String mimeType) {
+ return HardwareCodecCapabilityUtils.checkSupportsAdaptivePlayback(mCodec, mimeType);
+ }
+
+ @Override
+ public boolean isTunneledPlaybackSupported(final String mimeType) {
+ try {
+ return mCodec
+ .getCodecInfo()
+ .getCapabilitiesForType(mimeType)
+ .isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback);
+ } catch (final Exception e) {
+ return false;
+ }
+ }
+
+ private void assertCallbacks() {
+ if (mCallbackSender == null) {
+ throw new IllegalStateException(LOGTAG + ": callback must be supplied with setCallbacks().");
+ }
+ }
+
+ @Override
+ public void start() {
+ assertCallbacks();
+
+ mCodec.start();
+ mInputEnded = false;
+ mOutputEnded = false;
+ mInputBuffers = mCodec.getInputBuffers();
+ resumeReceivingInputs();
+ mOutputBuffers = mCodec.getOutputBuffers();
+ }
+
+ @Override
+ public void resumeReceivingInputs() {
+ for (int i = 0; i < mInputBuffers.length; i++) {
+ mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
+ }
+ }
+
+ @Override
+ public final void setBitrate(final int bps) {
+ final Bundle params = new Bundle();
+ params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bps);
+ mCodec.setParameters(params);
+ }
+
+ @Override
+ public final void queueInputBuffer(
+ final int index,
+ final int offset,
+ final int size,
+ final long presentationTimeUs,
+ final int flags) {
+ assertCallbacks();
+
+ mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+
+ if (((flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0)) {
+ final Bundle params = new Bundle();
+ params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
+ mCodec.setParameters(params);
+ }
+
+ try {
+ mCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
+ } catch (final IllegalStateException e) {
+ e.printStackTrace();
+ mCallbackSender.notifyError(ERROR_CODEC);
+ return;
+ }
+
+ mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS);
+ mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
+ }
+
+ @Override
+ public final void queueSecureInputBuffer(
+ final int index,
+ final int offset,
+ final MediaCodec.CryptoInfo cryptoInfo,
+ final long presentationTimeUs,
+ final int flags) {
+ assertCallbacks();
+
+ mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+
+ try {
+ mCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, flags);
+ } catch (final IllegalStateException e) {
+ e.printStackTrace();
+ mCallbackSender.notifyError(ERROR_CODEC);
+ return;
+ }
+
+ mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
+ mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS);
+ }
+
+ @Override
+ public final void releaseOutputBuffer(final int index, final boolean render) {
+ assertCallbacks();
+
+ mCodec.releaseOutputBuffer(index, render);
+ }
+
+ @Override
+ public final ByteBuffer getInputBuffer(final int index) {
+ assertCallbacks();
+
+ return mInputBuffers[index];
+ }
+
+ @Override
+ public final ByteBuffer getOutputBuffer(final int index) {
+ assertCallbacks();
+
+ return mOutputBuffers[index];
+ }
+
+ @Override
+ public MediaFormat getInputFormat() {
+ return null;
+ }
+
+ @Override
+ public void flush() {
+ assertCallbacks();
+
+ mInputEnded = false;
+ mOutputEnded = false;
+ cancelPendingTasks();
+ mCodec.flush();
+ }
+
+ private void cancelPendingTasks() {
+ mBufferPoller.cancel();
+ mCallbackSender.cancel();
+ }
+
+ @Override
+ public void stop() {
+ assertCallbacks();
+
+ cancelPendingTasks();
+ mCodec.stop();
+ }
+
+ @Override
+ public void release() {
+ assertCallbacks();
+
+ cancelPendingTasks();
+ mCallbackSender = null;
+ mCodec.release();
+ stopBufferPoller();
+ }
+
+ private void stopBufferPoller() {
+ if (mBufferPoller == null) {
+ Log.e(LOGTAG, "no initialized poller.");
+ return;
+ }
+
+ mBufferPoller.getLooper().quit();
+ mBufferPoller = null;
+
+ if (DEBUG) {
+ Log.d(LOGTAG, "stop poller " + this);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/LollipopAsyncCodec.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/LollipopAsyncCodec.java
new file mode 100644
index 0000000000..aaf8810bbb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/LollipopAsyncCodec.java
@@ -0,0 +1,248 @@
+/* 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.media;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.Surface;
+import androidx.annotation.NonNull;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
+
+/* package */ final class LollipopAsyncCodec implements AsyncCodec {
+ private final MediaCodec mCodec;
+
+ private class CodecCallback extends MediaCodec.Callback {
+ private final Forwarder mForwarder;
+
+ private class Forwarder extends Handler {
+ private static final int MSG_INPUT_BUFFER_AVAILABLE = 1;
+ private static final int MSG_OUTPUT_BUFFER_AVAILABLE = 2;
+ private static final int MSG_OUTPUT_FORMAT_CHANGE = 3;
+ private static final int MSG_ERROR = 4;
+
+ private final Callbacks mTarget;
+
+ private Forwarder(final Looper looper, final Callbacks target) {
+ super(looper);
+ mTarget = target;
+ }
+
+ @Override
+ public void handleMessage(final Message msg) {
+ switch (msg.what) {
+ case MSG_INPUT_BUFFER_AVAILABLE:
+ mTarget.onInputBufferAvailable(LollipopAsyncCodec.this, msg.arg1); // index
+ break;
+ case MSG_OUTPUT_BUFFER_AVAILABLE:
+ mTarget.onOutputBufferAvailable(
+ LollipopAsyncCodec.this,
+ msg.arg1, // index
+ (MediaCodec.BufferInfo) msg.obj); // buffer info
+ break;
+ case MSG_OUTPUT_FORMAT_CHANGE:
+ mTarget.onOutputFormatChanged(
+ LollipopAsyncCodec.this, (MediaFormat) msg.obj); // output format
+ break;
+ case MSG_ERROR:
+ mTarget.onError(LollipopAsyncCodec.this, msg.arg1); // error code
+ break;
+ default:
+ super.handleMessage(msg);
+ }
+ }
+
+ private void onInput(final int index) {
+ notify(obtainMessage(MSG_INPUT_BUFFER_AVAILABLE, index, 0));
+ }
+
+ private void notify(final Message msg) {
+ if (Looper.myLooper() == getLooper()) {
+ handleMessage(msg);
+ } else {
+ sendMessage(msg);
+ }
+ }
+
+ private void onOutput(final int index, final MediaCodec.BufferInfo info) {
+ final Message msg = obtainMessage(MSG_OUTPUT_BUFFER_AVAILABLE, index, 0, info);
+ notify(msg);
+ }
+
+ private void onOutputFormatChanged(final MediaFormat format) {
+ notify(obtainMessage(MSG_OUTPUT_FORMAT_CHANGE, format));
+ }
+
+ private void onError(final MediaCodec.CodecException e) {
+ e.printStackTrace();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ notify(obtainMessage(MSG_ERROR, e.getErrorCode()));
+ } else {
+ notify(obtainMessage(MSG_ERROR, e.getLocalizedMessage()));
+ }
+ }
+ }
+
+ private CodecCallback(final Callbacks callbacks, final Handler handler) {
+ Looper looper = (handler == null) ? null : handler.getLooper();
+ if (looper == null) {
+ // Use this thread if no handler supplied.
+ looper = Looper.myLooper();
+ }
+ if (looper == null) {
+ // This thread has no looper. Use main thread.
+ looper = Looper.getMainLooper();
+ }
+
+ mForwarder = new Forwarder(looper, callbacks);
+ }
+
+ @Override
+ public void onInputBufferAvailable(@NonNull final MediaCodec codec, final int index) {
+ mForwarder.onInput(index);
+ }
+
+ @Override
+ public void onOutputBufferAvailable(
+ @NonNull final MediaCodec codec,
+ final int index,
+ @NonNull final MediaCodec.BufferInfo info) {
+ mForwarder.onOutput(index, info);
+ }
+
+ @Override
+ public void onOutputFormatChanged(
+ @NonNull final MediaCodec codec, @NonNull final MediaFormat format) {
+ mForwarder.onOutputFormatChanged(format);
+ }
+
+ @Override
+ public void onError(
+ @NonNull final MediaCodec codec, @NonNull final MediaCodec.CodecException e) {
+ mForwarder.onError(e);
+ }
+ }
+
+ /* package */ LollipopAsyncCodec(final String name) throws IOException {
+ mCodec = MediaCodec.createByCodecName(name);
+ }
+
+ @Override
+ public void setCallbacks(final Callbacks callbacks, final Handler handler) {
+ if (callbacks == null) {
+ return;
+ }
+
+ mCodec.setCallback(new CodecCallback(callbacks, handler));
+ }
+
+ @Override
+ public void configure(
+ final MediaFormat format, final Surface surface, final MediaCrypto crypto, final int flags) {
+ mCodec.configure(format, surface, crypto, flags);
+ }
+
+ @Override
+ public boolean isAdaptivePlaybackSupported(final String mimeType) {
+ return HardwareCodecCapabilityUtils.checkSupportsAdaptivePlayback(mCodec, mimeType);
+ }
+
+ @Override
+ public boolean isTunneledPlaybackSupported(final String mimeType) {
+ try {
+ return mCodec
+ .getCodecInfo()
+ .getCapabilitiesForType(mimeType)
+ .isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback);
+ } catch (final Exception e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void start() {
+ mCodec.start();
+ }
+
+ @Override
+ public void stop() {
+ mCodec.stop();
+ }
+
+ @Override
+ public void flush() {
+ mCodec.flush();
+ }
+
+ @Override
+ public void resumeReceivingInputs() {
+ mCodec.start();
+ }
+
+ @Override
+ public void setBitrate(final int bps) {
+ final Bundle params = new Bundle();
+ params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bps);
+ mCodec.setParameters(params);
+ }
+
+ @Override
+ public void release() {
+ mCodec.release();
+ }
+
+ @Override
+ public ByteBuffer getInputBuffer(final int index) {
+ return mCodec.getInputBuffer(index);
+ }
+
+ @Override
+ public ByteBuffer getOutputBuffer(final int index) {
+ return mCodec.getOutputBuffer(index);
+ }
+
+ @Override
+ public MediaFormat getInputFormat() {
+ return mCodec.getInputFormat();
+ }
+
+ @Override
+ public void queueInputBuffer(
+ final int index,
+ final int offset,
+ final int size,
+ final long presentationTimeUs,
+ final int flags) {
+ if ((flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
+ final Bundle params = new Bundle();
+ params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
+ mCodec.setParameters(params);
+ }
+ mCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
+ }
+
+ @Override
+ public void queueSecureInputBuffer(
+ final int index,
+ final int offset,
+ final MediaCodec.CryptoInfo info,
+ final long presentationTimeUs,
+ final int flags) {
+ mCodec.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags);
+ }
+
+ @Override
+ public void releaseOutputBuffer(final int index, final boolean render) {
+ mCodec.releaseOutputBuffer(index, render);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/MediaDrmProxy.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/MediaDrmProxy.java
new file mode 100644
index 0000000000..1bfab37063
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/MediaDrmProxy.java
@@ -0,0 +1,297 @@
+/* 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.media;
+
+import android.annotation.SuppressLint;
+import android.media.MediaCrypto;
+import android.media.MediaDrm;
+import android.os.Build;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.UUID;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+
+public final class MediaDrmProxy {
+ private static final String LOGTAG = "GeckoMediaDrmProxy";
+ private static final boolean DEBUG = false;
+ private static final UUID WIDEVINE_SCHEME_UUID =
+ new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
+
+ private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha";
+ @WrapForJNI private static final String AAC = "audio/mp4a-latm";
+ @WrapForJNI private static final String AVC = "video/avc";
+ @WrapForJNI private static final String VORBIS = "audio/vorbis";
+ @WrapForJNI private static final String VP8 = "video/x-vnd.on2.vp8";
+ @WrapForJNI private static final String VP9 = "video/x-vnd.on2.vp9";
+ @WrapForJNI private static final String OPUS = "audio/opus";
+ @WrapForJNI private static final String FLAC = "audio/flac";
+
+ public static final ArrayList<MediaDrmProxy> sProxyList = new ArrayList<MediaDrmProxy>();
+
+ // A flag to avoid using the native object that has been destroyed.
+ private boolean mDestroyed;
+ private GeckoMediaDrm mImpl;
+ private String mDrmStubId;
+
+ private static boolean isSystemSupported() {
+ // Support versions >= Marshmallow
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ if (DEBUG)
+ Log.d(LOGTAG, "System Not supported !!, current SDK version is " + Build.VERSION.SDK_INT);
+ return false;
+ }
+ return true;
+ }
+
+ @SuppressLint("NewApi")
+ @WrapForJNI
+ public static boolean isSchemeSupported(final String keySystem) {
+ if (!isSystemSupported()) {
+ return false;
+ }
+ if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) {
+ return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID)
+ && MediaCrypto.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID);
+ }
+ if (DEBUG) Log.d(LOGTAG, "isSchemeSupported key sytem = " + keySystem);
+ return false;
+ }
+
+ @SuppressLint("NewApi")
+ @WrapForJNI
+ public static boolean IsCryptoSchemeSupported(final String keySystem, final String container) {
+ if (!isSystemSupported()) {
+ return false;
+ }
+ if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) {
+ return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID, container);
+ }
+ if (DEBUG)
+ Log.d(LOGTAG, "cannot decrypt key sytem = " + keySystem + ", container = " + container);
+ return false;
+ }
+
+ // Interface for callback to native.
+ public interface Callbacks {
+ void onSessionCreated(int createSessionToken, int promiseId, byte[] sessionId, byte[] request);
+
+ void onSessionUpdated(int promiseId, byte[] sessionId);
+
+ void onSessionClosed(int promiseId, byte[] sessionId);
+
+ void onSessionMessage(byte[] sessionId, int sessionMessageType, byte[] request);
+
+ void onSessionError(byte[] sessionId, String message);
+
+ // MediaDrm.KeyStatus is available in API level 23(M)
+ // https://developer.android.com/reference/android/media/MediaDrm.KeyStatus.html
+ // For compatibility between L and M above, we'll unwrap the KeyStatus structure
+ // and store the keyid and status into SessionKeyInfo and pass to native(MediaDrmCDMProxy).
+ void onSessionBatchedKeyChanged(byte[] sessionId, SessionKeyInfo[] keyInfos);
+
+ void onRejectPromise(int promiseId, String message);
+ } // Callbacks
+
+ public static class NativeMediaDrmProxyCallbacks extends JNIObject implements Callbacks {
+ @WrapForJNI(calledFrom = "gecko")
+ NativeMediaDrmProxyCallbacks() {}
+
+ @Override
+ @WrapForJNI(dispatchTo = "gecko")
+ public native void onSessionCreated(
+ int createSessionToken, int promiseId, byte[] sessionId, byte[] request);
+
+ @Override
+ @WrapForJNI(dispatchTo = "gecko")
+ public native void onSessionUpdated(int promiseId, byte[] sessionId);
+
+ @Override
+ @WrapForJNI(dispatchTo = "gecko")
+ public native void onSessionClosed(int promiseId, byte[] sessionId);
+
+ @Override
+ @WrapForJNI(dispatchTo = "gecko")
+ public native void onSessionMessage(byte[] sessionId, int sessionMessageType, byte[] request);
+
+ @Override
+ @WrapForJNI(dispatchTo = "gecko")
+ public native void onSessionError(byte[] sessionId, String message);
+
+ @Override
+ @WrapForJNI(dispatchTo = "gecko")
+ public native void onSessionBatchedKeyChanged(byte[] sessionId, SessionKeyInfo[] keyInfos);
+
+ @Override
+ @WrapForJNI(dispatchTo = "gecko")
+ public native void onRejectPromise(int promiseId, String message);
+
+ @Override // JNIObject
+ protected void disposeNative() {
+ throw new UnsupportedOperationException();
+ }
+ } // NativeMediaDrmProxyCallbacks
+
+ // A proxy to callback from LocalMediaDrmBridge to native instance.
+ public static class MediaDrmProxyCallbacks implements GeckoMediaDrm.Callbacks {
+ private final Callbacks mNativeCallbacks;
+ private final MediaDrmProxy mProxy;
+
+ public MediaDrmProxyCallbacks(final MediaDrmProxy proxy, final Callbacks callbacks) {
+ mNativeCallbacks = callbacks;
+ mProxy = proxy;
+ }
+
+ @Override
+ public void onSessionCreated(
+ final int createSessionToken,
+ final int promiseId,
+ final byte[] sessionId,
+ final byte[] request) {
+ if (!mProxy.isDestroyed()) {
+ mNativeCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request);
+ }
+ }
+
+ @Override
+ public void onSessionUpdated(final int promiseId, final byte[] sessionId) {
+ if (!mProxy.isDestroyed()) {
+ mNativeCallbacks.onSessionUpdated(promiseId, sessionId);
+ }
+ }
+
+ @Override
+ public void onSessionClosed(final int promiseId, final byte[] sessionId) {
+ if (!mProxy.isDestroyed()) {
+ mNativeCallbacks.onSessionClosed(promiseId, sessionId);
+ }
+ }
+
+ @Override
+ public void onSessionMessage(
+ final byte[] sessionId, final int sessionMessageType, final byte[] request) {
+ if (!mProxy.isDestroyed()) {
+ mNativeCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
+ }
+ }
+
+ @Override
+ public void onSessionError(final byte[] sessionId, final String message) {
+ if (!mProxy.isDestroyed()) {
+ mNativeCallbacks.onSessionError(sessionId, message);
+ }
+ }
+
+ @Override
+ public void onSessionBatchedKeyChanged(
+ final byte[] sessionId, final SessionKeyInfo[] keyInfos) {
+ if (!mProxy.isDestroyed()) {
+ mNativeCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
+ }
+ }
+
+ @Override
+ public void onRejectPromise(final int promiseId, final String message) {
+ if (!mProxy.isDestroyed()) {
+ mNativeCallbacks.onRejectPromise(promiseId, message);
+ }
+ }
+ } // MediaDrmProxyCallbacks
+
+ public boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static MediaDrmProxy create(final String keySystem, final Callbacks nativeCallbacks) {
+ return new MediaDrmProxy(keySystem, nativeCallbacks);
+ }
+
+ MediaDrmProxy(final String keySystem, final Callbacks nativeCallbacks) {
+ if (DEBUG) Log.d(LOGTAG, "Constructing MediaDrmProxy");
+ try {
+ mDrmStubId = UUID.randomUUID().toString();
+ final IMediaDrmBridge remoteBridge =
+ RemoteManager.getInstance().createRemoteMediaDrmBridge(keySystem, mDrmStubId);
+ mImpl = new RemoteMediaDrmBridge(remoteBridge);
+ mImpl.setCallbacks(new MediaDrmProxyCallbacks(this, nativeCallbacks));
+ sProxyList.add(this);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Constructing MediaDrmProxy ... error", e);
+ }
+ }
+
+ @WrapForJNI
+ private void createSession(
+ final int createSessionToken,
+ final int promiseId,
+ final String initDataType,
+ final byte[] initData) {
+ if (DEBUG) Log.d(LOGTAG, "createSession, promiseId = " + promiseId);
+ mImpl.createSession(createSessionToken, promiseId, initDataType, initData);
+ }
+
+ @WrapForJNI
+ private void updateSession(final int promiseId, final String sessionId, final byte[] response) {
+ if (DEBUG)
+ Log.d(LOGTAG, "updateSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")");
+ mImpl.updateSession(promiseId, sessionId, response);
+ }
+
+ @WrapForJNI
+ private void closeSession(final int promiseId, final String sessionId) {
+ if (DEBUG)
+ Log.d(LOGTAG, "closeSession, primiseId(" + promiseId + "sessionId(" + sessionId + ")");
+ mImpl.closeSession(promiseId, sessionId);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private String getStubId() {
+ return mDrmStubId;
+ }
+
+ @WrapForJNI
+ public boolean setServerCertificate(final byte[] cert) {
+ try {
+ mImpl.setServerCertificate(cert);
+ return true;
+ } catch (final RuntimeException e) {
+ return false;
+ }
+ }
+
+ // Get corresponding MediaCrypto object by a generated UUID for MediaCodec.
+ // Will be called on MediaFormatReader's TaskQueue.
+ @WrapForJNI
+ public static MediaCrypto getMediaCrypto(final String stubId) {
+ for (final MediaDrmProxy proxy : sProxyList) {
+ if (proxy.getStubId().equals(stubId)) {
+ return proxy.getMediaCryptoFromBridge();
+ }
+ }
+ if (DEBUG) Log.d(LOGTAG, " NULL crytpo ");
+ return null;
+ }
+
+ @WrapForJNI // Called when natvie object is destroyed.
+ private void destroy() {
+ if (DEBUG) Log.d(LOGTAG, "destroy!! Native object is destroyed.");
+ if (mDestroyed) {
+ return;
+ }
+ mDestroyed = true;
+ release();
+ }
+
+ private void release() {
+ if (DEBUG) Log.d(LOGTAG, "release");
+ sProxyList.remove(this);
+ mImpl.release();
+ }
+
+ private MediaCrypto getMediaCryptoFromBridge() {
+ return mImpl != null ? mImpl.getMediaCrypto() : null;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/MediaManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/MediaManager.java
new file mode 100644
index 0000000000..ef4fdc6932
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/MediaManager.java
@@ -0,0 +1,79 @@
+/* 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.media;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+import org.mozilla.gecko.mozglue.GeckoLoader;
+import org.mozilla.geckoview.BuildConfig;
+
+public final class MediaManager extends Service {
+ private static final String LOGTAG = "GeckoMediaManager";
+ private static final boolean DEBUG = !BuildConfig.MOZILLA_OFFICIAL;
+ private static boolean sNativeLibLoaded;
+ private int mNumActiveRequests = 0;
+
+ private Binder mBinder =
+ new IMediaManager.Stub() {
+ @Override
+ public ICodec createCodec() throws RemoteException {
+ if (DEBUG) Log.d(LOGTAG, "request codec. Current active requests:" + mNumActiveRequests);
+ mNumActiveRequests++;
+ return new Codec();
+ }
+
+ @Override
+ public IMediaDrmBridge createRemoteMediaDrmBridge(
+ final String keySystem, final String stubId) throws RemoteException {
+ if (DEBUG)
+ Log.d(LOGTAG, "request DRM bridge. Current active requests:" + mNumActiveRequests);
+ mNumActiveRequests++;
+ return new RemoteMediaDrmBridgeStub(keySystem, stubId);
+ }
+
+ @Override
+ public void endRequest() {
+ if (DEBUG) Log.d(LOGTAG, "end request. Current active requests:" + mNumActiveRequests);
+ if (mNumActiveRequests > 0) {
+ mNumActiveRequests--;
+ } else {
+ final RuntimeException e =
+ new RuntimeException("unmatched codec/DRM bridge creation and ending calls!");
+ Log.e(LOGTAG, "Error:", e);
+ }
+ }
+ };
+
+ @Override
+ public synchronized void onCreate() {
+ if (!sNativeLibLoaded) {
+ GeckoLoader.doLoadLibrary(this, "mozglue");
+ GeckoLoader.suppressCrashDialog();
+ sNativeLibLoaded = true;
+ }
+ }
+
+ @Override
+ public IBinder onBind(final Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public boolean onUnbind(final Intent intent) {
+ Log.i(LOGTAG, "Media service has been unbound. Stopping.");
+ stopSelf();
+ if (mNumActiveRequests != 0) {
+ // Not unbound by RemoteManager -- caller process is dead.
+ Log.w(LOGTAG, "unbound while client still active.");
+ Process.killProcess(Process.myPid());
+ }
+ return false;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteManager.java
new file mode 100644
index 0000000000..7a2e74c9af
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteManager.java
@@ -0,0 +1,248 @@
+/* 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.media;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.media.MediaFormat;
+import android.os.DeadObjectException;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.GeckoSurface;
+
+public final class RemoteManager implements IBinder.DeathRecipient {
+ private static final String LOGTAG = "GeckoRemoteManager";
+ private static final boolean DEBUG = false;
+ private static RemoteManager sRemoteManager = null;
+
+ public static synchronized RemoteManager getInstance() {
+ if (sRemoteManager == null) {
+ sRemoteManager = new RemoteManager();
+ }
+
+ sRemoteManager.init();
+ return sRemoteManager;
+ }
+
+ private List<CodecProxy> mCodecs = new LinkedList<CodecProxy>();
+ private List<IMediaDrmBridge> mDrmBridges = new LinkedList<IMediaDrmBridge>();
+
+ private volatile IMediaManager mRemote;
+
+ private final class RemoteConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(final ComponentName name, final IBinder service) {
+ if (DEBUG) Log.d(LOGTAG, "service connected");
+ try {
+ service.linkToDeath(RemoteManager.this, 0);
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ }
+ synchronized (this) {
+ mRemote = IMediaManager.Stub.asInterface(service);
+ notify();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ if (DEBUG) Log.d(LOGTAG, "service disconnected");
+ unlink();
+ }
+
+ private boolean connect() {
+ final Context appCtxt = GeckoAppShell.getApplicationContext();
+ appCtxt.bindService(
+ new Intent(appCtxt, MediaManager.class),
+ mConnection,
+ Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT);
+ waitConnect();
+ return mRemote != null;
+ }
+
+ // Wait up to 5s.
+ private synchronized void waitConnect() {
+ int waitCount = 0;
+ while (mRemote == null && waitCount < 5) {
+ try {
+ wait(1000);
+ waitCount++;
+ } catch (final InterruptedException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ }
+ }
+ if (DEBUG) {
+ Log.d(
+ LOGTAG,
+ "wait ~" + waitCount + "s for connection: " + (mRemote == null ? "fail" : "ok"));
+ }
+ }
+
+ private synchronized void waitDisconnect() {
+ while (mRemote != null) {
+ try {
+ wait(1000);
+ } catch (final InterruptedException e) {
+ if (DEBUG) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ private synchronized void unlink() {
+ if (mRemote == null) {
+ return;
+ }
+ try {
+ mRemote.asBinder().unlinkToDeath(RemoteManager.this, 0);
+ } catch (final NoSuchElementException e) {
+ Log.w(LOGTAG, "death recipient already released");
+ }
+ mRemote = null;
+ notify();
+ }
+ }
+
+ RemoteConnection mConnection = new RemoteConnection();
+
+ private synchronized boolean init() {
+ if (mRemote != null) {
+ return true;
+ }
+
+ if (DEBUG) Log.d(LOGTAG, "init remote manager " + this);
+ return mConnection.connect();
+ }
+
+ public synchronized CodecProxy createCodec(
+ final boolean isEncoder,
+ final MediaFormat format,
+ final GeckoSurface surface,
+ final CodecProxy.Callbacks callbacks,
+ final String drmStubId) {
+ if (mRemote == null) {
+ if (DEBUG) Log.d(LOGTAG, "createCodec failed due to not initialize");
+ return null;
+ }
+ try {
+ final ICodec remote = mRemote.createCodec();
+ final CodecProxy proxy =
+ CodecProxy.createCodecProxy(isEncoder, format, surface, callbacks, drmStubId);
+ if (proxy.init(remote)) {
+ mCodecs.add(proxy);
+ return proxy;
+ } else {
+ return null;
+ }
+ } catch (final RemoteException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public synchronized IMediaDrmBridge createRemoteMediaDrmBridge(
+ final String keySystem, final String stubId) {
+ if (mRemote == null) {
+ if (DEBUG) Log.d(LOGTAG, "createRemoteMediaDrmBridge failed due to not initialize");
+ return null;
+ }
+ try {
+ final IMediaDrmBridge remoteBridge = mRemote.createRemoteMediaDrmBridge(keySystem, stubId);
+ mDrmBridges.add(remoteBridge);
+ return remoteBridge;
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Got exception during createRemoteMediaDrmBridge().", e);
+ return null;
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ Log.e(LOGTAG, "remote codec is dead");
+ handleRemoteDeath();
+ }
+
+ private synchronized void handleRemoteDeath() {
+ mConnection.waitDisconnect();
+
+ notifyError(!(init() && recoverRemoteCodec()));
+ }
+
+ private synchronized void notifyError(final boolean fatal) {
+ for (final CodecProxy proxy : mCodecs) {
+ proxy.reportError(fatal);
+ }
+ }
+
+ private synchronized boolean recoverRemoteCodec() {
+ if (DEBUG) Log.d(LOGTAG, "recover codec");
+ boolean ok = true;
+ try {
+ for (final CodecProxy proxy : mCodecs) {
+ ok &= proxy.init(mRemote.createCodec());
+ }
+ return ok;
+ } catch (final RemoteException e) {
+ return false;
+ }
+ }
+
+ public void releaseCodec(final CodecProxy proxy) throws DeadObjectException, RemoteException {
+ if (mRemote == null) {
+ if (DEBUG) Log.d(LOGTAG, "releaseCodec called but not initialized yet");
+ return;
+ }
+ proxy.deinit();
+ synchronized (this) {
+ if (mCodecs.remove(proxy)) {
+ try {
+ mRemote.endRequest();
+ releaseIfNeeded();
+ } catch (final RemoteException | NullPointerException e) {
+ Log.e(LOGTAG, "fail to report remote codec disconnection");
+ }
+ }
+ }
+ }
+
+ private void releaseIfNeeded() {
+ if (!mCodecs.isEmpty() || !mDrmBridges.isEmpty()) {
+ return;
+ }
+
+ if (DEBUG) Log.d(LOGTAG, "release remote manager " + this);
+ mConnection.unlink();
+ final Context appCtxt = GeckoAppShell.getApplicationContext();
+ appCtxt.unbindService(mConnection);
+ }
+
+ public void onRemoteMediaDrmBridgeReleased(final IMediaDrmBridge remote) {
+ if (!mDrmBridges.contains(remote)) {
+ Log.e(LOGTAG, "Try to release unknown remote MediaDrm bridge: " + remote);
+ return;
+ }
+
+ synchronized (this) {
+ if (mDrmBridges.remove(remote)) {
+ try {
+ mRemote.endRequest();
+ releaseIfNeeded();
+ } catch (final RemoteException | NullPointerException e) {
+ Log.e(LOGTAG, "Fail to report remote DRM bridge disconnection");
+ }
+ }
+ }
+ }
+} // RemoteManager
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java
new file mode 100644
index 0000000000..b90f720300
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteMediaDrmBridge.java
@@ -0,0 +1,163 @@
+/* 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.media;
+
+import android.media.MediaCrypto;
+import android.util.Log;
+
+final class RemoteMediaDrmBridge implements GeckoMediaDrm {
+ private static final String LOGTAG = "RemoteMediaDrmBridge";
+ private static final boolean DEBUG = false;
+ private CallbacksForwarder mCallbacksFwd;
+ private IMediaDrmBridge mRemote;
+
+ // Forward callbacks from remote bridge stub to MediaDrmProxy.
+ private static class CallbacksForwarder extends IMediaDrmBridgeCallbacks.Stub {
+ private final GeckoMediaDrm.Callbacks mProxyCallbacks;
+
+ CallbacksForwarder(final Callbacks callbacks) {
+ assertTrue(callbacks != null);
+ mProxyCallbacks = callbacks;
+ }
+
+ @Override
+ public void onSessionCreated(
+ final int createSessionToken,
+ final int promiseId,
+ final byte[] sessionId,
+ final byte[] request) {
+ mProxyCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request);
+ }
+
+ @Override
+ public void onSessionUpdated(final int promiseId, final byte[] sessionId) {
+ mProxyCallbacks.onSessionUpdated(promiseId, sessionId);
+ }
+
+ @Override
+ public void onSessionClosed(final int promiseId, final byte[] sessionId) {
+ mProxyCallbacks.onSessionClosed(promiseId, sessionId);
+ }
+
+ @Override
+ public void onSessionMessage(
+ final byte[] sessionId, final int sessionMessageType, final byte[] request) {
+ mProxyCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
+ }
+
+ @Override
+ public void onSessionError(final byte[] sessionId, final String message) {
+ mProxyCallbacks.onSessionError(sessionId, message);
+ }
+
+ @Override
+ public void onSessionBatchedKeyChanged(
+ final byte[] sessionId, final SessionKeyInfo[] keyInfos) {
+ mProxyCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
+ }
+
+ @Override
+ public void onRejectPromise(final int promiseId, final String message) {
+ mProxyCallbacks.onRejectPromise(promiseId, message);
+ }
+ } // CallbacksForwarder
+
+ /* package-private */ static void assertTrue(final boolean condition) {
+ if (DEBUG && !condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ public RemoteMediaDrmBridge(final IMediaDrmBridge remoteBridge) {
+ assertTrue(remoteBridge != null);
+ mRemote = remoteBridge;
+ }
+
+ @Override
+ public synchronized void setCallbacks(final Callbacks callbacks) {
+ if (DEBUG) Log.d(LOGTAG, "setCallbacks()");
+ assertTrue(callbacks != null);
+ assertTrue(mRemote != null);
+
+ mCallbacksFwd = new CallbacksForwarder(callbacks);
+ try {
+ mRemote.setCallbacks(mCallbacksFwd);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Got exception during setCallbacks", e);
+ }
+ }
+
+ @Override
+ public synchronized void createSession(
+ final int createSessionToken,
+ final int promiseId,
+ final String initDataType,
+ final byte[] initData) {
+ if (DEBUG) Log.d(LOGTAG, "createSession()");
+
+ try {
+ mRemote.createSession(createSessionToken, promiseId, initDataType, initData);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Got exception while creating remote session.", e);
+ mCallbacksFwd.onRejectPromise(promiseId, "Failed to create session.");
+ }
+ }
+
+ @Override
+ public synchronized void updateSession(
+ final int promiseId, final String sessionId, final byte[] response) {
+ if (DEBUG) Log.d(LOGTAG, "updateSession()");
+
+ try {
+ mRemote.updateSession(promiseId, sessionId, response);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Got exception while updating remote session.", e);
+ mCallbacksFwd.onRejectPromise(promiseId, "Failed to update session.");
+ }
+ }
+
+ @Override
+ public synchronized void closeSession(final int promiseId, final String sessionId) {
+ if (DEBUG) Log.d(LOGTAG, "closeSession()");
+
+ try {
+ mRemote.closeSession(promiseId, sessionId);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Got exception while closing remote session.", e);
+ mCallbacksFwd.onRejectPromise(promiseId, "Failed to close session.");
+ }
+ }
+
+ @Override
+ public synchronized void release() {
+ if (DEBUG) Log.d(LOGTAG, "release()");
+
+ try {
+ mRemote.release();
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Got exception while releasing RemoteDrmBridge.", e);
+ }
+ RemoteManager.getInstance().onRemoteMediaDrmBridgeReleased(mRemote);
+ mRemote = null;
+ mCallbacksFwd = null;
+ }
+
+ @Override
+ public synchronized MediaCrypto getMediaCrypto() {
+ if (DEBUG) Log.d(LOGTAG, "getMediaCrypto(), should not enter here!");
+ assertTrue(false);
+ return null;
+ }
+
+ @Override
+ public synchronized void setServerCertificate(final byte[] cert) {
+ try {
+ mRemote.setServerCertificate(cert);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Got exception while setting server certificate.", e);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java
new file mode 100644
index 0000000000..8f9e42fde1
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/RemoteMediaDrmBridgeStub.java
@@ -0,0 +1,248 @@
+/* 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.media;
+
+import android.media.MediaCrypto;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import java.util.ArrayList;
+
+final class RemoteMediaDrmBridgeStub extends IMediaDrmBridge.Stub
+ implements IBinder.DeathRecipient {
+ private static final String LOGTAG = "RemoteDrmBridgeStub";
+ private static final boolean DEBUG = false;
+ private volatile IMediaDrmBridgeCallbacks mCallbacks = null;
+
+ // Underlying bridge implmenetaion, i.e. GeckoMediaDrmBrdigeV21.
+ private GeckoMediaDrm mBridge = null;
+
+ // mStubId is initialized during stub construction. It should be a unique
+ // string which is generated in MediaDrmProxy in Fennec App process and is
+ // used for Codec to obtain corresponding MediaCrypto as input to achieve
+ // decryption.
+ // The generated stubId will be delivered to Codec via a code path starting
+ // from MediaDrmProxy -> MediaDrmCDMProxy -> RemoteDataDecoder => IPC => Codec.
+ private String mStubId = "";
+
+ public static final ArrayList<RemoteMediaDrmBridgeStub> mBridgeStubs =
+ new ArrayList<RemoteMediaDrmBridgeStub>();
+
+ private String getId() {
+ return mStubId;
+ }
+
+ private MediaCrypto getMediaCryptoFromBridge() {
+ return mBridge != null ? mBridge.getMediaCrypto() : null;
+ }
+
+ public static synchronized MediaCrypto getMediaCrypto(final String stubId) {
+ if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
+
+ for (int i = 0; i < mBridgeStubs.size(); i++) {
+ if (mBridgeStubs.get(i) != null && mBridgeStubs.get(i).getId().equals(stubId)) {
+ return mBridgeStubs.get(i).getMediaCryptoFromBridge();
+ }
+ }
+ return null;
+ }
+
+ // Callback to RemoteMediaDrmBridge.
+ private final class Callbacks implements GeckoMediaDrm.Callbacks {
+ private IMediaDrmBridgeCallbacks mRemoteCallbacks;
+
+ public Callbacks(final IMediaDrmBridgeCallbacks remote) {
+ mRemoteCallbacks = remote;
+ }
+
+ @Override
+ public void onSessionCreated(
+ final int createSessionToken,
+ final int promiseId,
+ final byte[] sessionId,
+ final byte[] request) {
+ if (DEBUG) Log.d(LOGTAG, "onSessionCreated()");
+ try {
+ mRemoteCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
+ }
+ }
+
+ @Override
+ public void onSessionUpdated(final int promiseId, final byte[] sessionId) {
+ if (DEBUG) Log.d(LOGTAG, "onSessionUpdated()");
+ try {
+ mRemoteCallbacks.onSessionUpdated(promiseId, sessionId);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
+ }
+ }
+
+ @Override
+ public void onSessionClosed(final int promiseId, final byte[] sessionId) {
+ if (DEBUG) Log.d(LOGTAG, "onSessionClosed()");
+ try {
+ mRemoteCallbacks.onSessionClosed(promiseId, sessionId);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
+ }
+ }
+
+ @Override
+ public void onSessionMessage(
+ final byte[] sessionId, final int sessionMessageType, final byte[] request) {
+ if (DEBUG) Log.d(LOGTAG, "onSessionMessage()");
+ try {
+ mRemoteCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
+ }
+ }
+
+ @Override
+ public void onSessionError(final byte[] sessionId, final String message) {
+ if (DEBUG) Log.d(LOGTAG, "onSessionError()");
+ try {
+ mRemoteCallbacks.onSessionError(sessionId, message);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
+ }
+ }
+
+ @Override
+ public void onSessionBatchedKeyChanged(
+ final byte[] sessionId, final SessionKeyInfo[] keyInfos) {
+ if (DEBUG) Log.d(LOGTAG, "onSessionBatchedKeyChanged()");
+ try {
+ mRemoteCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
+ }
+ }
+
+ @Override
+ public void onRejectPromise(final int promiseId, final String message) {
+ if (DEBUG) Log.d(LOGTAG, "onRejectPromise()");
+ try {
+ mRemoteCallbacks.onRejectPromise(promiseId, message);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
+ }
+ }
+ }
+
+ /* package-private */ void assertTrue(final boolean condition) {
+ if (DEBUG && !condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ RemoteMediaDrmBridgeStub(final String keySystem, final String stubId) throws RemoteException {
+ try {
+ if (Build.VERSION.SDK_INT < 23) {
+ mBridge = new GeckoMediaDrmBridgeV21(keySystem);
+ } else {
+ mBridge = new GeckoMediaDrmBridgeV23(keySystem);
+ }
+ mStubId = stubId;
+ mBridgeStubs.add(this);
+ } catch (final Exception e) {
+ throw new RemoteException("RemoteMediaDrmBridgeStub cannot create bridge implementation.");
+ }
+ }
+
+ @Override
+ public synchronized void setCallbacks(final IMediaDrmBridgeCallbacks callbacks)
+ throws RemoteException {
+ if (DEBUG) Log.d(LOGTAG, "setCallbacks()");
+ assertTrue(mBridge != null);
+ assertTrue(callbacks != null);
+ mCallbacks = callbacks;
+ callbacks.asBinder().linkToDeath(this, 0);
+ mBridge.setCallbacks(new Callbacks(mCallbacks));
+ }
+
+ @Override
+ public synchronized void createSession(
+ final int createSessionToken,
+ final int promiseId,
+ final String initDataType,
+ final byte[] initData)
+ throws RemoteException {
+ if (DEBUG) Log.d(LOGTAG, "createSession()");
+ try {
+ assertTrue(mCallbacks != null);
+ assertTrue(mBridge != null);
+ mBridge.createSession(createSessionToken, promiseId, initDataType, initData);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Failed to createSession.", e);
+ mCallbacks.onRejectPromise(promiseId, "Failed to createSession.");
+ }
+ }
+
+ @Override
+ public synchronized void updateSession(
+ final int promiseId, final String sessionId, final byte[] response) throws RemoteException {
+ if (DEBUG) Log.d(LOGTAG, "updateSession()");
+ try {
+ assertTrue(mCallbacks != null);
+ assertTrue(mBridge != null);
+ mBridge.updateSession(promiseId, sessionId, response);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Failed to updateSession.", e);
+ mCallbacks.onRejectPromise(promiseId, "Failed to updateSession.");
+ }
+ }
+
+ @Override
+ public synchronized void closeSession(final int promiseId, final String sessionId)
+ throws RemoteException {
+ if (DEBUG) Log.d(LOGTAG, "closeSession()");
+ try {
+ assertTrue(mCallbacks != null);
+ assertTrue(mBridge != null);
+ mBridge.closeSession(promiseId, sessionId);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Failed to closeSession.", e);
+ mCallbacks.onRejectPromise(promiseId, "Failed to closeSession.");
+ }
+ }
+
+ // IBinder.DeathRecipient
+ @Override
+ public synchronized void binderDied() {
+ Log.e(LOGTAG, "Binder died !!");
+ try {
+ release();
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Exception ! Dead recipient !!", e);
+ }
+ }
+
+ @Override
+ public synchronized void release() {
+ if (DEBUG) Log.d(LOGTAG, "release()");
+ mBridgeStubs.remove(this);
+ if (mBridge != null) {
+ mBridge.release();
+ mBridge = null;
+ }
+ mCallbacks.asBinder().unlinkToDeath(this, 0);
+ mCallbacks = null;
+ mStubId = "";
+ }
+
+ @Override
+ public synchronized void setServerCertificate(final byte[] cert) {
+ try {
+ mBridge.setServerCertificate(cert);
+ } catch (final IllegalStateException e) {
+ Log.e(LOGTAG, "Failed to setServerCertificate.", e);
+ throw e;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Sample.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Sample.java
new file mode 100644
index 0000000000..baa6737427
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Sample.java
@@ -0,0 +1,291 @@
+/* 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.media;
+
+import android.annotation.SuppressLint;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CryptoInfo;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.ChecksSdkIntAtLeast;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+// Parcelable carrying input/output sample data and info cross process.
+public final class Sample implements Parcelable {
+ public static final Sample EOS;
+
+ static {
+ final BufferInfo eosInfo = new BufferInfo();
+ EOS = new Sample();
+ EOS.info.set(0, 0, Long.MIN_VALUE, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ }
+
+ @WrapForJNI public long session;
+
+ public static final int NO_BUFFER = -1;
+
+ public int bufferId = NO_BUFFER;
+ @WrapForJNI public BufferInfo info = new BufferInfo();
+ public CryptoInfo cryptoInfo;
+
+ // Simple Linked list for recycling objects.
+ // Used to nodify Sample objects. Do not marshal/unmarshal.
+ private Sample mNext;
+ private static Sample sPool = new Sample();
+ private static int sPoolSize = 1;
+
+ private Sample() {}
+
+ private void readInfo(final Parcel in) {
+ final int offset = in.readInt();
+ final int size = in.readInt();
+ final long pts = in.readLong();
+ final int flags = in.readInt();
+
+ info.set(offset, size, pts, flags);
+ }
+
+ private void readCrypto(final Parcel in) {
+ final int hasCryptoInfo = in.readInt();
+ if (hasCryptoInfo == 0) {
+ cryptoInfo = null;
+ return;
+ }
+
+ final byte[] iv = in.createByteArray();
+ final byte[] key = in.createByteArray();
+ final int mode = in.readInt();
+ final int[] numBytesOfClearData = in.createIntArray();
+ final int[] numBytesOfEncryptedData = in.createIntArray();
+ final int numSubSamples = in.readInt();
+
+ if (cryptoInfo == null) {
+ cryptoInfo = new CryptoInfo();
+ }
+ cryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv, mode);
+ if (supportsCryptoPattern()) {
+ final int numEncryptBlocks = in.readInt();
+ final int numSkipBlocks = in.readInt();
+ cryptoInfo.setPattern(new CryptoInfo.Pattern(numEncryptBlocks, numSkipBlocks));
+ }
+ }
+
+ public Sample set(final BufferInfo info, final CryptoInfo cryptoInfo) {
+ setBufferInfo(info);
+ setCryptoInfo(cryptoInfo);
+ return this;
+ }
+
+ public void setBufferInfo(final BufferInfo info) {
+ this.info.set(0, info.size, info.presentationTimeUs, info.flags);
+ }
+
+ public void setCryptoInfo(final CryptoInfo crypto) {
+ if (crypto == null) {
+ cryptoInfo = null;
+ return;
+ }
+
+ if (cryptoInfo == null) {
+ cryptoInfo = new CryptoInfo();
+ }
+ cryptoInfo.set(
+ crypto.numSubSamples,
+ crypto.numBytesOfClearData,
+ crypto.numBytesOfEncryptedData,
+ crypto.key,
+ crypto.iv,
+ crypto.mode);
+ if (supportsCryptoPattern()) {
+ final CryptoInfo.Pattern pattern = getCryptoPatternCompat(crypto);
+ if (pattern == null) {
+ return;
+ }
+ cryptoInfo.setPattern(pattern);
+ }
+ }
+
+ @WrapForJNI
+ public void dispose() {
+ if (isEOS()) {
+ return;
+ }
+
+ bufferId = NO_BUFFER;
+ info.set(0, 0, 0, 0);
+ if (cryptoInfo != null) {
+ cryptoInfo.set(0, null, null, null, null, 0);
+ }
+
+ // Recycle it.
+ synchronized (CREATOR) {
+ this.mNext = sPool;
+ sPool = this;
+ sPoolSize++;
+ }
+ }
+
+ public boolean isEOS() {
+ return (this == EOS) || ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0);
+ }
+
+ public static Sample obtain() {
+ synchronized (CREATOR) {
+ Sample s = null;
+ if (sPoolSize > 0) {
+ s = sPool;
+ sPool = s.mNext;
+ s.mNext = null;
+ sPoolSize--;
+ } else {
+ s = new Sample();
+ }
+ return s;
+ }
+ }
+
+ public static final Creator<Sample> CREATOR =
+ new Creator<Sample>() {
+ @Override
+ public Sample createFromParcel(final Parcel in) {
+ return obtainSample(in);
+ }
+
+ @Override
+ public Sample[] newArray(final int size) {
+ return new Sample[size];
+ }
+
+ private Sample obtainSample(final Parcel in) {
+ final Sample s = obtain();
+ s.session = in.readLong();
+ s.bufferId = in.readInt();
+ s.readInfo(in);
+ s.readCrypto(in);
+ return s;
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int parcelableFlags) {
+ dest.writeLong(session);
+ dest.writeInt(bufferId);
+ writeInfo(dest);
+ writeCrypto(dest);
+ }
+
+ private void writeInfo(final Parcel dest) {
+ dest.writeInt(info.offset);
+ dest.writeInt(info.size);
+ dest.writeLong(info.presentationTimeUs);
+ dest.writeInt(info.flags);
+ }
+
+ private void writeCrypto(final Parcel dest) {
+ if (cryptoInfo != null) {
+ dest.writeInt(1);
+ dest.writeByteArray(cryptoInfo.iv);
+ dest.writeByteArray(cryptoInfo.key);
+ dest.writeInt(cryptoInfo.mode);
+ dest.writeIntArray(cryptoInfo.numBytesOfClearData);
+ dest.writeIntArray(cryptoInfo.numBytesOfEncryptedData);
+ dest.writeInt(cryptoInfo.numSubSamples);
+ if (supportsCryptoPattern()) {
+ final CryptoInfo.Pattern pattern = getCryptoPatternCompat(cryptoInfo);
+ if (pattern != null) {
+ dest.writeInt(pattern.getEncryptBlocks());
+ dest.writeInt(pattern.getSkipBlocks());
+ } else {
+ // Couldn't get pattern - write default values
+ dest.writeInt(0);
+ dest.writeInt(0);
+ }
+ }
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ public static byte[] byteArrayFromBuffer(
+ final ByteBuffer buffer, final int offset, final int size) {
+ if (buffer == null || buffer.capacity() == 0 || size == 0) {
+ return null;
+ }
+ if (buffer.hasArray() && offset == 0 && buffer.array().length == size) {
+ return buffer.array();
+ }
+ final int length = Math.min(offset + size, buffer.capacity()) - offset;
+ final byte[] bytes = new byte[length];
+ buffer.position(offset);
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ @Override
+ public String toString() {
+ if (isEOS()) {
+ return "EOS sample";
+ }
+
+ final StringBuilder str = new StringBuilder();
+ str.append("{ session#:")
+ .append(session)
+ .append(", buffer#")
+ .append(bufferId)
+ .append(", info=")
+ .append("{ offset=")
+ .append(info.offset)
+ .append(", size=")
+ .append(info.size)
+ .append(", pts=")
+ .append(info.presentationTimeUs)
+ .append(", flags=")
+ .append(Integer.toHexString(info.flags))
+ .append(" }")
+ .append(" }");
+ return str.toString();
+ }
+
+ @ChecksSdkIntAtLeast(api = android.os.Build.VERSION_CODES.N)
+ public static boolean supportsCryptoPattern() {
+ return Build.VERSION.SDK_INT >= 24;
+ }
+
+ @SuppressLint("DiscouragedPrivateApi")
+ public static CryptoInfo.Pattern getCryptoPatternCompat(final CryptoInfo cryptoInfo) {
+ if (!supportsCryptoPattern()) {
+ return null;
+ }
+ // getPattern() added in API 31:
+ // https://developer.android.com/reference/android/media/MediaCodec.CryptoInfo#getPattern()
+ if (Build.VERSION.SDK_INT >= 31) {
+ return cryptoInfo.getPattern();
+ }
+
+ // CryptoInfo.Pattern added in API 24:
+ // https://developer.android.com/reference/android/media/MediaCodec.CryptoInfo.Pattern
+ if (Build.VERSION.SDK_INT >= 24) {
+ try {
+ // Without getPattern(), no way to access the pattern without reflection.
+ // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/media/java/android/media/MediaCodec.java;l=2718;drc=3c715d5778e15dc84082e63dc65b382d31fe8e45
+ final Field patternField = CryptoInfo.class.getDeclaredField("pattern");
+ patternField.setAccessible(true);
+ return (CryptoInfo.Pattern) patternField.get(cryptoInfo);
+ } catch (final NoSuchFieldException | IllegalAccessException e) {
+ return null;
+ }
+ }
+ return null;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java
new file mode 100644
index 0000000000..e6b242708d
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java
@@ -0,0 +1,101 @@
+/* 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.media;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.SharedMemory;
+
+public final class SampleBuffer implements Parcelable {
+ private SharedMemory mSharedMem;
+
+ /* package */
+ public SampleBuffer(final SharedMemory sharedMem) {
+ mSharedMem = sharedMem;
+ }
+
+ protected SampleBuffer(final Parcel in) {
+ mSharedMem = in.readParcelable(SampleBuffer.class.getClassLoader());
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int flags) {
+ dest.writeParcelable(mSharedMem, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<SampleBuffer> CREATOR =
+ new Creator<SampleBuffer>() {
+ @Override
+ public SampleBuffer createFromParcel(final Parcel in) {
+ return new SampleBuffer(in);
+ }
+
+ @Override
+ public SampleBuffer[] newArray(final int size) {
+ return new SampleBuffer[size];
+ }
+ };
+
+ public int capacity() {
+ return mSharedMem != null ? mSharedMem.getSize() : 0;
+ }
+
+ public void readFromByteBuffer(final ByteBuffer src, final int offset, final int size)
+ throws IOException {
+ if (!src.isDirect()) {
+ throw new IOException("SharedMemBuffer only support reading from direct byte buffer.");
+ }
+ try {
+ nativeReadFromDirectBuffer(src, mSharedMem.getPointer(), offset, size);
+ mSharedMem.flush();
+ } catch (final NullPointerException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private static native void nativeReadFromDirectBuffer(
+ ByteBuffer src, long dest, int offset, int size);
+
+ @WrapForJNI
+ public void writeToByteBuffer(final ByteBuffer dest, final int offset, final int size)
+ throws IOException {
+ if (!dest.isDirect()) {
+ throw new IOException("SharedMemBuffer only support writing to direct byte buffer.");
+ }
+ try {
+ nativeWriteToDirectBuffer(mSharedMem.getPointer(), dest, offset, size);
+ } catch (final NullPointerException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private static native void nativeWriteToDirectBuffer(
+ long src, ByteBuffer dest, int offset, int size);
+
+ public void dispose() {
+ if (mSharedMem != null) {
+ mSharedMem.dispose();
+ mSharedMem = null;
+ }
+ }
+
+ @WrapForJNI
+ public boolean isValid() {
+ return mSharedMem != null;
+ }
+
+ @Override
+ public String toString() {
+ return "Buffer: " + mSharedMem;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SamplePool.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SamplePool.java
new file mode 100644
index 0000000000..a2101b3aeb
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SamplePool.java
@@ -0,0 +1,154 @@
+/* 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.media;
+
+import android.media.MediaCodec;
+import android.util.SparseArray;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.mozilla.gecko.mozglue.SharedMemory;
+
+final class SamplePool {
+ private static final class Impl {
+ private final String mName;
+ private int mDefaultBufferSize = 4096;
+ private final List<Sample> mRecycledSamples = new ArrayList<>();
+ private final boolean mBufferless;
+
+ private int mNextBufferId = Sample.NO_BUFFER + 1;
+ private SparseArray<SampleBuffer> mBuffers = new SparseArray<>();
+
+ private Impl(final String name, final boolean bufferless) {
+ mName = name;
+ mBufferless = bufferless;
+ }
+
+ private void setDefaultBufferSize(final int size) {
+ if (mBufferless) {
+ throw new IllegalStateException("Setting buffer size of a bufferless pool is not allowed");
+ }
+ mDefaultBufferSize = size;
+ }
+
+ private synchronized Sample obtain(final int size) {
+ if (!mRecycledSamples.isEmpty()) {
+ return mRecycledSamples.remove(0);
+ }
+
+ if (mBufferless) {
+ return Sample.obtain();
+ } else {
+ return allocateSampleAndBuffer(size);
+ }
+ }
+
+ private Sample allocateSampleAndBuffer(final int size) {
+ final int id = mNextBufferId++;
+ try {
+ final SharedMemory shm = new SharedMemory(id, Math.max(size, mDefaultBufferSize));
+ mBuffers.put((Integer) id, new SampleBuffer(shm));
+ final Sample s = Sample.obtain();
+ s.bufferId = id;
+ return s;
+ } catch (final NoSuchMethodException | IOException e) {
+ mBuffers.delete(id);
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ private synchronized SampleBuffer getBuffer(final int id) {
+ return mBuffers.get(id);
+ }
+
+ private synchronized void recycle(final Sample recycled) {
+ if (mBufferless || isUsefulSample(recycled)) {
+ mRecycledSamples.add(recycled);
+ } else {
+ disposeSample(recycled);
+ }
+ }
+
+ private boolean isUsefulSample(final Sample sample) {
+ return mBuffers.get(sample.bufferId).capacity() >= mDefaultBufferSize;
+ }
+
+ private synchronized void clear() {
+ for (final Sample s : mRecycledSamples) {
+ disposeSample(s);
+ }
+ mRecycledSamples.clear();
+
+ for (int i = 0; i < mBuffers.size(); ++i) {
+ mBuffers.valueAt(i).dispose();
+ }
+ mBuffers.clear();
+ }
+
+ private void disposeSample(final Sample sample) {
+ if (sample.bufferId != Sample.NO_BUFFER) {
+ mBuffers.get(sample.bufferId).dispose();
+ mBuffers.delete(sample.bufferId);
+ }
+ sample.dispose();
+ }
+
+ @Override
+ protected void finalize() {
+ clear();
+ }
+ }
+
+ private final Impl mInputs;
+ private final Impl mOutputs;
+
+ /* package */ SamplePool(final String name, final boolean renderToSurface) {
+ mInputs = new Impl(name + " input sample pool", false);
+ // Buffers are useless when rendering to surface.
+ mOutputs = new Impl(name + " output sample pool", renderToSurface);
+ }
+
+ /* package */ void setInputBufferSize(final int size) {
+ mInputs.setDefaultBufferSize(size);
+ }
+
+ /* package */ void setOutputBufferSize(final int size) {
+ mOutputs.setDefaultBufferSize(size);
+ }
+
+ /* package */ Sample obtainInput(final int size) {
+ final Sample input = mInputs.obtain(size);
+ input.info.set(0, 0, 0, 0);
+ return input;
+ }
+
+ /* package */ Sample obtainOutput(final MediaCodec.BufferInfo info) {
+ final Sample output = mOutputs.obtain(info.size);
+ output.info.set(0, info.size, info.presentationTimeUs, info.flags);
+ return output;
+ }
+
+ /* package */ void recycleInput(final Sample sample) {
+ sample.cryptoInfo = null;
+ mInputs.recycle(sample);
+ }
+
+ /* package */ void recycleOutput(final Sample sample) {
+ mOutputs.recycle(sample);
+ }
+
+ /* package */ void reset() {
+ mInputs.clear();
+ mOutputs.clear();
+ }
+
+ /* package */ SampleBuffer getInputBuffer(final int id) {
+ return mInputs.getBuffer(id);
+ }
+
+ /* package */ SampleBuffer getOutputBuffer(final int id) {
+ return mOutputs.getBuffer(id);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SessionKeyInfo.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SessionKeyInfo.java
new file mode 100644
index 0000000000..5e70a6f2a7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SessionKeyInfo.java
@@ -0,0 +1,50 @@
+/* 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.media;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public final class SessionKeyInfo implements Parcelable {
+ @WrapForJNI public byte[] keyId;
+
+ @WrapForJNI public int status;
+
+ @WrapForJNI
+ public SessionKeyInfo(final byte[] keyId, final int status) {
+ this.keyId = keyId;
+ this.status = status;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int parcelableFlags) {
+ dest.writeByteArray(keyId);
+ dest.writeInt(status);
+ }
+
+ public static final Creator<SessionKeyInfo> CREATOR =
+ new Creator<SessionKeyInfo>() {
+ @Override
+ public SessionKeyInfo createFromParcel(final Parcel in) {
+ return new SessionKeyInfo(in);
+ }
+
+ @Override
+ public SessionKeyInfo[] newArray(final int size) {
+ return new SessionKeyInfo[size];
+ }
+ };
+
+ private SessionKeyInfo(final Parcel src) {
+ keyId = src.createByteArray();
+ status = src.readInt();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Utils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Utils.java
new file mode 100644
index 0000000000..5cc32e127c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/Utils.java
@@ -0,0 +1,39 @@
+/* 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.media;
+
+import android.util.Log;
+
+public class Utils {
+ public static long getThreadId() {
+ final Thread t = Thread.currentThread();
+ return t.getId();
+ }
+
+ public static String getThreadSignature() {
+ final Thread t = Thread.currentThread();
+ final long l = t.getId();
+ final String name = t.getName();
+ final long p = t.getPriority();
+ final String gname = t.getThreadGroup().getName();
+ return (name + ":(id)" + l + ":(priority)" + p + ":(group)" + gname);
+ }
+
+ public static void logThreadSignature() {
+ Log.d("ThreadUtils", getThreadSignature());
+ }
+
+ private static final char[] hexArray = "0123456789ABCDEF".toCharArray();
+
+ public static String bytesToHex(final byte[] bytes) {
+ final char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ final int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
new file mode 100644
index 0000000000..bebc580916
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/GeckoLoader.java
@@ -0,0 +1,432 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.mozglue;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Environment;
+import android.util.Log;
+import dalvik.system.BaseDexClassLoader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.annotation.JNITarget;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public final class GeckoLoader {
+ private static final String LOGTAG = "GeckoLoader";
+
+ private static File sGREDir;
+
+ /* Synchronized on GeckoLoader.class. */
+ private static boolean sSQLiteLibsLoaded;
+ private static boolean sNSSLibsLoaded;
+ private static boolean sMozGlueLoaded;
+
+ private GeckoLoader() {
+ // prevent instantiation
+ }
+
+ public static File getGREDir(final Context context) {
+ if (sGREDir == null) {
+ sGREDir = new File(context.getApplicationInfo().dataDir);
+ }
+ return sGREDir;
+ }
+
+ private static void setupDownloadEnvironment(final Context context) {
+ try {
+ File downloadDir =
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ File updatesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+ if (downloadDir == null) {
+ downloadDir = new File(Environment.getExternalStorageDirectory().getPath(), "download");
+ }
+ if (updatesDir == null) {
+ updatesDir = downloadDir;
+ }
+ putenv("DOWNLOADS_DIRECTORY=" + downloadDir.getPath());
+ putenv("UPDATES_DIRECTORY=" + updatesDir.getPath());
+ } catch (final Exception e) {
+ Log.w(LOGTAG, "No download directory found.", e);
+ }
+ }
+
+ private static void delTree(final File file) {
+ if (file.isDirectory()) {
+ final File[] children = file.listFiles();
+ for (final File child : children) {
+ delTree(child);
+ }
+ }
+ file.delete();
+ }
+
+ private static File getTmpDir(final Context context) {
+ // It's important that this folder is in the cache directory so users can actually
+ // clear it when it gets too big.
+ return new File(context.getCacheDir(), "gecko_temp");
+ }
+
+ private static String escapeDoubleQuotes(final String str) {
+ return str.replaceAll("\"", "\\\"");
+ }
+
+ private static void setupInitialPrefs(final Map<String, Object> prefs) {
+ if (prefs != null) {
+ final StringBuilder prefsEnv = new StringBuilder("MOZ_DEFAULT_PREFS=");
+ for (final String key : prefs.keySet()) {
+ final Object value = prefs.get(key);
+ if (value == null) {
+ continue;
+ }
+ prefsEnv.append(String.format("pref(\"%s\",", escapeDoubleQuotes(key)));
+ if (value instanceof String) {
+ prefsEnv.append(String.format("\"%s\"", escapeDoubleQuotes(value.toString())));
+ } else if (value instanceof Boolean) {
+ prefsEnv.append((Boolean) value ? "true" : "false");
+ } else {
+ prefsEnv.append(value.toString());
+ }
+
+ prefsEnv.append(");\n");
+ }
+
+ putenv(prefsEnv.toString());
+ }
+ }
+
+ @SuppressWarnings("deprecation") // for Build.CPU_ABI
+ public static synchronized void setupGeckoEnvironment(
+ final Context context,
+ final boolean isChildProcess,
+ final String profilePath,
+ final Collection<String> env,
+ final Map<String, Object> prefs,
+ final boolean xpcshell) {
+ for (final String e : env) {
+ putenv(e);
+ }
+
+ putenv("MOZ_ANDROID_PACKAGE_NAME=" + context.getPackageName());
+
+ if (!isChildProcess) {
+ setupDownloadEnvironment(context);
+
+ // profile home path
+ putenv("HOME=" + profilePath);
+
+ // setup the downloads path
+ File f = Environment.getDownloadCacheDirectory();
+ putenv("EXTERNAL_STORAGE=" + f.getPath());
+
+ // setup the app-specific cache path
+ f = context.getCacheDir();
+ putenv("CACHE_DIRECTORY=" + f.getPath());
+
+ f = context.getExternalFilesDir(null);
+ if (f != null) {
+ putenv("PUBLIC_STORAGE=" + f.getPath());
+ }
+
+ final android.os.UserManager um =
+ (android.os.UserManager) context.getSystemService(Context.USER_SERVICE);
+ if (um != null) {
+ putenv(
+ "MOZ_ANDROID_USER_SERIAL_NUMBER="
+ + um.getSerialNumberForUser(android.os.Process.myUserHandle()));
+ } else {
+ Log.d(
+ LOGTAG,
+ "Unable to obtain user manager service on a device with SDK version "
+ + Build.VERSION.SDK_INT);
+ }
+
+ setupInitialPrefs(prefs);
+ }
+
+ // Xpcshell tests set up their own temp directory
+ if (!xpcshell) {
+ // setup the tmp path
+ final File f = getTmpDir(context);
+ if (!f.exists()) {
+ f.mkdirs();
+ }
+ putenv("TMPDIR=" + f.getPath());
+ }
+
+ putenv("LANG=" + Locale.getDefault().toString());
+
+ final Class<?> crashHandler = GeckoAppShell.getCrashHandlerService();
+ if (crashHandler != null) {
+ putenv(
+ "MOZ_ANDROID_CRASH_HANDLER=" + context.getPackageName() + "/" + crashHandler.getName());
+ }
+
+ putenv("MOZ_ANDROID_DEVICE_SDK_VERSION=" + Build.VERSION.SDK_INT);
+ putenv("MOZ_ANDROID_CPU_ABI=" + Build.CPU_ABI);
+
+ // env from extras could have reset out linker flags; set them again.
+ loadLibsSetupLocked(context);
+ }
+
+ // Adapted from
+ // https://source.chromium.org/chromium/chromium/src/+/main:base/android/java/src/org/chromium/base/BundleUtils.java;l=196;drc=c0fedddd4a1444653235912cfae3d44b544ded01
+ private static String getLibraryPath(final String libraryName) {
+ // Due to b/171269960 isolated split class loaders have an empty library path, so check
+ // the base module class loader first which loaded GeckoAppShell. If the library is not
+ // found there, attempt to construct the correct library path from the split.
+ String path =
+ ((BaseDexClassLoader) GeckoAppShell.class.getClassLoader()).findLibrary(libraryName);
+ if (path != null) {
+ return path;
+ }
+
+ // SplitCompat is installed on the application context, so check there for library paths
+ // which were added to that ClassLoader.
+ final ClassLoader classLoader = GeckoAppShell.getApplicationContext().getClassLoader();
+ if (classLoader instanceof BaseDexClassLoader) {
+ path = ((BaseDexClassLoader) classLoader).findLibrary(libraryName);
+ if (path != null) {
+ return path;
+ }
+ }
+
+ throw new RuntimeException("Could not find mozglue path.");
+ }
+
+ private static String getLibraryBase() {
+ final String mozglue = getLibraryPath("mozglue");
+ final int lastSlash = mozglue.lastIndexOf('/');
+ if (lastSlash < 0) {
+ throw new IllegalStateException("Invalid library path for libmozglue.so: " + mozglue);
+ }
+ final String base = mozglue.substring(0, lastSlash);
+ Log.i(LOGTAG, "Library base=" + base);
+ return base;
+ }
+
+ private static void loadLibsSetupLocked(final Context context) {
+ putenv("GRE_HOME=" + getGREDir(context).getPath());
+ putenv("MOZ_ANDROID_LIBDIR=" + getLibraryBase());
+ }
+
+ @RobocopTarget
+ public static synchronized void loadSQLiteLibs(final Context context) {
+ if (sSQLiteLibsLoaded) {
+ return;
+ }
+
+ loadMozGlue(context);
+ loadLibsSetupLocked(context);
+ loadSQLiteLibsNative();
+ sSQLiteLibsLoaded = true;
+ }
+
+ public static synchronized void loadNSSLibs(final Context context) {
+ if (sNSSLibsLoaded) {
+ return;
+ }
+
+ loadMozGlue(context);
+ loadLibsSetupLocked(context);
+ loadNSSLibsNative();
+ sNSSLibsLoaded = true;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static String getCPUABI() {
+ return android.os.Build.CPU_ABI;
+ }
+
+ /**
+ * Copy a library out of our APK.
+ *
+ * @param context a Context.
+ * @param lib the name of the library; e.g., "mozglue".
+ * @param outDir the output directory for the .so. No trailing slash.
+ * @return true on success, false on failure.
+ */
+ private static boolean extractLibrary(
+ final Context context, final String lib, final String outDir) {
+ final String apkPath = context.getApplicationInfo().sourceDir;
+
+ // Sanity check.
+ if (!apkPath.endsWith(".apk")) {
+ Log.w(LOGTAG, "sourceDir is not an APK.");
+ return false;
+ }
+
+ // Try to extract the named library from the APK.
+ final File outDirFile = new File(outDir);
+ if (!outDirFile.isDirectory()) {
+ if (!outDirFile.mkdirs()) {
+ Log.e(LOGTAG, "Couldn't create " + outDir);
+ return false;
+ }
+ }
+
+ final String[] abis = Build.SUPPORTED_ABIS;
+ for (final String abi : abis) {
+ if (tryLoadWithABI(lib, outDir, apkPath, abi)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean tryLoadWithABI(
+ final String lib, final String outDir, final String apkPath, final String abi) {
+ try {
+ final ZipFile zipFile = new ZipFile(new File(apkPath));
+ try {
+ final String libPath = "lib/" + abi + "/lib" + lib + ".so";
+ final ZipEntry entry = zipFile.getEntry(libPath);
+ if (entry == null) {
+ Log.w(LOGTAG, libPath + " not found in APK " + apkPath);
+ return false;
+ }
+
+ final InputStream in = zipFile.getInputStream(entry);
+ try {
+ final String outPath = outDir + "/lib" + lib + ".so";
+ final FileOutputStream out = new FileOutputStream(outPath);
+ final byte[] bytes = new byte[1024];
+ int read;
+
+ Log.d(LOGTAG, "Copying " + libPath + " to " + outPath);
+ boolean failed = false;
+ try {
+ while ((read = in.read(bytes, 0, 1024)) != -1) {
+ out.write(bytes, 0, read);
+ }
+ } catch (final Exception e) {
+ Log.w(LOGTAG, "Failing library copy.", e);
+ failed = true;
+ } finally {
+ out.close();
+ }
+
+ if (failed) {
+ // Delete the partial copy so we don't fail to load it.
+ // Don't bother to check the return value -- there's nothing
+ // we can do about a failure.
+ new File(outPath).delete();
+ } else {
+ // Mark the file as executable. This doesn't seem to be
+ // necessary for the loader, but it's the normal state of
+ // affairs.
+ Log.d(LOGTAG, "Marking " + outPath + " as executable.");
+ new File(outPath).setExecutable(true);
+ }
+
+ return !failed;
+ } finally {
+ in.close();
+ }
+ } finally {
+ zipFile.close();
+ }
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Failed to extract lib from APK.", e);
+ return false;
+ }
+ }
+
+ private static boolean attemptLoad(final String path) {
+ try {
+ System.load(path);
+ return true;
+ } catch (final Throwable e) {
+ Log.wtf(LOGTAG, "Couldn't load " + path + ": " + e);
+ }
+
+ return false;
+ }
+
+ /**
+ * The first two attempts at loading a library: directly, and then using the app library path.
+ *
+ * <p>Returns null or the cause exception.
+ */
+ public static Throwable doLoadLibrary(final Context context, final String lib) {
+ try {
+ // Attempt 1: the way that should work.
+ System.loadLibrary(lib);
+ return null;
+ } catch (final Throwable e) {
+ final String libPath = getLibraryPath(lib);
+ // Does it even exist?
+ if (new File(libPath).exists()) {
+ if (attemptLoad(libPath)) {
+ // Success!
+ return null;
+ }
+ throw new RuntimeException(
+ "Library exists but couldn't load." + "Path: " + libPath + " lib: " + lib, e);
+ }
+ throw new RuntimeException(
+ "Library doesn't exist when it should." + "Path: " + libPath + " lib: " + lib, e);
+ }
+ }
+
+ public static synchronized void loadMozGlue(final Context context) {
+ if (sMozGlueLoaded) {
+ return;
+ }
+
+ doLoadLibrary(context, "mozglue");
+ sMozGlueLoaded = true;
+ }
+
+ public static synchronized void loadGeckoLibs(final Context context) {
+ loadLibsSetupLocked(context);
+ loadGeckoLibsNative();
+ }
+
+ @SuppressWarnings("serial")
+ public static class AbortException extends Exception {
+ public AbortException(final String msg) {
+ super(msg);
+ }
+ }
+
+ @JNITarget
+ public static void abort(final String msg) {
+ final Thread thread = Thread.currentThread();
+ final Thread.UncaughtExceptionHandler uncaughtHandler = thread.getUncaughtExceptionHandler();
+ if (uncaughtHandler != null) {
+ uncaughtHandler.uncaughtException(thread, new AbortException(msg));
+ }
+ }
+
+ // These methods are implemented in mozglue/android/nsGeckoUtils.cpp
+ private static native void putenv(String map);
+
+ // These methods are implemented in mozglue/android/APKOpen.cpp
+ public static native void nativeRun(
+ String[] args,
+ int prefsFd,
+ int prefMapFd,
+ int ipcFd,
+ int crashFd,
+ boolean xpcshell,
+ String outFilePath);
+
+ private static native void loadGeckoLibsNative();
+
+ private static native void loadSQLiteLibsNative();
+
+ private static native void loadNSSLibsNative();
+
+ public static native void suppressCrashDialog();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java
new file mode 100644
index 0000000000..3b0f8cc96b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/JNIObject.java
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.mozglue;
+
+// Class that all classes with native methods extend from.
+public abstract class JNIObject {
+ // Pointer that references the native object. This is volatile because it may be accessed
+ // by multiple threads simultaneously.
+ private volatile long mHandle;
+
+ // Dispose of any reference to a native object.
+ //
+ // If the native instance is destroyed from the native side, this should never be
+ // called, so you should throw an UnsupportedOperationException. If instead you
+ // want to destroy the native side from the Java end, make override this with
+ // a native call, and the right thing will be done in the native code.
+ protected abstract void disposeNative();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java
new file mode 100644
index 0000000000..7e6139ffd7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/NativeReference.java
@@ -0,0 +1,12 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.mozglue;
+
+public interface NativeReference {
+ void release();
+
+ boolean isReleased();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SharedMemory.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SharedMemory.java
new file mode 100644
index 0000000000..af8b62c382
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/mozglue/SharedMemory.java
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.mozglue;
+
+import android.annotation.SuppressLint;
+import android.os.MemoryFile;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.util.Log;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.reflect.Method;
+
+@SuppressLint("DiscouragedPrivateApi")
+public class SharedMemory implements Parcelable {
+ private static final String LOGTAG = "GeckoShmem";
+ private static final Method sGetFDMethod;
+ private ParcelFileDescriptor mDescriptor;
+ private int mSize;
+ private int mId;
+ private long mHandle; // The native pointer.
+ private boolean mIsMapped;
+ private MemoryFile mBackedFile;
+
+ // MemoryFile.getFileDescriptor() is hidden. :(
+ static {
+ Method method = null;
+ try {
+ method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
+ } catch (final NoSuchMethodException e) {
+ e.printStackTrace();
+ }
+ sGetFDMethod = method;
+ }
+
+ private SharedMemory(final Parcel in) {
+ mDescriptor = in.readFileDescriptor();
+ mSize = in.readInt();
+ mId = in.readInt();
+ }
+
+ public static final Creator<SharedMemory> CREATOR =
+ new Creator<SharedMemory>() {
+ @Override
+ public SharedMemory createFromParcel(final Parcel in) {
+ return new SharedMemory(in);
+ }
+
+ @Override
+ public SharedMemory[] newArray(final int size) {
+ return new SharedMemory[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return CONTENTS_FILE_DESCRIPTOR;
+ }
+
+ @Override
+ public void writeToParcel(final Parcel dest, final int flags) {
+ // We don't want ParcelFileDescriptor.writeToParcel() to close the fd.
+ dest.writeFileDescriptor(mDescriptor.getFileDescriptor());
+ dest.writeInt(mSize);
+ dest.writeInt(mId);
+ }
+
+ public SharedMemory(final int id, final int size) throws NoSuchMethodException, IOException {
+ if (sGetFDMethod == null) {
+ throw new NoSuchMethodException("MemoryFile.getFileDescriptor() doesn't exist.");
+ }
+ mBackedFile = new MemoryFile(null, size);
+ try {
+ final FileDescriptor fd = (FileDescriptor) sGetFDMethod.invoke(mBackedFile);
+ mDescriptor = ParcelFileDescriptor.dup(fd);
+ mSize = size;
+ mId = id;
+ mBackedFile.allowPurging(false);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ close();
+ throw new IOException(e.getMessage());
+ }
+ }
+
+ public void flush() {
+ if (!mIsMapped) {
+ return;
+ }
+
+ unmap(mHandle, mSize);
+ mHandle = 0;
+ mIsMapped = false;
+ }
+
+ public void close() {
+ flush();
+
+ if (mDescriptor != null) {
+ try {
+ mDescriptor.close();
+ } catch (final IOException e) {
+ e.printStackTrace();
+ }
+ mDescriptor = null;
+ }
+ }
+
+ // Should only be called by process that allocates shared memory.
+ public void dispose() {
+ if (!isValid()) {
+ return;
+ }
+
+ close();
+
+ if (mBackedFile != null) {
+ mBackedFile.close();
+ mBackedFile = null;
+ }
+ }
+
+ private native void unmap(long address, int size);
+
+ public boolean isValid() {
+ return mDescriptor != null;
+ }
+
+ public int getSize() {
+ return mSize;
+ }
+
+ private int getFD() {
+ return isValid() ? mDescriptor.getFd() : -1;
+ }
+
+ public long getPointer() {
+ if (!isValid()) {
+ return 0;
+ }
+
+ if (!mIsMapped) {
+ try {
+ mHandle = map(getFD(), mSize);
+ } catch (final NullPointerException e) {
+ Log.e(LOGTAG, "SharedMemory#" + mId + " error.", e);
+ throw e;
+ }
+ if (mHandle != 0) {
+ mIsMapped = true;
+ }
+ }
+ return mHandle;
+ }
+
+ private native long map(int fd, int size);
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (mBackedFile != null) {
+ Log.w(LOGTAG, "dispose() not called before finalizing");
+ }
+ dispose();
+
+ super.finalize();
+ }
+
+ @Override
+ public String toString() {
+ return "SHM("
+ + getSize()
+ + " bytes): id="
+ + mId
+ + ", backing="
+ + mBackedFile
+ + ",fd="
+ + mDescriptor;
+ }
+
+ @Override
+ public boolean equals(final Object that) {
+ return (this == that) || ((that instanceof SharedMemory) && (hashCode() == that.hashCode()));
+ }
+
+ @Override
+ public int hashCode() {
+ return mId;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja
new file mode 100644
index 0000000000..fa2f336566
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja
@@ -0,0 +1,19 @@
+/* -*- 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.process;
+
+public class GeckoChildProcessServices {
+ /* package */ static final int MAX_NUM_ISOLATED_CONTENT_SERVICES = {{MOZ_ANDROID_CONTENT_SERVICE_COUNT}};
+ public static final class gmplugin extends GeckoServiceChildProcess {}
+ public static final class socket extends GeckoServiceChildProcess {}
+ public static final class gpu extends GeckoServiceGpuProcess {}
+ public static final class utility extends GeckoServiceChildProcess {}
+ public static final class ipdlunittest extends GeckoServiceChildProcess {}
+
+{% for id in range(0, MOZ_ANDROID_CONTENT_SERVICE_COUNT | int) %}
+ public static final class tab{{ id }} extends GeckoServiceChildProcess {}
+{% endfor %}
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
new file mode 100644
index 0000000000..736c292ff1
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
@@ -0,0 +1,924 @@
+/* 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.process;
+
+import android.os.DeadObjectException;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.collection.SimpleArrayMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoNetworkManager;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.GeckoThread.FileDescriptors;
+import org.mozilla.gecko.GeckoThread.ParcelFileDescriptors;
+import org.mozilla.gecko.IGeckoEditableChild;
+import org.mozilla.gecko.IGeckoEditableParent;
+import org.mozilla.gecko.TelemetryUtils;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.CompositorSurfaceManager;
+import org.mozilla.gecko.gfx.ISurfaceAllocator;
+import org.mozilla.gecko.gfx.RemoteSurfaceAllocator;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.process.ServiceAllocator.PriorityLevel;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.XPCOMEventTarget;
+import org.mozilla.geckoview.GeckoResult;
+
+public final class GeckoProcessManager extends IProcessManager.Stub {
+ private static final String LOGTAG = "GeckoProcessManager";
+ private static final GeckoProcessManager INSTANCE = new GeckoProcessManager();
+ private static final int INVALID_PID = 0;
+
+ // This id univocally identifies the current process manager instance
+ private final String mInstanceId;
+
+ public static GeckoProcessManager getInstance() {
+ return INSTANCE;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setEditableChildParent(
+ final IGeckoEditableChild child, final IGeckoEditableParent parent) {
+ try {
+ child.transferParent(parent);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Cannot set parent", e);
+ }
+ }
+
+ @WrapForJNI(stubName = "GetEditableParent", dispatchTo = "gecko")
+ private static native void nativeGetEditableParent(
+ IGeckoEditableChild child, long contentId, long tabId);
+
+ @Override // IProcessManager
+ public void getEditableParent(
+ final IGeckoEditableChild child, final long contentId, final long tabId) {
+ nativeGetEditableParent(child, contentId, tabId);
+ }
+
+ /**
+ * Returns the surface allocator interface to be used by child processes to allocate Surfaces. The
+ * service bound to the returned interface may live in either the GPU process or parent process.
+ */
+ @Override // IProcessManager
+ public ISurfaceAllocator getSurfaceAllocator() {
+ final GeckoResult<Boolean> gpuEnabled = GeckoAppShell.isGpuProcessEnabled();
+
+ try {
+ final GeckoResult<ISurfaceAllocator> allocator = new GeckoResult<>();
+ if (gpuEnabled.poll(1000)) {
+ // The GPU process is enabled, so look it up and ask it for its surface allocator.
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ final Selector selector = new Selector(GeckoProcessType.GPU);
+ final GpuProcessConnection conn =
+ (GpuProcessConnection) INSTANCE.mConnections.getExistingConnection(selector);
+ if (conn != null) {
+ allocator.complete(conn.getSurfaceAllocator());
+ } else {
+ // If we cannot find a GPU process, it has probably been killed and not yet
+ // restarted. Return null here, and allow the caller to try again later.
+ // We definitely do *not* want to return the parent process allocator instead, as
+ // that will result in surfaces being allocated in the parent process, which
+ // therefore won't be usable when the GPU process is eventually launched.
+ allocator.complete(null);
+ }
+ });
+ } else {
+ // The GPU process is disabled, so return the parent process allocator instance.
+ allocator.complete(RemoteSurfaceAllocator.getInstance(0));
+ }
+ return allocator.poll(100);
+ } catch (final Throwable e) {
+ Log.e(LOGTAG, "Error in getSurfaceAllocator", e);
+ return null;
+ }
+ }
+
+ @WrapForJNI
+ public static CompositorSurfaceManager getCompositorSurfaceManager() {
+ final Selector selector = new Selector(GeckoProcessType.GPU);
+ final GpuProcessConnection conn =
+ (GpuProcessConnection) INSTANCE.mConnections.getExistingConnection(selector);
+ if (conn == null) {
+ return null;
+ }
+ return conn.getCompositorSurfaceManager();
+ }
+
+ /** Gecko uses this class to uniquely identify a process managed by GeckoProcessManager. */
+ public static final class Selector {
+ private final GeckoProcessType mType;
+ private final int mPid;
+
+ @WrapForJNI
+ private Selector(@NonNull final GeckoProcessType type, final int pid) {
+ if (pid == INVALID_PID) {
+ throw new RuntimeException("Invalid PID");
+ }
+
+ mType = type;
+ mPid = pid;
+ }
+
+ @WrapForJNI
+ private Selector(@NonNull final GeckoProcessType type) {
+ mType = type;
+ mPid = INVALID_PID;
+ }
+
+ public GeckoProcessType getType() {
+ return mType;
+ }
+
+ public int getPid() {
+ return mPid;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+
+ if (obj == ((Object) this)) {
+ return true;
+ }
+
+ final Selector other = (Selector) obj;
+ return mType == other.mType && mPid == other.mPid;
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(new Object[] {mType, mPid});
+ }
+ }
+
+ private static final class IncompleteChildConnectionException extends RuntimeException {
+ public IncompleteChildConnectionException(@NonNull final String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Maintains state pertaining to an individual child process. Inheriting from
+ * ServiceAllocator.InstanceInfo enables this class to work with ServiceAllocator.
+ */
+ private static class ChildConnection extends ServiceAllocator.InstanceInfo {
+ private IChildProcess mChild;
+ private GeckoResult<IChildProcess> mPendingBind;
+ private int mPid;
+
+ protected ChildConnection(
+ @NonNull final ServiceAllocator allocator,
+ @NonNull final GeckoProcessType type,
+ @NonNull final PriorityLevel initialPriority) {
+ super(allocator, type, initialPriority);
+ mPid = INVALID_PID;
+ }
+
+ public int getPid() {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (mChild == null) {
+ throw new IncompleteChildConnectionException(
+ "Calling ChildConnection.getPid() on an incomplete connection");
+ }
+
+ return mPid;
+ }
+
+ private GeckoResult<IChildProcess> completeFailedBind(
+ @NonNull final ServiceAllocator.BindException e) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ Log.e(LOGTAG, "Failed bind", e);
+
+ if (mPendingBind == null) {
+ throw new IllegalStateException("Bind failed with null mPendingBind");
+ }
+
+ final GeckoResult<IChildProcess> bindResult = mPendingBind;
+ mPendingBind = null;
+ unbind().accept(v -> bindResult.completeExceptionally(e));
+ return bindResult;
+ }
+
+ public GeckoResult<IChildProcess> bind() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ if (mChild != null) {
+ // Already bound
+ return GeckoResult.fromValue(mChild);
+ }
+
+ if (mPendingBind != null) {
+ // Bind in progress
+ return mPendingBind;
+ }
+
+ mPendingBind = new GeckoResult<>();
+ try {
+ if (!bindService()) {
+ throw new ServiceAllocator.BindException("Cannot connect to process");
+ }
+ } catch (final ServiceAllocator.BindException e) {
+ return completeFailedBind(e);
+ }
+
+ return mPendingBind;
+ }
+
+ public GeckoResult<Void> unbind() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ if (mPendingBind != null) {
+ // We called unbind() while bind() was still pending completion
+ return mPendingBind.then(child -> unbind());
+ }
+
+ if (mChild == null) {
+ // Not bound in the first place
+ return GeckoResult.fromValue(null);
+ }
+
+ unbindService();
+
+ return GeckoResult.fromValue(null);
+ }
+
+ @Override
+ protected void onBinderConnected(final IBinder service) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ final IChildProcess child = IChildProcess.Stub.asInterface(service);
+ try {
+ mPid = child.getPid();
+ onBinderConnected(child);
+ } catch (final DeadObjectException e) {
+ unbindService();
+
+ // mPendingBind might be null if a bind was initiated by the system (eg Service Restart)
+ if (mPendingBind != null) {
+ mPendingBind.completeExceptionally(e);
+ mPendingBind = null;
+ }
+
+ return;
+ } catch (final RemoteException e) {
+ throw new RuntimeException(e);
+ }
+
+ mChild = child;
+ GeckoProcessManager.INSTANCE.mConnections.onBindComplete(this);
+
+ // mPendingBind might be null if a bind was initiated by the system (eg Service Restart)
+ if (mPendingBind != null) {
+ mPendingBind.complete(mChild);
+ mPendingBind = null;
+ }
+ }
+
+ // Subclasses of ChildConnection can override this method to make any IChildProcess calls
+ // specific to their process type immediately after connection.
+ protected void onBinderConnected(@NonNull final IChildProcess child) throws RemoteException {}
+
+ @Override
+ protected void onReleaseResources() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ // NB: This must happen *before* resetting mPid!
+ GeckoProcessManager.INSTANCE.mConnections.removeConnection(this);
+
+ mChild = null;
+ mPid = INVALID_PID;
+ }
+ }
+
+ private static class NonContentConnection extends ChildConnection {
+ public NonContentConnection(
+ @NonNull final ServiceAllocator allocator, @NonNull final GeckoProcessType type) {
+ super(allocator, type, PriorityLevel.FOREGROUND);
+ if (type == GeckoProcessType.CONTENT) {
+ throw new AssertionError("Attempt to create a NonContentConnection as CONTENT");
+ }
+ }
+
+ protected void onAppForeground() {
+ setPriorityLevel(PriorityLevel.FOREGROUND);
+ }
+
+ protected void onAppBackground() {
+ setPriorityLevel(PriorityLevel.IDLE);
+ }
+ }
+
+ private static final class GpuProcessConnection extends NonContentConnection {
+ private CompositorSurfaceManager mCompositorSurfaceManager;
+ private ISurfaceAllocator mSurfaceAllocator;
+
+ // Unique ID used to identify each GPU process instance. Will always be non-zero,
+ // and unlike the process' pid cannot be the same value for successive instances.
+ private int mUniqueGpuProcessId;
+ // Static counter used to initialize each instance's mUniqueGpuProcessId
+ private static int sUniqueGpuProcessIdCounter = 0;
+
+ public GpuProcessConnection(@NonNull final ServiceAllocator allocator) {
+ super(allocator, GeckoProcessType.GPU);
+
+ // Initialize the unique ID ensuring we skip 0 (as that is reserved for parent process
+ // allocators).
+ if (sUniqueGpuProcessIdCounter == 0) {
+ sUniqueGpuProcessIdCounter++;
+ }
+ mUniqueGpuProcessId = sUniqueGpuProcessIdCounter++;
+ }
+
+ @Override
+ protected void onBinderConnected(@NonNull final IChildProcess child) throws RemoteException {
+ mCompositorSurfaceManager = new CompositorSurfaceManager(child.getCompositorSurfaceManager());
+ mSurfaceAllocator = child.getSurfaceAllocator(mUniqueGpuProcessId);
+ }
+
+ public CompositorSurfaceManager getCompositorSurfaceManager() {
+ return mCompositorSurfaceManager;
+ }
+
+ public ISurfaceAllocator getSurfaceAllocator() {
+ return mSurfaceAllocator;
+ }
+ }
+
+ private static final class SocketProcessConnection extends NonContentConnection {
+ private boolean mIsForeground = true;
+ private boolean mIsNetworkUp = true;
+
+ public SocketProcessConnection(@NonNull final ServiceAllocator allocator) {
+ super(allocator, GeckoProcessType.SOCKET);
+ GeckoProcessManager.INSTANCE.mConnections.enableNetworkNotifications();
+ }
+
+ public void onNetworkStateChange(final boolean isNetworkUp) {
+ mIsNetworkUp = isNetworkUp;
+ prioritize();
+ }
+
+ @Override
+ protected void onAppForeground() {
+ mIsForeground = true;
+ prioritize();
+ }
+
+ @Override
+ protected void onAppBackground() {
+ mIsForeground = false;
+ prioritize();
+ }
+
+ private static final PriorityLevel[][] sPriorityStates = initPriorityStates();
+
+ private static PriorityLevel[][] initPriorityStates() {
+ final PriorityLevel[][] states = new PriorityLevel[2][2];
+ // Background, no network
+ states[0][0] = PriorityLevel.IDLE;
+ // Background, network
+ states[0][1] = PriorityLevel.BACKGROUND;
+ // Foreground, no network
+ states[1][0] = PriorityLevel.IDLE;
+ // Foreground, network
+ states[1][1] = PriorityLevel.FOREGROUND;
+ return states;
+ }
+
+ private void prioritize() {
+ final PriorityLevel nextPriority =
+ sPriorityStates[mIsForeground ? 1 : 0][mIsNetworkUp ? 1 : 0];
+ setPriorityLevel(nextPriority);
+ }
+ }
+
+ private static final class ContentConnection extends ChildConnection {
+ private static final String TELEMETRY_PROCESS_LIFETIME_HISTOGRAM_NAME =
+ "GV_CONTENT_PROCESS_LIFETIME_MS";
+
+ private TelemetryUtils.UptimeTimer mLifetimeTimer = null;
+
+ public ContentConnection(
+ @NonNull final ServiceAllocator allocator, @NonNull final PriorityLevel initialPriority) {
+ super(allocator, GeckoProcessType.CONTENT, initialPriority);
+ }
+
+ @Override
+ protected void onBinderConnected(final IBinder service) {
+ mLifetimeTimer = new TelemetryUtils.UptimeTimer(TELEMETRY_PROCESS_LIFETIME_HISTOGRAM_NAME);
+ super.onBinderConnected(service);
+ }
+
+ @Override
+ protected void onReleaseResources() {
+ if (mLifetimeTimer != null) {
+ mLifetimeTimer.stop();
+ mLifetimeTimer = null;
+ }
+
+ super.onReleaseResources();
+ }
+ }
+
+ /** This class manages the state surrounding existing connections and their priorities. */
+ private static final class ConnectionManager extends JNIObject {
+ // Connections to non-content processes
+ private final ArrayMap<GeckoProcessType, NonContentConnection> mNonContentConnections;
+ // Mapping of pid to content process
+ private final SimpleArrayMap<Integer, ContentConnection> mContentPids;
+ // Set of initialized content process connections
+ private final ArraySet<ContentConnection> mContentConnections;
+ // Set of bound but uninitialized content connections
+ private final ArraySet<ContentConnection> mNonStartedContentConnections;
+ // Allocator for service IDs
+ private final ServiceAllocator mServiceAllocator;
+ private boolean mIsObservingNetwork = false;
+
+ public ConnectionManager() {
+ mNonContentConnections = new ArrayMap<GeckoProcessType, NonContentConnection>();
+ mContentPids = new SimpleArrayMap<Integer, ContentConnection>();
+ mContentConnections = new ArraySet<ContentConnection>();
+ mNonStartedContentConnections = new ArraySet<ContentConnection>();
+ mServiceAllocator = new ServiceAllocator();
+
+ // Attach to native once JNI is ready.
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) {
+ attachTo(this);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.JNI_READY, ConnectionManager.class, "attachTo", this);
+ }
+ }
+
+ private void enableNetworkNotifications() {
+ if (mIsObservingNetwork) {
+ return;
+ }
+
+ mIsObservingNetwork = true;
+
+ // Ensure that GeckoNetworkManager is monitoring network events so that we can
+ // prioritize the socket process.
+ ThreadUtils.runOnUiThread(
+ () -> {
+ GeckoNetworkManager.getInstance().enableNotifications();
+ });
+
+ observeNetworkNotifications();
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void attachTo(ConnectionManager instance);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private native void observeNetworkNotifications();
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onBackground() {
+ XPCOMEventTarget.runOnLauncherThread(() -> onAppBackgroundInternal());
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onForeground() {
+ XPCOMEventTarget.runOnLauncherThread(() -> onAppForegroundInternal());
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onNetworkStateChange(final boolean isUp) {
+ XPCOMEventTarget.runOnLauncherThread(() -> onNetworkStateChangeInternal(isUp));
+ }
+
+ @Override
+ protected native void disposeNative();
+
+ private void onAppBackgroundInternal() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ for (final NonContentConnection conn : mNonContentConnections.values()) {
+ conn.onAppBackground();
+ }
+ }
+
+ private void onAppForegroundInternal() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ for (final NonContentConnection conn : mNonContentConnections.values()) {
+ conn.onAppForeground();
+ }
+ }
+
+ private void onNetworkStateChangeInternal(final boolean isUp) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ final SocketProcessConnection conn =
+ (SocketProcessConnection) mNonContentConnections.get(GeckoProcessType.SOCKET);
+ if (conn == null) {
+ return;
+ }
+
+ conn.onNetworkStateChange(isUp);
+ }
+
+ private void removeContentConnection(@NonNull final ChildConnection conn) {
+ if (!mContentConnections.remove(conn)) {
+ throw new RuntimeException("Attempt to remove non-registered connection");
+ }
+ mNonStartedContentConnections.remove(conn);
+
+ final int pid;
+
+ try {
+ pid = conn.getPid();
+ } catch (final IncompleteChildConnectionException e) {
+ // conn lost its binding before it was able to retrieve its pid. It follows that
+ // mContentPids does not have an entry for this connection, so we can just return.
+ return;
+ }
+
+ if (pid == INVALID_PID) {
+ return;
+ }
+
+ final ChildConnection removed = mContentPids.remove(Integer.valueOf(pid));
+ if (removed != null && removed != conn) {
+ throw new RuntimeException(
+ "Integrity error - connection mismatch for pid " + Integer.toString(pid));
+ }
+ }
+
+ public void removeConnection(@NonNull final ChildConnection conn) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ if (conn.getType() == GeckoProcessType.CONTENT) {
+ removeContentConnection(conn);
+ return;
+ }
+
+ final ChildConnection removed = mNonContentConnections.remove(conn.getType());
+ if (removed != conn) {
+ throw new RuntimeException(
+ "Integrity error - connection mismatch for process type " + conn.getType().toString());
+ }
+ }
+
+ /** Saves any state information that was acquired upon start completion. */
+ public void onBindComplete(@NonNull final ChildConnection conn) {
+ if (conn.getType() == GeckoProcessType.CONTENT) {
+ final int pid = conn.getPid();
+ if (pid == INVALID_PID) {
+ throw new AssertionError(
+ "PID is invalid even though our caller just successfully retrieved it after binding");
+ }
+
+ mContentPids.put(pid, (ContentConnection) conn);
+ }
+ }
+
+ /** Retrieve the ChildConnection for an already running content process. */
+ private ContentConnection getExistingContentConnection(@NonNull final Selector selector) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (selector.getType() != GeckoProcessType.CONTENT) {
+ throw new IllegalArgumentException("Selector is not for content!");
+ }
+
+ return mContentPids.get(selector.getPid());
+ }
+
+ /** Unconditionally create a new content connection for the specified priority. */
+ private ContentConnection getNewContentConnection(@NonNull final PriorityLevel newPriority) {
+ final ContentConnection result = new ContentConnection(mServiceAllocator, newPriority);
+ mContentConnections.add(result);
+
+ return result;
+ }
+
+ /** Retrieve the ChildConnection for an already running child process of any type. */
+ public ChildConnection getExistingConnection(@NonNull final Selector selector) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ final GeckoProcessType type = selector.getType();
+
+ if (type == GeckoProcessType.CONTENT) {
+ return getExistingContentConnection(selector);
+ }
+
+ return mNonContentConnections.get(type);
+ }
+
+ /**
+ * Retrieve a ChildConnection for a content process for the purposes of starting. If there are
+ * any preloaded content processes already running, we will use one of those. Otherwise we will
+ * allocate a new ChildConnection.
+ */
+ private ChildConnection getContentConnectionForStart() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ if (mNonStartedContentConnections.isEmpty()) {
+ return getNewContentConnection(PriorityLevel.FOREGROUND);
+ }
+
+ final ChildConnection conn =
+ mNonStartedContentConnections.removeAt(mNonStartedContentConnections.size() - 1);
+ conn.setPriorityLevel(PriorityLevel.FOREGROUND);
+ return conn;
+ }
+
+ /** Retrieve or create a new child process for the specified non-content process. */
+ private ChildConnection getNonContentConnection(@NonNull final GeckoProcessType type) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (type == GeckoProcessType.CONTENT) {
+ throw new IllegalArgumentException("Content processes not supported by this method");
+ }
+
+ NonContentConnection connection = mNonContentConnections.get(type);
+ if (connection == null) {
+ if (type == GeckoProcessType.SOCKET) {
+ connection = new SocketProcessConnection(mServiceAllocator);
+ } else if (type == GeckoProcessType.GPU) {
+ connection = new GpuProcessConnection(mServiceAllocator);
+ } else {
+ connection = new NonContentConnection(mServiceAllocator, type);
+ }
+
+ mNonContentConnections.put(type, connection);
+ }
+
+ return connection;
+ }
+
+ /** Retrieve a ChildConnection for the purposes of starting a new child process. */
+ public ChildConnection getConnectionForStart(@NonNull final GeckoProcessType type) {
+ if (type == GeckoProcessType.CONTENT) {
+ return getContentConnectionForStart();
+ }
+
+ return getNonContentConnection(type);
+ }
+
+ /** Retrieve a ChildConnection for the purposes of preloading a new child process. */
+ public ChildConnection getConnectionForPreload(@NonNull final GeckoProcessType type) {
+ if (type == GeckoProcessType.CONTENT) {
+ final ContentConnection conn = getNewContentConnection(PriorityLevel.BACKGROUND);
+ mNonStartedContentConnections.add(conn);
+ return conn;
+ }
+
+ return getNonContentConnection(type);
+ }
+ }
+
+ private final ConnectionManager mConnections;
+
+ private GeckoProcessManager() {
+ mConnections = new ConnectionManager();
+ mInstanceId = UUID.randomUUID().toString();
+ }
+
+ public void preload(final GeckoProcessType... types) {
+ XPCOMEventTarget.launcherThread()
+ .execute(
+ () -> {
+ for (final GeckoProcessType type : types) {
+ final ChildConnection connection = mConnections.getConnectionForPreload(type);
+ connection.bind();
+ }
+ });
+ }
+
+ public void crashChild(@NonNull final Selector selector) {
+ XPCOMEventTarget.launcherThread()
+ .execute(
+ () -> {
+ final ChildConnection conn = mConnections.getExistingConnection(selector);
+ if (conn == null) {
+ return;
+ }
+
+ conn.bind()
+ .accept(
+ proc -> {
+ try {
+ proc.crash();
+ } catch (final RemoteException e) {
+ }
+ });
+ });
+ }
+
+ @WrapForJNI
+ private static void shutdownProcess(final Selector selector) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ final ChildConnection conn = INSTANCE.mConnections.getExistingConnection(selector);
+ if (conn == null) {
+ return;
+ }
+
+ conn.unbind();
+ }
+
+ @WrapForJNI
+ private static void setProcessPriority(
+ @NonNull final Selector selector,
+ @NonNull final PriorityLevel priorityLevel,
+ final int relativeImportance) {
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ final ChildConnection conn = INSTANCE.mConnections.getExistingConnection(selector);
+ if (conn == null) {
+ return;
+ }
+
+ conn.setPriorityLevel(priorityLevel, relativeImportance);
+ });
+ }
+
+ @WrapForJNI
+ private static GeckoResult<Integer> start(
+ final GeckoProcessType type,
+ final String[] args,
+ final int prefsFd,
+ final int prefMapFd,
+ final int ipcFd,
+ final int crashFd) {
+ final GeckoResult<Integer> result = new GeckoResult<>();
+ final StartInfo info =
+ new StartInfo(
+ type,
+ GeckoThread.InitInfo.builder()
+ .args(args)
+ .userSerialNumber(System.getenv("MOZ_ANDROID_USER_SERIAL_NUMBER"))
+ .extras(GeckoThread.getActiveExtras())
+ .flags(filterFlagsForChild(GeckoThread.getActiveFlags()))
+ .fds(
+ FileDescriptors.builder()
+ .prefs(prefsFd)
+ .prefMap(prefMapFd)
+ .ipc(ipcFd)
+ .crashReporter(crashFd)
+ .build())
+ .build());
+
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ INSTANCE
+ .start(info)
+ .accept(result::complete, result::completeExceptionally)
+ .finally_(info.pfds::close);
+ });
+
+ return result;
+ }
+
+ private static int filterFlagsForChild(final int flags) {
+ return flags & GeckoThread.FLAG_ENABLE_NATIVE_CRASHREPORTER;
+ }
+
+ private static class StartInfo {
+ final GeckoProcessType type;
+ final String crashHandler;
+ final GeckoThread.InitInfo init;
+
+ final ParcelFileDescriptors pfds;
+
+ private StartInfo(final GeckoProcessType type, final GeckoThread.InitInfo initInfo) {
+ this.type = type;
+ this.init = initInfo;
+ crashHandler =
+ GeckoAppShell.getCrashHandlerService() != null
+ ? GeckoAppShell.getCrashHandlerService().getName()
+ : null;
+ // The native side owns the File Descriptors so we cannot call adopt here.
+ pfds = ParcelFileDescriptors.from(initInfo.fds);
+ }
+ }
+
+ private static final int MAX_RETRIES = 3;
+
+ private GeckoResult<Integer> start(final StartInfo info) {
+ return start(info, new ArrayList<>());
+ }
+
+ private GeckoResult<Integer> retry(
+ final StartInfo info, final List<Throwable> retryLog, final Throwable error) {
+ retryLog.add(error);
+
+ if (error instanceof StartException) {
+ final StartException startError = (StartException) error;
+ if (startError.errorCode == IChildProcess.STARTED_BUSY) {
+ // This process is owned by a different runtime, so we can't use
+ // it. We will keep retrying indefinitely until we find a non-busy process.
+ // Note: this strategy is pretty bad, we go through each process in
+ // sequence until one works, the multiple runtime case is test-only
+ // for now, so that's ok. We can improve on this if we eventually
+ // end up needing something fancier.
+ return start(info, retryLog);
+ }
+ }
+
+ // If we couldn't unbind there's something very wrong going on and we bail
+ // immediately.
+ if (retryLog.size() >= MAX_RETRIES || error instanceof UnbindException) {
+ return GeckoResult.fromException(fromRetryLog(retryLog));
+ }
+
+ return start(info, retryLog);
+ }
+
+ private String serializeLog(final List<Throwable> retryLog) {
+ if (retryLog == null || retryLog.size() == 0) {
+ return "Empty log.";
+ }
+
+ final StringBuilder message = new StringBuilder();
+
+ for (final Throwable error : retryLog) {
+ if (error instanceof UnbindException) {
+ message.append("Could not unbind: ");
+ } else if (error instanceof StartException) {
+ message.append("Cannot restart child: ");
+ } else {
+ message.append("Error while binding: ");
+ }
+ message.append(error);
+ message.append(";");
+ }
+
+ return message.toString();
+ }
+
+ private RuntimeException fromRetryLog(final List<Throwable> retryLog) {
+ return new RuntimeException(serializeLog(retryLog), retryLog.get(retryLog.size() - 1));
+ }
+
+ private GeckoResult<Integer> start(final StartInfo info, final List<Throwable> retryLog) {
+ return startInternal(info).then(GeckoResult::fromValue, error -> retry(info, retryLog, error));
+ }
+
+ private static class StartException extends RuntimeException {
+ public final int errorCode;
+
+ public StartException(final int errorCode, final int pid) {
+ super("Could not start process, errorCode: " + errorCode + " PID: " + pid);
+ this.errorCode = errorCode;
+ }
+ }
+
+ private GeckoResult<Integer> startInternal(final StartInfo info) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ final ChildConnection connection = mConnections.getConnectionForStart(info.type);
+ return connection
+ .bind()
+ .map(
+ child -> {
+ final int result =
+ child.start(
+ this,
+ mInstanceId,
+ info.init.args,
+ info.init.extras,
+ info.init.flags,
+ info.init.userSerialNumber,
+ info.crashHandler,
+ info.pfds.prefs,
+ info.pfds.prefMap,
+ info.pfds.ipc,
+ info.pfds.crashReporter);
+ if (result == IChildProcess.STARTED_OK) {
+ return connection.getPid();
+ } else {
+ throw new StartException(result, connection.getPid());
+ }
+ })
+ .then(GeckoResult::fromValue, error -> handleBindError(connection, error));
+ }
+
+ private GeckoResult<Integer> handleBindError(
+ final ChildConnection connection, final Throwable error) {
+ return connection
+ .unbind()
+ .then(
+ unused -> GeckoResult.fromException(error),
+ unbindError -> GeckoResult.fromException(new UnbindException(unbindError)));
+ }
+
+ private static class UnbindException extends RuntimeException {
+ public UnbindException(final Throwable cause) {
+ super(cause);
+ }
+ }
+} // GeckoProcessManager
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java
new file mode 100644
index 0000000000..92ab609908
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java
@@ -0,0 +1,40 @@
+/* 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.process;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+@WrapForJNI
+public enum GeckoProcessType {
+ // These need to match the stringified names from the GeckoProcessType enum
+ PARENT("default"),
+ PLUGIN("plugin"),
+ CONTENT("tab"),
+ IPDLUNITTEST("ipdlunittest"),
+ GMPLUGIN("gmplugin"),
+ GPU("gpu"),
+ VR("vr"),
+ RDD("rdd"),
+ SOCKET("socket"),
+ REMOTESANDBOXBROKER("sandboxbroker"),
+ FORKSERVER("forkserver"),
+ UTILITY("utility");
+
+ private final String mGeckoName;
+
+ GeckoProcessType(final String geckoName) {
+ mGeckoName = geckoName;
+ }
+
+ @Override
+ public String toString() {
+ return mGeckoName;
+ }
+
+ @WrapForJNI
+ private static GeckoProcessType fromInt(final int type) {
+ return values()[type];
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
new file mode 100644
index 0000000000..f0a234a2d6
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
@@ -0,0 +1,223 @@
+/* -*- 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.process;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.GeckoThread.FileDescriptors;
+import org.mozilla.gecko.GeckoThread.ParcelFileDescriptors;
+import org.mozilla.gecko.IGeckoEditableChild;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.ICompositorSurfaceManager;
+import org.mozilla.gecko.gfx.ISurfaceAllocator;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class GeckoServiceChildProcess extends Service {
+ private static final String LOGTAG = "ServiceChildProcess";
+
+ private static IProcessManager sProcessManager;
+ private static String sOwnerProcessId;
+ private final MemoryController mMemoryController = new MemoryController();
+
+ private enum ProcessState {
+ NEW,
+ CREATED,
+ BOUND,
+ STARTED,
+ DESTROYED,
+ }
+
+ // Keep track of the process state to ensure we don't reuse the process
+ private static ProcessState sState = ProcessState.NEW;
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void getEditableParent(
+ final IGeckoEditableChild child, final long contentId, final long tabId) {
+ try {
+ sProcessManager.getEditableParent(child, contentId, tabId);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Cannot get editable", e);
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.i(LOGTAG, "onCreate");
+
+ if (sState != ProcessState.NEW) {
+ // We don't support reusing processes, and this could get us in a really weird state,
+ // so let's throw here.
+ throw new RuntimeException(
+ String.format("Cannot reuse process %s: %s", getClass().getSimpleName(), sState));
+ }
+ sState = ProcessState.CREATED;
+
+ GeckoAppShell.setApplicationContext(getApplicationContext());
+ GeckoThread.launch(); // Preload Gecko.
+ }
+
+ protected static class ChildProcessBinder extends IChildProcess.Stub {
+ @Override
+ public int getPid() {
+ return Process.myPid();
+ }
+
+ @Override
+ public int start(
+ final IProcessManager procMan,
+ final String mainProcessId,
+ final String[] args,
+ final Bundle extras,
+ final int flags,
+ final String userSerialNumber,
+ final String crashHandlerService,
+ final ParcelFileDescriptor prefsPfd,
+ final ParcelFileDescriptor prefMapPfd,
+ final ParcelFileDescriptor ipcPfd,
+ final ParcelFileDescriptor crashReporterPfd) {
+
+ final ParcelFileDescriptors pfds =
+ ParcelFileDescriptors.builder()
+ .prefs(prefsPfd)
+ .prefMap(prefMapPfd)
+ .ipc(ipcPfd)
+ .crashReporter(crashReporterPfd)
+ .build();
+
+ synchronized (GeckoServiceChildProcess.class) {
+ if (sOwnerProcessId != null && !sOwnerProcessId.equals(mainProcessId)) {
+ Log.w(
+ LOGTAG,
+ "This process belongs to a different GeckoRuntime owner: "
+ + sOwnerProcessId
+ + " process: "
+ + mainProcessId);
+ // We need to close the File Descriptors here otherwise we will leak them causing a
+ // shutdown hang.
+ pfds.close();
+ return IChildProcess.STARTED_BUSY;
+ }
+ if (sProcessManager != null) {
+ Log.e(LOGTAG, "Child process already started");
+ pfds.close();
+ return IChildProcess.STARTED_FAIL;
+ }
+ sProcessManager = procMan;
+ sOwnerProcessId = mainProcessId;
+ }
+
+ final FileDescriptors fds = pfds.detach();
+ ThreadUtils.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (crashHandlerService != null) {
+ try {
+ @SuppressWarnings("unchecked")
+ final Class<? extends Service> crashHandler =
+ (Class<? extends Service>) Class.forName(crashHandlerService);
+
+ // Native crashes are reported through pipes, so we don't have to
+ // do anything special for that.
+ GeckoAppShell.setCrashHandlerService(crashHandler);
+ GeckoAppShell.ensureCrashHandling(crashHandler);
+ } catch (final ClassNotFoundException e) {
+ Log.w(LOGTAG, "Couldn't find crash handler service " + crashHandlerService);
+ }
+ }
+
+ final GeckoThread.InitInfo info =
+ GeckoThread.InitInfo.builder()
+ .args(args)
+ .extras(extras)
+ .flags(flags)
+ .userSerialNumber(userSerialNumber)
+ .fds(fds)
+ .build();
+
+ if (GeckoThread.init(info)) {
+ GeckoThread.launch();
+ }
+ }
+ });
+ sState = ProcessState.STARTED;
+ return IChildProcess.STARTED_OK;
+ }
+
+ @Override
+ public void crash() {
+ GeckoThread.crash();
+ }
+
+ @Override
+ public ICompositorSurfaceManager getCompositorSurfaceManager() {
+ Log.e(
+ LOGTAG, "Invalid call to IChildProcess.getCompositorSurfaceManager for non-GPU process");
+ throw new AssertionError(
+ "Invalid call to IChildProcess.getCompositorSurfaceManager for non-GPU process.");
+ }
+
+ @Override
+ public ISurfaceAllocator getSurfaceAllocator(final int allocatorId) {
+ Log.e(LOGTAG, "Invalid call to IChildProcess.getSurfaceAllocator for non-GPU process");
+ throw new AssertionError(
+ "Invalid call to IChildProcess.getSurfaceAllocator for non-GPU process.");
+ }
+ }
+
+ protected Binder createBinder() {
+ return new ChildProcessBinder();
+ }
+
+ private final Binder mBinder = createBinder();
+
+ @Override
+ public void onDestroy() {
+ Log.i(LOGTAG, "Destroying GeckoServiceChildProcess");
+ sState = ProcessState.DESTROYED;
+ System.exit(0);
+ }
+
+ @Override
+ public IBinder onBind(final Intent intent) {
+ // Calling stopSelf ensures that whenever the client unbinds the process dies immediately.
+ stopSelf();
+ sState = ProcessState.BOUND;
+ return mBinder;
+ }
+
+ @Override
+ public void onTrimMemory(final int level) {
+ mMemoryController.onTrimMemory(level);
+
+ // This is currently a no-op in Service, but let's future-proof.
+ super.onTrimMemory(level);
+ }
+
+ @Override
+ public void onLowMemory() {
+ mMemoryController.onLowMemory();
+ super.onLowMemory();
+ }
+
+ /**
+ * Returns the surface allocator interface that should be used by this process to allocate
+ * Surfaces, for consumption in either the GPU process or parent process.
+ */
+ public static ISurfaceAllocator getSurfaceAllocator() throws RemoteException {
+ return sProcessManager.getSurfaceAllocator();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java
new file mode 100644
index 0000000000..e4312c7e67
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java
@@ -0,0 +1,63 @@
+/* -*- 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.process;
+
+import android.os.Binder;
+import android.util.SparseArray;
+import android.view.Surface;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.ICompositorSurfaceManager;
+import org.mozilla.gecko.gfx.ISurfaceAllocator;
+import org.mozilla.gecko.gfx.RemoteSurfaceAllocator;
+
+public class GeckoServiceGpuProcess extends GeckoServiceChildProcess {
+ private static final String LOGTAG = "ServiceGpuProcess";
+
+ private static final class GpuProcessBinder extends GeckoServiceChildProcess.ChildProcessBinder {
+ @Override
+ public ICompositorSurfaceManager getCompositorSurfaceManager() {
+ return RemoteCompositorSurfaceManager.getInstance();
+ }
+
+ @Override
+ public ISurfaceAllocator getSurfaceAllocator(final int allocatorId) {
+ return RemoteSurfaceAllocator.getInstance(allocatorId);
+ }
+ }
+
+ @Override
+ protected Binder createBinder() {
+ return new GpuProcessBinder();
+ }
+
+ public static final class RemoteCompositorSurfaceManager extends ICompositorSurfaceManager.Stub {
+ private static RemoteCompositorSurfaceManager mInstance;
+
+ @WrapForJNI
+ private static synchronized RemoteCompositorSurfaceManager getInstance() {
+ if (mInstance == null) {
+ mInstance = new RemoteCompositorSurfaceManager();
+ }
+ return mInstance;
+ }
+
+ private final SparseArray<Surface> mSurfaces = new SparseArray<Surface>();
+
+ @Override
+ public synchronized void onSurfaceChanged(final int widgetId, final Surface surface) {
+ if (surface != null) {
+ mSurfaces.put(widgetId, surface);
+ } else {
+ mSurfaces.remove(widgetId);
+ }
+ }
+
+ @WrapForJNI
+ public synchronized Surface getCompositorSurface(final int widgetId) {
+ return mSurfaces.get(widgetId);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java
new file mode 100644
index 0000000000..f2dcb7a52b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java
@@ -0,0 +1,74 @@
+/* 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.process;
+
+import android.content.ComponentCallbacks2;
+import android.content.res.Configuration;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import org.mozilla.gecko.GeckoAppShell;
+
+public class MemoryController implements ComponentCallbacks2 {
+ private static final String LOGTAG = "MemoryController";
+ private long mLastLowMemoryNotificationTime = 0;
+
+ // Allowed elapsed time between full GCs while under constant memory pressure
+ private static final long LOW_MEMORY_ONGOING_RESET_TIME_MS = 10000;
+
+ private static final int LOW = 0;
+ private static final int MODERATE = 1;
+ private static final int CRITICAL = 2;
+
+ private int memoryLevelFromTrim(final int level) {
+ if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE
+ || level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
+ return CRITICAL;
+ } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
+ return MODERATE;
+ }
+ return LOW;
+ }
+
+ public void onTrimMemory(final int level) {
+ Log.i(LOGTAG, "onTrimMemory(" + level + ")");
+ onMemoryNotification(memoryLevelFromTrim(level));
+ }
+
+ @Override
+ public void onConfigurationChanged(final @NonNull Configuration newConfig) {}
+
+ public void onLowMemory() {
+ Log.i(LOGTAG, "onLowMemory");
+ onMemoryNotification(CRITICAL);
+ }
+
+ private void onMemoryNotification(final int level) {
+ if (level == LOW) {
+ // The trim level is too low to be actionable
+ return;
+ }
+
+ // See nsIMemory.idl for descriptions of the various arguments to the "memory-pressure"
+ // observer.
+ final String observerArg;
+
+ final long currentNotificationTime = System.currentTimeMillis();
+ if (level == CRITICAL
+ || (currentNotificationTime - mLastLowMemoryNotificationTime)
+ >= LOW_MEMORY_ONGOING_RESET_TIME_MS) {
+ // We do a full "low-memory" notification for both new and last-ditch onTrimMemory requests.
+ observerArg = "low-memory";
+ mLastLowMemoryNotificationTime = currentNotificationTime;
+ } else {
+ // If it has been less than ten seconds since the last time we sent a "low-memory"
+ // notification, we send a "low-memory-ongoing" notification instead.
+ // This prevents Gecko from re-doing full GC's repeatedly over and over in succession,
+ // as they are expensive and quickly result in diminishing returns.
+ observerArg = "low-memory-ongoing";
+ }
+
+ GeckoAppShell.notifyObservers("memory-pressure", observerArg);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java
new file mode 100644
index 0000000000..496fa9d2be
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java
@@ -0,0 +1,613 @@
+/* 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.process;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import java.security.SecureRandom;
+import java.util.BitSet;
+import java.util.EnumMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.UUID;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.XPCOMEventTarget;
+
+/* package */ final class ServiceAllocator {
+ private static final String LOGTAG = "ServiceAllocator";
+ private static final int MAX_NUM_ISOLATED_CONTENT_SERVICES =
+ GeckoChildProcessServices.MAX_NUM_ISOLATED_CONTENT_SERVICES;
+
+ private static boolean hasQApis() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
+ }
+
+ /**
+ * Possible priority levels that are available to child services. Each one maps to a flag that is
+ * passed into Context.bindService().
+ */
+ @WrapForJNI
+ public enum PriorityLevel {
+ FOREGROUND(Context.BIND_IMPORTANT),
+ BACKGROUND(0),
+ IDLE(Context.BIND_WAIVE_PRIORITY);
+
+ private final int mAndroidFlag;
+
+ PriorityLevel(final int androidFlag) {
+ mAndroidFlag = androidFlag;
+ }
+
+ public int getAndroidFlag() {
+ return mAndroidFlag;
+ }
+ }
+
+ public static final class BindException extends RuntimeException {
+ public BindException(@NonNull final String msg) {
+ super(msg);
+ }
+ }
+
+ private interface BindServiceDelegate {
+ boolean bindService(ServiceConnection binding, PriorityLevel priority);
+
+ String getServiceName();
+ }
+
+ /**
+ * Abstract class that holds the essential per-service data that is required to work with
+ * ServiceAllocator. ServiceAllocator clients should extend this class when implementing their
+ * per-service connection objects.
+ */
+ public abstract static class InstanceInfo {
+ private class Binding implements ServiceConnection {
+ /**
+ * This implementation of ServiceConnection.onServiceConnected simply bounces the connection
+ * notification over to the launcher thread (if it is not already on it).
+ */
+ @Override
+ public final void onServiceConnected(final ComponentName name, final IBinder service) {
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ onBinderConnectedInternal(service);
+ });
+ }
+
+ /**
+ * This implementation of ServiceConnection.onServiceDisconnected simply bounces the
+ * disconnection notification over to the launcher thread (if it is not already on it).
+ */
+ @Override
+ public final void onServiceDisconnected(final ComponentName name) {
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ onBinderConnectionLostInternal();
+ });
+ }
+ }
+
+ private class DefaultBindDelegate implements BindServiceDelegate {
+ @Override
+ public boolean bindService(
+ @NonNull final ServiceConnection binding, @NonNull final PriorityLevel priority) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final Intent intent = new Intent();
+ intent.setClassName(context, getServiceName());
+ return bindServiceDefault(context, intent, binding, getAndroidFlags(priority));
+ }
+
+ @Override
+ public String getServiceName() {
+ return getSvcClassNameDefault(InstanceInfo.this);
+ }
+ }
+
+ private class IsolatedBindDelegate implements BindServiceDelegate {
+ @Override
+ public boolean bindService(
+ @NonNull final ServiceConnection binding, @NonNull final PriorityLevel priority) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final Intent intent = new Intent();
+ intent.setClassName(context, getServiceName());
+ return bindServiceIsolated(
+ context, intent, getAndroidFlags(priority), getIdInternal(), binding);
+ }
+
+ @Override
+ public String getServiceName() {
+ return ServiceUtils.buildIsolatedSvcName(getType());
+ }
+ }
+
+ private final ServiceAllocator mAllocator;
+ private final GeckoProcessType mType;
+ private final String mId;
+ private final EnumMap<PriorityLevel, Binding> mBindings;
+ private final BindServiceDelegate mBindDelegate;
+
+ private boolean mCalledConnected = false;
+ private boolean mCalledConnectionLost = false;
+ private boolean mIsDefunct = false;
+
+ private PriorityLevel mCurrentPriority;
+ private int mRelativeImportance = 0;
+
+ protected InstanceInfo(
+ @NonNull final ServiceAllocator allocator,
+ @NonNull final GeckoProcessType type,
+ @NonNull final PriorityLevel initialPriority) {
+ mAllocator = allocator;
+ mType = type;
+ mId = mAllocator.allocate(type);
+ mBindings = new EnumMap<PriorityLevel, Binding>(PriorityLevel.class);
+ mBindDelegate = getBindServiceDelegate();
+
+ mCurrentPriority = initialPriority;
+ }
+
+ private BindServiceDelegate getBindServiceDelegate() {
+ if (mType != GeckoProcessType.CONTENT) {
+ // Non-content services just use default binding
+ return this.new DefaultBindDelegate();
+ }
+
+ // Content services defer to the alloc policy
+ return mAllocator.mContentAllocPolicy.getBindServiceDelegate(this);
+ }
+
+ public PriorityLevel getPriorityLevel() {
+ XPCOMEventTarget.assertOnLauncherThread();
+ return mCurrentPriority;
+ }
+
+ public boolean setPriorityLevel(@NonNull final PriorityLevel newPriority) {
+ return setPriorityLevel(newPriority, 0);
+ }
+
+ public boolean setPriorityLevel(
+ @NonNull final PriorityLevel newPriority, final int relativeImportance) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ mCurrentPriority = newPriority;
+ mRelativeImportance = relativeImportance;
+
+ // If we haven't bound yet then we can just return
+ if (mBindings.size() == 0) {
+ return true;
+ }
+
+ // Otherwise we need to update our bindings
+ return updateBindings();
+ }
+
+ /**
+ * Only content services have unique IDs. This method throws if called for a non-content service
+ * type.
+ */
+ public String getId() {
+ if (mId == null) {
+ throw new RuntimeException("This service does not have a unique id");
+ }
+
+ return mId;
+ }
+
+ /** This method is infallible and returns an empty string for non-content services. */
+ private String getIdInternal() {
+ return mId == null ? "" : mId;
+ }
+
+ public boolean isContent() {
+ return mType == GeckoProcessType.CONTENT;
+ }
+
+ public GeckoProcessType getType() {
+ return mType;
+ }
+
+ protected boolean bindService() {
+ if (mIsDefunct) {
+ final String errorMsg =
+ "Attempt to bind a defunct InstanceInfo for " + mType + " child process";
+ throw new BindException(errorMsg);
+ }
+
+ return updateBindings();
+ }
+
+ /**
+ * Unbinds the service described by |this| and releases our unique ID. This method may safely be
+ * called multiple times even if we are already defunct.
+ */
+ protected void unbindService() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ // This could happen if a service death races with our attempt to shut it down.
+ if (mIsDefunct) {
+ return;
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+
+ // Make a clone of mBindings to iterate over since we're going to mutate the original
+ final EnumMap<PriorityLevel, Binding> cloned = mBindings.clone();
+ for (final Entry<PriorityLevel, Binding> entry : cloned.entrySet()) {
+ try {
+ context.unbindService(entry.getValue());
+ } catch (final IllegalArgumentException e) {
+ // The binding was already dead. That's okay.
+ }
+
+ mBindings.remove(entry.getKey());
+ }
+
+ if (mBindings.size() != 0) {
+ throw new IllegalStateException("Unable to release all bindings");
+ }
+
+ mIsDefunct = true;
+ mAllocator.release(this);
+ onReleaseResources();
+ }
+
+ private void onBinderConnectedInternal(@NonNull final IBinder service) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ // We only care about the first time this is called; subsequent bindings can be ignored.
+ if (mCalledConnected) {
+ return;
+ }
+
+ mCalledConnected = true;
+
+ onBinderConnected(service);
+ }
+
+ private void onBinderConnectionLostInternal() {
+ XPCOMEventTarget.assertOnLauncherThread();
+ // We only care about the first time this is called; subsequent connection errors can be
+ // ignored.
+ if (mCalledConnectionLost) {
+ return;
+ }
+
+ mCalledConnectionLost = true;
+
+ onBinderConnectionLost();
+ }
+
+ protected abstract void onBinderConnected(@NonNull final IBinder service);
+
+ protected abstract void onReleaseResources();
+
+ // Optionally overridable by subclasses, but this is a sane default
+ protected void onBinderConnectionLost() {
+ // The binding has lost its connection, but the binding itself might still be active.
+ // Gecko itself will request a process restart, so here we attempt to unbind so that
+ // Android does not try to automatically restart and reconnect the service.
+ unbindService();
+ }
+
+ /**
+ * This function relies on the fact that the PriorityLevel enum is ordered from highest priority
+ * to lowest priority. We examine the ordinal of the current priority setting, and then iterate
+ * across all possible priority levels, adjusting as necessary. Any priority levels whose
+ * ordinals are less than then current priority level ordinal must be unbound, while all
+ * priority levels whose ordinals are greater than or equal to the current priority level
+ * ordinal must be bound.
+ */
+ @TargetApi(29)
+ private boolean updateBindings() {
+ XPCOMEventTarget.assertOnLauncherThread();
+ int numBindSuccesses = 0;
+ int numBindFailures = 0;
+ int numUnbindSuccesses = 0;
+
+ final Context context = GeckoAppShell.getApplicationContext();
+
+ // This code assumes that the order of the PriorityLevel enum is highest to lowest
+ final int curPriorityOrdinal = mCurrentPriority.ordinal();
+ final PriorityLevel[] levels = PriorityLevel.values();
+
+ for (int curLevelIdx = 0; curLevelIdx < levels.length; ++curLevelIdx) {
+ final PriorityLevel curLevel = levels[curLevelIdx];
+ final Binding existingBinding = mBindings.get(curLevel);
+ final boolean hasExistingBinding = existingBinding != null;
+
+ if (curLevelIdx < curPriorityOrdinal) {
+ // Remove if present
+ if (hasExistingBinding) {
+ try {
+ context.unbindService(existingBinding);
+ ++numUnbindSuccesses;
+ mBindings.remove(curLevel);
+ } catch (final IllegalArgumentException e) {
+ // The binding was already dead. That's okay.
+ ++numUnbindSuccesses;
+ mBindings.remove(curLevel);
+ }
+ }
+ } else {
+ // Normally we only need to do a bind if we do not yet have an existing binding
+ // for this priority level.
+ boolean bindNeeded = !hasExistingBinding;
+
+ // We only update the service group when the binding for this level already
+ // exists and no binds have occurred yet during the current updateBindings call.
+ if (hasExistingBinding && hasQApis() && (numBindSuccesses + numBindFailures) == 0) {
+ // NB: Right now we're passing 0 as the |group| argument, indicating that
+ // the process is not grouped with any other processes. Once we support
+ // Fission we should re-evaluate this.
+ context.updateServiceGroup(existingBinding, 0, mRelativeImportance);
+ // Now we need to call bindService with the existing binding to make this
+ // change take effect.
+ bindNeeded = true;
+ }
+
+ if (bindNeeded) {
+ final Binding useBinding = hasExistingBinding ? existingBinding : this.new Binding();
+ if (mBindDelegate.bindService(useBinding, curLevel)) {
+ ++numBindSuccesses;
+ if (!hasExistingBinding) {
+ mBindings.put(curLevel, useBinding);
+ }
+ } else {
+ ++numBindFailures;
+ }
+ }
+ }
+ }
+
+ final String svcName = mBindDelegate.getServiceName();
+ final StringBuilder builder = new StringBuilder(svcName);
+ builder
+ .append(" updateBindings: ")
+ .append(mCurrentPriority)
+ .append(" priority, ")
+ .append(mRelativeImportance)
+ .append(" importance, ")
+ .append(numBindSuccesses)
+ .append(" successful binds, ")
+ .append(numBindFailures)
+ .append(" failed binds, ")
+ .append(numUnbindSuccesses)
+ .append(" successful unbinds");
+ Log.d(LOGTAG, builder.toString());
+
+ return numBindFailures == 0;
+ }
+ }
+
+ private interface ContentAllocationPolicy {
+ /**
+ * @return BindServiceDelegate that will be used for binding a new content service.
+ */
+ BindServiceDelegate getBindServiceDelegate(InstanceInfo info);
+
+ /**
+ * Allocate an unused service ID for use by the caller.
+ *
+ * @return The new service id.
+ */
+ String allocate();
+
+ /**
+ * Release a previously used service ID.
+ *
+ * @param id The service id being released.
+ */
+ void release(final String id);
+ }
+
+ /**
+ * This policy is intended for Android versions &lt; 10, as well as for content process services
+ * that are not defined as isolated processes. In this case, the number of possible content
+ * service IDs has a fixed upper bound, so we use a BitSet to manage their allocation.
+ */
+ private static final class DefaultContentPolicy implements ContentAllocationPolicy {
+ private final int mMaxNumSvcs;
+ private final BitSet mAllocator;
+ private final SecureRandom mRandom;
+
+ public DefaultContentPolicy() {
+ mMaxNumSvcs = getContentServiceCount();
+ mAllocator = new BitSet(mMaxNumSvcs);
+ mRandom = new SecureRandom();
+ }
+
+ @Override
+ public BindServiceDelegate getBindServiceDelegate(@NonNull final InstanceInfo info) {
+ return info.new DefaultBindDelegate();
+ }
+
+ @Override
+ public String allocate() {
+ final int[] available = new int[mMaxNumSvcs];
+ int size = 0;
+ for (int i = 0; i < mMaxNumSvcs; i++) {
+ if (!mAllocator.get(i)) {
+ available[size] = i;
+ size++;
+ }
+ }
+
+ if (size == 0) {
+ throw new RuntimeException("No more content services available");
+ }
+
+ final int next = available[mRandom.nextInt(size)];
+ mAllocator.set(next);
+ return Integer.toString(next);
+ }
+
+ @Override
+ public void release(final String stringId) {
+ final int id = Integer.valueOf(stringId);
+ if (!mAllocator.get(id)) {
+ throw new IllegalStateException("Releasing an unallocated id=" + id);
+ }
+
+ mAllocator.clear(id);
+ }
+
+ /**
+ * @return The number of content services defined in our manifest.
+ */
+ private static int getContentServiceCount() {
+ return ServiceUtils.getServiceCount(
+ GeckoAppShell.getApplicationContext(), GeckoProcessType.CONTENT);
+ }
+ }
+
+ /**
+ * This policy is intended for Android versions &gt;= 10 when our content process services are
+ * defined in our manifest as having isolated processes. Since isolated services share a single
+ * service definition, there is no longer an Android-induced hard limit on the number of content
+ * processes that may be started. We simply use a monotonically-increasing counter to generate
+ * unique instance IDs in this case.
+ */
+ private static final class IsolatedContentPolicy implements ContentAllocationPolicy {
+ private final Set<String> mRunningServiceIds = new HashSet<>();
+
+ @Override
+ public BindServiceDelegate getBindServiceDelegate(@NonNull final InstanceInfo info) {
+ return info.new IsolatedBindDelegate();
+ }
+
+ /**
+ * We generate a new instance ID simply by incrementing a counter. We do track how many content
+ * services are currently active for the purposes of maintaining the configured limit on number
+ * of simultaneous content processes.
+ */
+ @Override
+ public String allocate() {
+ if (mRunningServiceIds.size() >= MAX_NUM_ISOLATED_CONTENT_SERVICES) {
+ throw new RuntimeException("No more content services available");
+ }
+
+ final String newId = UUID.randomUUID().toString();
+ mRunningServiceIds.add(newId);
+ return newId;
+ }
+
+ /** Just drop the count of active services. */
+ @Override
+ public void release(final String id) {
+ if (!mRunningServiceIds.remove(id)) {
+ throw new IllegalStateException("Releasing an unallocated id");
+ }
+ }
+ }
+
+ /** The policy used for allocating content processes. */
+ private ContentAllocationPolicy mContentAllocPolicy = null;
+
+ /**
+ * Allocate a service ID.
+ *
+ * @param type The type of service.
+ * @return Integer encapsulating the service ID, or null if no ID is necessary.
+ */
+ private String allocate(@NonNull final GeckoProcessType type) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (type != GeckoProcessType.CONTENT) {
+ // No unique id necessary
+ return null;
+ }
+
+ // Lazy initialization of mContentAllocPolicy to ensure that it is constructed on the
+ // launcher thread.
+ if (mContentAllocPolicy == null) {
+ if (canBindIsolated(GeckoProcessType.CONTENT)) {
+ mContentAllocPolicy = new IsolatedContentPolicy();
+ } else {
+ mContentAllocPolicy = new DefaultContentPolicy();
+ }
+ }
+
+ return mContentAllocPolicy.allocate();
+ }
+
+ /**
+ * Free a defunct service's ID if necessary.
+ *
+ * @param info The InstanceInfo-derived object that contains essential information for tearing
+ * down the child service.
+ */
+ private void release(@NonNull final InstanceInfo info) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (!info.isContent()) {
+ return;
+ }
+
+ mContentAllocPolicy.release(info.getId());
+ }
+
+ /**
+ * Find out whether the desired service type is defined in our manifest as having an isolated
+ * process.
+ *
+ * @param type Service type to query
+ * @return true if this service type may use isolated binding, otherwise false.
+ */
+ private static boolean canBindIsolated(@NonNull final GeckoProcessType type) {
+ if (!hasQApis()) {
+ return false;
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+ final int svcFlags = ServiceUtils.getServiceFlags(context, type);
+ return (svcFlags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0;
+ }
+
+ /** Convert PriorityLevel into the flags argument to Context.bindService() et al */
+ private static int getAndroidFlags(@NonNull final PriorityLevel priority) {
+ return Context.BIND_AUTO_CREATE | priority.getAndroidFlag();
+ }
+
+ /** Obtain the class name to use for service binding in the default (ie, non-isolated) case. */
+ private static String getSvcClassNameDefault(@NonNull final InstanceInfo info) {
+ return ServiceUtils.buildSvcName(info.getType(), info.getIdInternal());
+ }
+
+ /**
+ * Wrapper for bindService() that utilizes the Context.bindService() overload that accepts an
+ * Executor argument, when available. Otherwise it falls back to the legacy overload.
+ */
+ @TargetApi(29)
+ private static boolean bindServiceDefault(
+ @NonNull final Context context,
+ @NonNull final Intent intent,
+ @NonNull final ServiceConnection conn,
+ final int flags) {
+ if (hasQApis()) {
+ // We always specify the launcher thread as our Executor.
+ return context.bindService(intent, flags, XPCOMEventTarget.launcherThread(), conn);
+ }
+
+ return context.bindService(intent, conn, flags);
+ }
+
+ @TargetApi(29)
+ private static boolean bindServiceIsolated(
+ @NonNull final Context context,
+ @NonNull final Intent intent,
+ final int flags,
+ @NonNull final String instanceId,
+ @NonNull final ServiceConnection conn) {
+ // We always specify the launcher thread as our Executor.
+ return context.bindIsolatedService(
+ intent, flags, instanceId, XPCOMEventTarget.launcherThread(), conn);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java
new file mode 100644
index 0000000000..695c69666b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java
@@ -0,0 +1,141 @@
+/* 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.process;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import androidx.annotation.NonNull;
+
+/* package */ final class ServiceUtils {
+ private static final String DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX = "0";
+
+ private ServiceUtils() {}
+
+ /**
+ * @return StringBuilder containing the name of a service class but not qualifed with any unique
+ * identifiers.
+ */
+ private static StringBuilder startSvcName(@NonNull final GeckoProcessType type) {
+ final StringBuilder builder = new StringBuilder(GeckoChildProcessServices.class.getName());
+ builder.append("$").append(type);
+ return builder;
+ }
+
+ /**
+ * Given a service's GeckoProcessType, obtain the name of its class, including any qualifiers that
+ * are needed to uniquely identify its manifest definition.
+ */
+ public static String buildSvcName(
+ @NonNull final GeckoProcessType type, final String... suffixes) {
+ final StringBuilder builder = startSvcName(type);
+
+ for (final String suffix : suffixes) {
+ builder.append(suffix);
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Given a service's GeckoProcessType, obtain the name of its class to be used for the purpose of
+ * binding as an isolated service.
+ *
+ * <p>Content services are defined in the manifest as "tab0" through "tabN" for some value of N.
+ * For the purposes of binding to an isolated content service, we simply need to repeatedly re-use
+ * the definition of "tab0", the "0" being stored as the
+ * DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX constant.
+ */
+ public static String buildIsolatedSvcName(@NonNull final GeckoProcessType type) {
+ if (type == GeckoProcessType.CONTENT) {
+ return buildSvcName(type, DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX);
+ }
+
+ // Non-content services do not require any unique IDs
+ return buildSvcName(type);
+ }
+
+ /**
+ * Given a service's GeckoProcessType, obtain the unqualified name of its class.
+ *
+ * @return The name of the class that hosts the implementation of the service corresponding to
+ * type, but without any unique identifiers that may be required to actually instantiate it.
+ */
+ private static String buildSvcNamePrefix(@NonNull final GeckoProcessType type) {
+ return startSvcName(type).toString();
+ }
+
+ /**
+ * Extracts flags from the manifest definition of a service.
+ *
+ * @param context Context to use for extraction
+ * @param type Service type
+ * @return flags that are specified in the service's definition in our manifest.
+ * @see android.content.pm.ServiceInfo for explanation of the various flags.
+ */
+ public static int getServiceFlags(
+ @NonNull final Context context, @NonNull final GeckoProcessType type) {
+ final ComponentName component = new ComponentName(context, buildIsolatedSvcName(type));
+ final PackageManager pkgMgr = context.getPackageManager();
+
+ try {
+ final ServiceInfo svcInfo = pkgMgr.getServiceInfo(component, 0);
+ // svcInfo is never null
+ return svcInfo.flags;
+ } catch (final PackageManager.NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Obtain the list of all services defined for |context|. */
+ private static ServiceInfo[] getServiceList(@NonNull final Context context) {
+ final PackageInfo packageInfo;
+ try {
+ packageInfo =
+ context
+ .getPackageManager()
+ .getPackageInfo(context.getPackageName(), PackageManager.GET_SERVICES);
+ } catch (final PackageManager.NameNotFoundException e) {
+ throw new AssertionError("Should not happen: Can't get package info of own package");
+ }
+ return packageInfo.services;
+ }
+
+ /**
+ * Count the number of service definitions in our manifest that satisfy bindings for a particular
+ * service type.
+ *
+ * @param context Context object to use for extracting the service definitions
+ * @param type The type of service to count
+ * @return The number of available service definitions.
+ */
+ public static int getServiceCount(
+ @NonNull final Context context, @NonNull final GeckoProcessType type) {
+ final ServiceInfo[] svcList = getServiceList(context);
+ final String serviceNamePrefix = buildSvcNamePrefix(type);
+
+ int result = 0;
+ for (final ServiceInfo svc : svcList) {
+ final String svcName = svc.name;
+ // If svcName starts with serviceNamePrefix, then both strings must either be equal
+ // or else the first subsequent character in svcName must be a digit.
+ // This guards against any future GeckoProcessType whose string representation shares
+ // a common prefix with another GeckoProcessType value.
+ if (svcName.startsWith(serviceNamePrefix)
+ && (svcName.length() == serviceNamePrefix.length()
+ || Character.isDigit(svcName.codePointAt(serviceNamePrefix.length())))) {
+ ++result;
+ }
+ }
+
+ if (result <= 0) {
+ throw new RuntimeException("Could not count " + serviceNamePrefix + " services in manifest");
+ }
+
+ return result;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
new file mode 100644
index 0000000000..b8d7ea3107
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/BundleEventListener.java
@@ -0,0 +1,21 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+@RobocopTarget
+public interface BundleEventListener {
+ /**
+ * Handles a message sent from Gecko.
+ *
+ * @param event The name of the event being sent.
+ * @param message The message data.
+ * @param callback The callback interface for this message. A callback is provided only if the
+ * originating call included a callback argument; otherwise, callback will be null.
+ */
+ void handleMessage(String event, GeckoBundle message, EventCallback callback);
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java
new file mode 100644
index 0000000000..7a445de90a
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/DebugConfig.java
@@ -0,0 +1,130 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.os.Bundle;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.TypeDescription;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.Constructor;
+import org.yaml.snakeyaml.error.YAMLException;
+
+// Raptor writes a *-config.yaml file to specify Gecko runtime settings (e.g.
+// the profile dir). This file gets deserialized into a DebugConfig object.
+// Yaml uses reflection to create this class so we have to tell PG to keep it.
+@ReflectionTarget
+public class DebugConfig {
+ private static final String LOGTAG = "GeckoDebugConfig";
+
+ protected Map<String, Object> prefs;
+ protected Map<String, String> env;
+ protected List<String> args;
+
+ public static class ConfigException extends RuntimeException {
+ public ConfigException(final String message) {
+ super(message);
+ }
+ }
+
+ public static @NonNull DebugConfig fromFile(final @NonNull File configFile)
+ throws FileNotFoundException {
+ final LoaderOptions options = new LoaderOptions();
+ final Constructor constructor = new Constructor(DebugConfig.class, options);
+ final TypeDescription description = new TypeDescription(DebugConfig.class);
+ description.putMapPropertyType("prefs", String.class, Object.class);
+ description.putMapPropertyType("env", String.class, String.class);
+ description.putListPropertyType("args", String.class);
+
+ final Yaml yaml = new Yaml(constructor);
+ yaml.addTypeDescription(description);
+
+ final FileInputStream fileInputStream = new FileInputStream(configFile);
+ try {
+ return yaml.load(fileInputStream);
+ } catch (final YAMLException e) {
+ throw new ConfigException(e.getMessage());
+ } finally {
+ try {
+ if (fileInputStream != null) {
+ ((Closeable) fileInputStream).close();
+ }
+ } catch (final IOException e) {
+ }
+ }
+ }
+
+ @Nullable
+ public Bundle mergeIntoExtras(final @Nullable Bundle extras) {
+ if (env == null) {
+ return extras;
+ }
+
+ Log.d(LOGTAG, "Adding environment variables from debug config: " + env);
+
+ final Bundle result = extras != null ? extras : new Bundle();
+
+ int c = 0;
+ while (result.getString("env" + c) != null) {
+ c += 1;
+ }
+
+ for (final Map.Entry<String, String> entry : env.entrySet()) {
+ result.putString("env" + c, entry.getKey() + "=" + entry.getValue());
+ c += 1;
+ }
+
+ return result;
+ }
+
+ @Nullable
+ public String[] mergeIntoArgs(final @Nullable String[] initArgs) {
+ if (args == null) {
+ return initArgs;
+ }
+
+ Log.d(LOGTAG, "Adding arguments from debug config: " + args);
+
+ final ArrayList<String> combinedArgs = new ArrayList<>();
+ if (initArgs != null) {
+ combinedArgs.addAll(Arrays.asList(initArgs));
+ }
+ combinedArgs.addAll(args);
+
+ return combinedArgs.toArray(new String[combinedArgs.size()]);
+ }
+
+ @Nullable
+ public Map<String, Object> mergeIntoPrefs(final @Nullable Map<String, Object> initPrefs) {
+ if (prefs == null) {
+ return initPrefs;
+ }
+
+ Log.d(LOGTAG, "Adding prefs from debug config: " + prefs);
+
+ final Map<String, Object> combinedPrefs = new HashMap<>();
+ if (initPrefs != null) {
+ combinedPrefs.putAll(initPrefs);
+ }
+ combinedPrefs.putAll(prefs);
+
+ return Collections.unmodifiableMap(combinedPrefs);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
new file mode 100644
index 0000000000..3ef469ac1b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/EventCallback.java
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import androidx.annotation.Nullable;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.geckoview.GeckoResult;
+
+/**
+ * Callback interface for Gecko requests.
+ *
+ * <p>For each instance of EventCallback, exactly one of sendResponse, sendError, or sendCancel must
+ * be called to prevent observer leaks. If more than one send* method is called, or if a single send
+ * method is called multiple times, an {@link IllegalStateException} will be thrown.
+ */
+@RobocopTarget
+@WrapForJNI(calledFrom = "gecko")
+public interface EventCallback {
+ /**
+ * Sends a success response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ void sendSuccess(Object response);
+
+ /**
+ * Sends an error response with the given data.
+ *
+ * @param response The response data to send to Gecko. Can be any of the types accepted by
+ * JSONObject#put(String, Object).
+ */
+ void sendError(Object response);
+
+ /**
+ * Resolve this Event callback with the result from the {@link GeckoResult}.
+ *
+ * @param response the result that will be used for this callback.
+ */
+ default <T> void resolveTo(final @Nullable GeckoResult<T> response) {
+ if (response == null) {
+ sendSuccess(null);
+ return;
+ }
+ response.accept(
+ this::sendSuccess,
+ throwable -> {
+ // Don't propagate Errors, just crash
+ if (!(throwable instanceof Exception)) {
+ throw new GeckoResult.UncaughtException(throwable);
+ }
+ sendError(throwable.getMessage());
+ });
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
new file mode 100644
index 0000000000..01b177fe21
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBackgroundThread.java
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.os.Handler;
+import android.os.Looper;
+
+final class GeckoBackgroundThread extends Thread {
+ private static final String LOOPER_NAME = "GeckoBackgroundThread";
+
+ // Guarded by 'GeckoBackgroundThread.class'.
+ private static Handler handler;
+ private static Thread thread;
+
+ // The initial Runnable to run on the new thread. Its purpose
+ // is to avoid us having to wait for the new thread to start.
+ private Runnable mInitialRunnable;
+
+ // Singleton, so private constructor.
+ private GeckoBackgroundThread(final Runnable initialRunnable) {
+ mInitialRunnable = initialRunnable;
+ }
+
+ @Override
+ public void run() {
+ setName(LOOPER_NAME);
+ Looper.prepare();
+
+ synchronized (GeckoBackgroundThread.class) {
+ handler = new Handler();
+ GeckoBackgroundThread.class.notifyAll();
+ }
+
+ if (mInitialRunnable != null) {
+ mInitialRunnable.run();
+ mInitialRunnable = null;
+ }
+
+ Looper.loop();
+ }
+
+ private static void startThread(final Runnable initialRunnable) {
+ thread = new GeckoBackgroundThread(initialRunnable);
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ // Get a Handler for a looper thread, or create one if it doesn't yet exist.
+ /*package*/ static synchronized Handler getHandler() {
+ if (thread == null) {
+ startThread(null);
+ }
+
+ while (handler == null) {
+ try {
+ GeckoBackgroundThread.class.wait();
+ } catch (final InterruptedException e) {
+ }
+ }
+ return handler;
+ }
+
+ /*package*/ static synchronized void post(final Runnable runnable) {
+ if (thread == null) {
+ startThread(runnable);
+ return;
+ }
+ getHandler().post(runnable);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java
new file mode 100644
index 0000000000..315c4a89d7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java
@@ -0,0 +1,1194 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.collection.SimpleArrayMap;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+/**
+ * A lighter-weight version of Bundle that adds support for type coercion (e.g. int to double) in
+ * order to better cooperate with JS objects.
+ */
+@RobocopTarget
+public final class GeckoBundle implements Parcelable {
+ private static final String LOGTAG = "GeckoBundle";
+ private static final boolean DEBUG = false;
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
+
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+ private static final int[] EMPTY_INT_ARRAY = new int[0];
+ private static final long[] EMPTY_LONG_ARRAY = new long[0];
+ private static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ private static final GeckoBundle[] EMPTY_BUNDLE_ARRAY = new GeckoBundle[0];
+
+ private SimpleArrayMap<String, Object> mMap;
+
+ /** Construct an empty GeckoBundle. */
+ public GeckoBundle() {
+ mMap = new SimpleArrayMap<>();
+ }
+
+ /**
+ * Construct an empty GeckoBundle with specific capacity.
+ *
+ * @param capacity Initial capacity.
+ */
+ public GeckoBundle(final int capacity) {
+ mMap = new SimpleArrayMap<>(capacity);
+ }
+
+ /**
+ * Construct a copy of another GeckoBundle.
+ *
+ * @param bundle GeckoBundle to copy from.
+ */
+ public GeckoBundle(final GeckoBundle bundle) {
+ mMap = new SimpleArrayMap<>(bundle.mMap);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private GeckoBundle(final String[] keys, final Object[] values) {
+ final int len = keys.length;
+ mMap = new SimpleArrayMap<>(len);
+ for (int i = 0; i < len; i++) {
+ mMap.put(keys[i], values[i]);
+ }
+ }
+
+ /** Clear all mappings. */
+ public void clear() {
+ mMap.clear();
+ }
+
+ /**
+ * Returns whether a mapping exists. Null String, Bundle, or arrays are treated as nonexistent.
+ *
+ * @param key Key to look for.
+ * @return True if the specified key exists and the value is not null.
+ */
+ public boolean containsKey(final String key) {
+ return mMap.get(key) != null;
+ }
+
+ /**
+ * Returns the value associated with a mapping as an Object.
+ *
+ * @param key Key to look for.
+ * @return Mapping value or null if the mapping does not exist.
+ */
+ public Object get(final String key) {
+ return mMap.get(key);
+ }
+
+ /**
+ * Returns the value associated with a boolean mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Boolean value
+ */
+ public boolean getBoolean(final String key, final boolean defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : (Boolean) value;
+ }
+
+ /**
+ * Returns the value associated with a boolean mapping, or false if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Boolean value
+ */
+ public boolean getBoolean(final String key) {
+ return getBoolean(key, false);
+ }
+
+ /**
+ * Returns the value associated with a Boolean mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Boolean value
+ */
+ public Boolean getBooleanObject(final String key, final Boolean defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : (Boolean) value;
+ }
+
+ /**
+ * Returns the value associated with a Boolean mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Boolean value
+ */
+ public Boolean getBooleanObject(final String key) {
+ return getBooleanObject(key, null);
+ }
+
+ /**
+ * Returns the value associated with a boolean array mapping, or null if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return Boolean array value
+ */
+ public boolean[] getBooleanArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0 ? EMPTY_BOOLEAN_ARRAY : (boolean[]) value;
+ }
+
+ /**
+ * Returns the value associated with a double mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Double value
+ */
+ public double getDouble(final String key, final double defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : ((Number) value).doubleValue();
+ }
+
+ /**
+ * Returns the value associated with a double mapping, or 0.0 if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Double value
+ */
+ public double getDouble(final String key) {
+ return getDouble(key, 0.0);
+ }
+
+ private static double[] getDoubleArray(final int[] array) {
+ final int len = array.length;
+ final double[] ret = new double[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = (double) array[i];
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the value associated with a double array mapping, or null if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return Double array value
+ */
+ public double[] getDoubleArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0
+ ? EMPTY_DOUBLE_ARRAY
+ : value instanceof int[] ? getDoubleArray((int[]) value) : (double[]) value;
+ }
+
+ /**
+ * Returns the value associated with a Double mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Double value
+ */
+ public Double getDoubleObject(final String key) {
+ return getDoubleObject(key, null);
+ }
+
+ /**
+ * Returns the value associated with a Double mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return Double value
+ */
+ public Double getDoubleObject(final String key, final Double defaultValue) {
+ final Object value = mMap.get(key);
+ if (value == null) {
+ return defaultValue;
+ }
+ return ((Number) value).doubleValue();
+ }
+
+ /**
+ * Returns the value associated with an int mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Int value
+ */
+ public int getInt(final String key, final int defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : ((Number) value).intValue();
+ }
+
+ /**
+ * Returns the value associated with an int mapping, or 0 if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Int value
+ */
+ public int getInt(final String key) {
+ return getInt(key, 0);
+ }
+
+ /**
+ * Returns the value associated with an Integer mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Int value
+ */
+ public Integer getInteger(final String key, final Integer defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : ((Integer) value);
+ }
+
+ /**
+ * Returns the value associated with an Integer mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Int value
+ */
+ public Integer getInteger(final String key) {
+ return getInteger(key, null);
+ }
+
+ private static int[] getIntArray(final double[] array) {
+ final int len = array.length;
+ final int[] ret = new int[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = (int) array[i];
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the value associated with an int array mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Int array value
+ */
+ public int[] getIntArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0
+ ? EMPTY_INT_ARRAY
+ : value instanceof double[] ? getIntArray((double[]) value) : (int[]) value;
+ }
+
+ /**
+ * Returns the value associated with an byte array mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Byte array value
+ */
+ public byte[] getByteArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null ? null : Array.getLength(value) == 0 ? EMPTY_BYTE_ARRAY : (byte[]) value;
+ }
+
+ /**
+ * Returns the value associated with an int/double mapping as a long value, or defaultValue if the
+ * mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping does not exist.
+ * @return Long value
+ */
+ public long getLong(final String key, final long defaultValue) {
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : ((Number) value).longValue();
+ }
+
+ /**
+ * Returns the value associated with an int/double mapping as a long value, or 0 if the mapping
+ * does not exist.
+ *
+ * @param key Key to look for.
+ * @return Long value
+ */
+ public long getLong(final String key) {
+ return getLong(key, 0L);
+ }
+
+ private static long[] getLongArray(final Object array) {
+ final int len = Array.getLength(array);
+ final long[] ret = new long[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = ((Number) Array.get(array, i)).longValue();
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the value associated with an int/double array mapping as a long array, or null if the
+ * mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Long array value
+ */
+ public long[] getLongArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0 ? EMPTY_LONG_ARRAY : getLongArray(value);
+ }
+
+ /**
+ * Returns the value associated with a String mapping, or defaultValue if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @param defaultValue Value to return if mapping value is null or mapping does not exist.
+ * @return String value
+ */
+ public String getString(final String key, final String defaultValue) {
+ // If the key maps to null, technically we should return null because the mapping
+ // exists and null is a valid string value. However, people expect the default
+ // value to be returned instead, so we make an exception to return the default value.
+ final Object value = mMap.get(key);
+ return value == null ? defaultValue : (String) value;
+ }
+
+ /**
+ * Returns the value associated with a String mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return String value
+ */
+ public String getString(final String key) {
+ return getString(key, null);
+ }
+
+ // The only case where we convert String[] to/from GeckoBundle[] is if every element
+ // is null.
+ private static int getNullArrayLength(final Object array) {
+ final int len = Array.getLength(array);
+ for (int i = 0; i < len; i++) {
+ if (Array.get(array, i) != null) {
+ throw new ClassCastException("Cannot cast array type");
+ }
+ }
+ return len;
+ }
+
+ /**
+ * Returns the value associated with a String array mapping, or null if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return String array value
+ */
+ public String[] getStringArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0
+ ? EMPTY_STRING_ARRAY
+ : !(value instanceof String[])
+ ? new String[getNullArrayLength(value)]
+ : (String[]) value;
+ }
+
+ /*
+ * Returns the value associated with a RectF mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return RectF value
+ */
+ public RectF getRectF(final String key) {
+ final GeckoBundle rectBundle = getBundle(key);
+ if (rectBundle == null) {
+ return null;
+ }
+
+ return new RectF(
+ (float) rectBundle.getDouble("left"),
+ (float) rectBundle.getDouble("top"),
+ (float) rectBundle.getDouble("right"),
+ (float) rectBundle.getDouble("bottom"));
+ }
+
+ /**
+ * Returns the value associated with a Point mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Point value
+ */
+ public Point getPoint(final String key) {
+ final GeckoBundle ptBundle = getBundle(key);
+ if (ptBundle == null) {
+ return null;
+ }
+
+ return new Point(ptBundle.getInt("x"), ptBundle.getInt("y"));
+ }
+
+ /**
+ * Returns the value associated with a PointF mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return Point value
+ */
+ public PointF getPointF(final String key) {
+ final GeckoBundle ptBundle = getBundle(key);
+ if (ptBundle == null) {
+ return null;
+ }
+
+ return new PointF((float) ptBundle.getDouble("x"), (float) ptBundle.getDouble("y"));
+ }
+
+ /**
+ * Returns the value associated with a GeckoBundle mapping, or null if the mapping does not exist.
+ *
+ * @param key Key to look for.
+ * @return GeckoBundle value
+ */
+ public GeckoBundle getBundle(final String key) {
+ return (GeckoBundle) mMap.get(key);
+ }
+
+ /**
+ * Returns the value associated with a GeckoBundle array mapping, or null if the mapping does not
+ * exist.
+ *
+ * @param key Key to look for.
+ * @return GeckoBundle array value
+ */
+ public GeckoBundle[] getBundleArray(final String key) {
+ final Object value = mMap.get(key);
+ return value == null
+ ? null
+ : Array.getLength(value) == 0
+ ? EMPTY_BUNDLE_ARRAY
+ : !(value instanceof GeckoBundle[])
+ ? new GeckoBundle[getNullArrayLength(value)]
+ : (GeckoBundle[]) value;
+ }
+
+ /**
+ * Returns whether this GeckoBundle has no mappings.
+ *
+ * @return True if no mapping exists.
+ */
+ public boolean isEmpty() {
+ return mMap.isEmpty();
+ }
+
+ /**
+ * Returns an array of all mapped keys.
+ *
+ * @return String array containing all mapped keys.
+ */
+ @WrapForJNI(calledFrom = "gecko")
+ public String[] keys() {
+ final int len = mMap.size();
+ final String[] ret = new String[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = mMap.keyAt(i);
+ }
+ return ret;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private Object[] values() {
+ final int len = mMap.size();
+ final Object[] ret = new Object[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = mMap.valueAt(i);
+ }
+ return ret;
+ }
+
+ private void put(final String key, final Object value) {
+ // We intentionally disallow a generic put() method for type safety and sanity. For
+ // example, we assume elsewhere in the code that a value belongs to a small list of
+ // predefined types, and cannot be any arbitrary object. If you want to put an
+ // Object in the bundle, check the type of the Object first and call the
+ // corresponding put methods. For example,
+ //
+ // if (obj instanceof Integer) {
+ // bundle.putInt(key, (Integer) key);
+ // } else if (obj instanceof String) {
+ // bundle.putString(key, (String) obj);
+ // } else {
+ // throw new IllegalArgumentException("unexpected type");
+ // }
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Map a key to a boolean value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBoolean(final String key, final boolean value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a boolean array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBooleanArray(final String key, final boolean[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a boolean array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBooleanArray(final String key, final Boolean[] value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final boolean[] array = new boolean[value.length];
+ for (int i = 0; i < value.length; i++) {
+ array[i] = value[i];
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a boolean array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBooleanArray(final String key, final Collection<Boolean> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final boolean[] array = new boolean[value.size()];
+ int i = 0;
+ for (final Boolean element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a double value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putDouble(final String key, final double value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putDoubleArray(final String key, final double[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putDoubleArray(final String key, final Double[] value) {
+ putDoubleArray(key, Arrays.asList(value));
+ }
+
+ /**
+ * Map a key to a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putDoubleArray(final String key, final Collection<Double> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final double[] array = new double[value.size()];
+ int i = 0;
+ for (final Double element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to an int value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putInt(final String key, final int value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to an int array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putIntArray(final String key, final int[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a int array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putIntArray(final String key, final Integer[] value) {
+ putIntArray(key, Arrays.asList(value));
+ }
+
+ /**
+ * Map a key to a int array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putIntArray(final String key, final Collection<Integer> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final int[] array = new int[value.size()];
+ int i = 0;
+ for (final Integer element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a long value stored as a double value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putLong(final String key, final long value) {
+ mMap.put(key, (double) value);
+ }
+
+ /**
+ * Map a key to a long array value stored as a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putLongArray(final String key, final long[] value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final double[] array = new double[value.length];
+ for (int i = 0; i < value.length; i++) {
+ array[i] = (double) value[i];
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a long array value stored as a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putLongArray(final String key, final Long[] value) {
+ putLongArray(key, Arrays.asList(value));
+ }
+
+ /**
+ * Map a key to a long array value stored as a double array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putLongArray(final String key, final Collection<Long> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final double[] array = new double[value.size()];
+ int i = 0;
+ for (final Long element : value) {
+ array[i++] = (double) element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a String value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putString(final String key, final String value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a String array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putStringArray(final String key, final String[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a String array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putStringArray(final String key, final Collection<String> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final String[] array = new String[value.size()];
+ int i = 0;
+ for (final String element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Map a key to a GeckoBundle value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBundle(final String key, final GeckoBundle value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a GeckoBundle array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBundleArray(final String key, final GeckoBundle[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Map a key to a GeckoBundle array value.
+ *
+ * @param key Key to map.
+ * @param value Value to map to.
+ */
+ public void putBundleArray(final String key, final Collection<GeckoBundle> value) {
+ if (value == null) {
+ mMap.put(key, null);
+ return;
+ }
+ final GeckoBundle[] array = new GeckoBundle[value.size()];
+ int i = 0;
+ for (final GeckoBundle element : value) {
+ array[i++] = element;
+ }
+ mMap.put(key, array);
+ }
+
+ /**
+ * Remove a mapping.
+ *
+ * @param key Key to remove.
+ */
+ public void remove(final String key) {
+ mMap.remove(key);
+ }
+
+ /**
+ * Returns number of mappings in this GeckoBundle.
+ *
+ * @return Number of mappings.
+ */
+ public int size() {
+ return mMap.size();
+ }
+
+ private static Object normalizeValue(final Object value) {
+ if (value instanceof Integer) {
+ // We treat int and double as the same type.
+ return ((Integer) value).doubleValue();
+
+ } else if (value instanceof int[]) {
+ // We treat int[] and double[] as the same type.
+ final int[] array = (int[]) value;
+ return array.length == 0 ? EMPTY_STRING_ARRAY : getDoubleArray(array);
+
+ } else if (value != null && value.getClass().isArray()) {
+ // We treat arrays of all nulls as the same type, including empty arrays.
+ final int len = Array.getLength(value);
+ for (int i = 0; i < len; i++) {
+ if (Array.get(value, i) != null) {
+ return value;
+ }
+ }
+ return len == 0 ? EMPTY_STRING_ARRAY : new String[len];
+ }
+ return value;
+ }
+
+ @Override // Object
+ public boolean equals(final Object other) {
+ if (!(other instanceof GeckoBundle)) {
+ return false;
+ }
+
+ // Support library's SimpleArrayMap.equals is buggy, so roll our own version.
+ final SimpleArrayMap<String, Object> otherMap = ((GeckoBundle) other).mMap;
+ if (mMap == otherMap) {
+ return true;
+ }
+ if (mMap.size() != otherMap.size()) {
+ return false;
+ }
+
+ for (int i = 0; i < mMap.size(); i++) {
+ final String thisKey = mMap.keyAt(i);
+ final int otherKey = otherMap.indexOfKey(thisKey);
+ if (otherKey < 0) {
+ return false;
+ }
+ final Object thisValue = normalizeValue(mMap.valueAt(i));
+ final Object otherValue = normalizeValue(otherMap.valueAt(otherKey));
+ if (thisValue == otherValue) {
+ continue;
+ } else if (thisValue == null || otherValue == null) {
+ return false;
+ }
+
+ final Class<?> thisClass = thisValue.getClass();
+ final Class<?> otherClass = otherValue.getClass();
+ if (thisClass != otherClass && !thisClass.equals(otherClass)) {
+ return false;
+ } else if (!thisClass.isArray()) {
+ if (!thisValue.equals(otherValue)) {
+ return false;
+ }
+ continue;
+ }
+
+ // Work with both primitive arrays and Object arrays, unlike Arrays.equals().
+ final int thisLen = Array.getLength(thisValue);
+ final int otherLen = Array.getLength(otherValue);
+ if (thisLen != otherLen) {
+ return false;
+ }
+ for (int j = 0; j < thisLen; j++) {
+ final Object thisElem = Array.get(thisValue, j);
+ final Object otherElem = Array.get(otherValue, j);
+ if (thisElem != otherElem
+ && (thisElem == null || otherElem == null || !thisElem.equals(otherElem))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override // Object
+ public int hashCode() {
+ return mMap.hashCode();
+ }
+
+ @Override // Object
+ public String toString() {
+ return mMap.toString();
+ }
+
+ public JSONObject toJSONObject() throws JSONException {
+ final JSONObject out = new JSONObject();
+ for (int i = 0; i < mMap.size(); i++) {
+ final Object value = mMap.valueAt(i);
+ final Object jsonValue;
+
+ if (value instanceof GeckoBundle) {
+ jsonValue = ((GeckoBundle) value).toJSONObject();
+ } else if (value instanceof GeckoBundle[]) {
+ final GeckoBundle[] array = (GeckoBundle[]) value;
+ final JSONArray jsonArray = new JSONArray();
+ for (final GeckoBundle element : array) {
+ jsonArray.put(element == null ? JSONObject.NULL : element.toJSONObject());
+ }
+ jsonValue = jsonArray;
+ } else if (Build.VERSION.SDK_INT >= 19) {
+ // gradle task (testWithGeckoBinariesDebugUnitTest) won't use this since that unit test
+ // runs on build task.
+ final Object wrapped = JSONObject.wrap(value);
+ jsonValue = wrapped != null ? wrapped : value.toString();
+ } else if (value == null) {
+ // This is used by UnitTest only
+ jsonValue = JSONObject.NULL;
+ } else if (value.getClass().isArray()) {
+ // This is used by UnitTest only
+ final JSONArray jsonArray = new JSONArray();
+ for (int j = 0; j < Array.getLength(value); j++) {
+ jsonArray.put(Array.get(value, j));
+ }
+ jsonValue = jsonArray;
+ } else {
+ // This is used by UnitTest only
+ jsonValue = value;
+ }
+ out.put(mMap.keyAt(i), jsonValue);
+ }
+ return out;
+ }
+
+ public Bundle toBundle() {
+ final Bundle out = new Bundle(mMap.size());
+ for (int i = 0; i < mMap.size(); i++) {
+ final String key = mMap.keyAt(i);
+ final Object val = mMap.valueAt(i);
+
+ if (val == null) {
+ out.putString(key, null);
+ } else if (val instanceof GeckoBundle) {
+ out.putBundle(key, ((GeckoBundle) val).toBundle());
+ } else if (val instanceof GeckoBundle[]) {
+ final GeckoBundle[] array = (GeckoBundle[]) val;
+ final Parcelable[] parcelables = new Parcelable[array.length];
+ for (int j = 0; j < array.length; j++) {
+ if (array[j] != null) {
+ parcelables[j] = array[j].toBundle();
+ }
+ }
+ out.putParcelableArray(key, parcelables);
+ } else if (val instanceof Boolean) {
+ out.putBoolean(key, (Boolean) val);
+ } else if (val instanceof boolean[]) {
+ out.putBooleanArray(key, (boolean[]) val);
+ } else if (val instanceof Byte || val instanceof Short || val instanceof Integer) {
+ out.putInt(key, ((Number) val).intValue());
+ } else if (val instanceof int[]) {
+ out.putIntArray(key, (int[]) val);
+ } else if (val instanceof Float || val instanceof Double || val instanceof Long) {
+ out.putDouble(key, ((Number) val).doubleValue());
+ } else if (val instanceof double[]) {
+ out.putDoubleArray(key, (double[]) val);
+ } else if (val instanceof CharSequence || val instanceof Character) {
+ out.putString(key, val.toString());
+ } else if (val instanceof String[]) {
+ out.putStringArray(key, (String[]) val);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ return out;
+ }
+
+ public static GeckoBundle fromBundle(final Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+
+ final String[] keys = new String[bundle.size()];
+ final Object[] values = new Object[bundle.size()];
+ int i = 0;
+
+ for (final String key : bundle.keySet()) {
+ final Object value = bundle.get(key);
+ keys[i] = key;
+
+ if (value instanceof Bundle || value == null) {
+ values[i] = fromBundle((Bundle) value);
+ } else if (value instanceof Parcelable[]) {
+ final Parcelable[] array = (Parcelable[]) value;
+ final GeckoBundle[] out = new GeckoBundle[array.length];
+ for (int j = 0; j < array.length; j++) {
+ out[j] = fromBundle((Bundle) array[j]);
+ }
+ values[i] = out;
+ } else if (value instanceof Boolean
+ || value instanceof Integer
+ || value instanceof Double
+ || value instanceof String
+ || value instanceof boolean[]
+ || value instanceof int[]
+ || value instanceof double[]
+ || value instanceof String[]) {
+ values[i] = value;
+ } else if (value instanceof Byte || value instanceof Short) {
+ values[i] = ((Number) value).intValue();
+ } else if (value instanceof Float || value instanceof Long) {
+ values[i] = ((Number) value).doubleValue();
+ } else if (value instanceof CharSequence || value instanceof Character) {
+ values[i] = value.toString();
+ } else {
+ throw new UnsupportedOperationException();
+ }
+
+ i++;
+ }
+ return new GeckoBundle(keys, values);
+ }
+
+ private static Object fromJSONValue(final Object value) throws JSONException {
+ if (value == null || value == JSONObject.NULL) {
+ return null;
+ } else if (value instanceof JSONObject) {
+ return fromJSONObject((JSONObject) value);
+ }
+ if (value instanceof JSONArray) {
+ final JSONArray array = (JSONArray) value;
+ final int len = array.length();
+ if (len == 0) {
+ return EMPTY_BOOLEAN_ARRAY;
+ }
+ Object out = null;
+ for (int i = 0; i < len; i++) {
+ final Object element = fromJSONValue(array.opt(i));
+ if (element == null) {
+ continue;
+ }
+ if (out == null) {
+ Class<?> type = element.getClass();
+ if (type == Boolean.class) {
+ type = boolean.class;
+ } else if (type == Integer.class) {
+ type = int.class;
+ } else if (type == Double.class) {
+ type = double.class;
+ }
+ out = Array.newInstance(type, len);
+ }
+ Array.set(out, i, element);
+ }
+ if (out == null) {
+ // Treat all-null arrays as String arrays.
+ return new String[len];
+ }
+ return out;
+ }
+ if (value instanceof Boolean
+ || value instanceof Integer
+ || value instanceof Double
+ || value instanceof String) {
+ return value;
+ }
+ if (value instanceof Byte || value instanceof Short) {
+ return ((Number) value).intValue();
+ }
+ if (value instanceof Float || value instanceof Long) {
+ return ((Number) value).doubleValue();
+ }
+ return value.toString();
+ }
+
+ public static GeckoBundle fromJSONObject(final JSONObject obj) throws JSONException {
+ if (obj == null || obj == JSONObject.NULL) {
+ return null;
+ }
+
+ final String[] keys = new String[obj.length()];
+ final Object[] values = new Object[obj.length()];
+
+ final Iterator<String> iter = obj.keys();
+ for (int i = 0; iter.hasNext(); i++) {
+ final String key = iter.next();
+ keys[i] = key;
+ values[i] = fromJSONValue(obj.opt(key));
+ }
+ return new GeckoBundle(keys, values);
+ }
+
+ @Override // Parcelable
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override // Parcelable
+ public void writeToParcel(final Parcel dest, final int flags) {
+ final int len = mMap.size();
+ dest.writeInt(len);
+
+ for (int i = 0; i < len; i++) {
+ dest.writeString(mMap.keyAt(i));
+ dest.writeValue(mMap.valueAt(i));
+ }
+ }
+
+ // AIDL code may call readFromParcel even though it's not part of Parcelable.
+ public void readFromParcel(final Parcel source) {
+ final ClassLoader loader = getClass().getClassLoader();
+ final int len = source.readInt();
+ mMap.clear();
+ mMap.ensureCapacity(len);
+
+ for (int i = 0; i < len; i++) {
+ final String key = source.readString();
+ Object val = source.readValue(loader);
+
+ if (val instanceof Parcelable[]) {
+ final Parcelable[] array = (Parcelable[]) val;
+ val = Arrays.copyOf(array, array.length, GeckoBundle[].class);
+ }
+
+ mMap.put(key, val);
+ }
+ }
+
+ public static final Parcelable.Creator<GeckoBundle> CREATOR =
+ new Parcelable.Creator<GeckoBundle>() {
+ @Override
+ public GeckoBundle createFromParcel(final Parcel source) {
+ final GeckoBundle bundle = new GeckoBundle(0);
+ bundle.readFromParcel(source);
+ return bundle;
+ }
+
+ @Override
+ public GeckoBundle[] newArray(final int size) {
+ return new GeckoBundle[size];
+ }
+ };
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
new file mode 100644
index 0000000000..ccfce796bd
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareCodecCapabilityUtils.java
@@ -0,0 +1,389 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.annotation.SuppressLint;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.os.Build;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import org.mozilla.gecko.annotation.WrapForJNI;
+
+public final class HardwareCodecCapabilityUtils {
+ private static final String LOGTAG = "HardwareCodecCapability";
+
+ // List of supported HW VP8 encoders.
+ private static final String[] supportedVp8HwEncCodecPrefixes = {"OMX.qcom.", "OMX.Intel."};
+ // List of supported HW VP8 decoders.
+ private static final String[] supportedVp8HwDecCodecPrefixes = {
+ "OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "c2.exynos", "OMX.Intel."
+ };
+ private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8";
+ // List of supported HW VP9 codecs.
+ private static final String[] supportedVp9HwCodecPrefixes = {
+ "OMX.qcom.", "OMX.Exynos.", "c2.exynos"
+ };
+ private static final String VP9_MIME_TYPE = "video/x-vnd.on2.vp9";
+ // List of supported HW H.264 codecs.
+ private static final String[] supportedH264HwCodecPrefixes = {
+ "OMX.qcom.",
+ "OMX.Intel.",
+ "OMX.Exynos.",
+ "c2.exynos",
+ "OMX.Nvidia",
+ "OMX.SEC.",
+ "OMX.IMG.",
+ "OMX.k3.",
+ "OMX.hisi.",
+ "OMX.TI.",
+ "OMX.MTK."
+ };
+ private static final String H264_MIME_TYPE = "video/avc";
+ // NV12 color format supported by QCOM codec, but not declared in MediaCodec -
+ // see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h
+ private static final int COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04;
+ // Allowable color formats supported by codec - in order of preference.
+ private static final int[] supportedColorList = {
+ CodecCapabilities.COLOR_FormatYUV420Planar,
+ CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
+ CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
+ COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m
+ };
+ private static final int COLOR_FORMAT_NOT_SUPPORTED = -1;
+ private static final String[] adaptivePlaybackBlacklist = {
+ "GT-I9300", // S3 (I9300 / I9300I)
+ "SCH-I535", // S3
+ "SGH-T999", // S3 (T-Mobile)
+ "SAMSUNG-SGH-T999", // S3 (T-Mobile)
+ "SGH-M919", // S4
+ "GT-I9505", // S4
+ "GT-I9515", // S4
+ "SCH-R970", // S4
+ "SGH-I337", // S4
+ "SPH-L720", // S4 (Sprint)
+ "SAMSUNG-SGH-I337", // S4
+ "GT-I9195", // S4 Mini
+ "300E5EV/300E4EV/270E5EV/270E4EV/2470EV/2470EE",
+ "LG-D605" // LG Optimus L9 II
+ };
+
+ private static MediaCodecInfo[] getCodecListWithOldAPI() {
+ int numCodecs = 0;
+ try {
+ numCodecs = MediaCodecList.getCodecCount();
+ } catch (final RuntimeException e) {
+ Log.e(LOGTAG, "Failed to retrieve media codec count", e);
+ return new MediaCodecInfo[numCodecs];
+ }
+
+ final MediaCodecInfo[] codecList = new MediaCodecInfo[numCodecs];
+
+ for (int i = 0; i < numCodecs; ++i) {
+ final MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+ codecList[i] = info;
+ }
+
+ return codecList;
+ }
+
+ // Return list of all codecs (decode + encode).
+ private static MediaCodecInfo[] getCodecList() {
+ final MediaCodecInfo[] codecList;
+ try {
+ final MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+ codecList = list.getCodecInfos();
+ } catch (final RuntimeException e) {
+ Log.e(LOGTAG, "Failed to retrieve media codec support list", e);
+ return new MediaCodecInfo[0];
+ }
+ return codecList;
+ }
+
+ // Return list of all decoders.
+ private static MediaCodecInfo[] getDecoderInfos() {
+ final ArrayList<MediaCodecInfo> decoderList = new ArrayList<MediaCodecInfo>();
+ for (final MediaCodecInfo info : getCodecList()) {
+ if (!info.isEncoder()) {
+ decoderList.add(info);
+ }
+ }
+ return decoderList.toArray(new MediaCodecInfo[0]);
+ }
+
+ // Return list of all encoders.
+ private static MediaCodecInfo[] getEncoderInfos() {
+ final ArrayList<MediaCodecInfo> encoderList = new ArrayList<MediaCodecInfo>();
+ for (final MediaCodecInfo info : getCodecList()) {
+ if (info.isEncoder()) {
+ encoderList.add(info);
+ }
+ }
+ return encoderList.toArray(new MediaCodecInfo[0]);
+ }
+
+ // Return list of all decoder-supported MIME types without distinguishing
+ // between SW/HW support.
+ @WrapForJNI
+ public static String[] getDecoderSupportedMimeTypes() {
+ final Set<String> mimeTypes = new HashSet<>();
+ for (final MediaCodecInfo info : getDecoderInfos()) {
+ mimeTypes.addAll(Arrays.asList(info.getSupportedTypes()));
+ }
+ return mimeTypes.toArray(new String[0]);
+ }
+
+ // Return list of all decoder-supported MIME types, each prefixed with
+ // either SW or HW indicating software or hardware support.
+ @WrapForJNI
+ public static String[] getDecoderSupportedMimeTypesWithAccelInfo() {
+ final Set<String> mimeTypes = new HashSet<>();
+ final String[] hwPrefixes = getAllSupportedHWCodecPrefixes(false);
+
+ for (final MediaCodecInfo info : getDecoderInfos()) {
+ final String[] supportedTypes = info.getSupportedTypes();
+ for (final String mimeType : info.getSupportedTypes()) {
+ boolean isHwPrefix = false;
+ for (final String prefix : hwPrefixes) {
+ if (info.getName().startsWith(prefix)) {
+ isHwPrefix = true;
+ break;
+ }
+ }
+ if (!isHwPrefix) {
+ mimeTypes.add("SW " + mimeType);
+ continue;
+ }
+ final CodecCapabilities caps = info.getCapabilitiesForType(mimeType);
+ if (getSupportsYUV420orNV12(caps) != COLOR_FORMAT_NOT_SUPPORTED) {
+ mimeTypes.add("HW " + mimeType);
+ }
+ }
+ }
+ for (final String typeit : mimeTypes) {
+ Log.d(LOGTAG, "MIME support: " + typeit);
+ }
+ return mimeTypes.toArray(new String[0]);
+ }
+
+ public static boolean checkSupportsAdaptivePlayback(
+ final MediaCodec aCodec, final String aMimeType) {
+ if (isAdaptivePlaybackBlacklisted(aMimeType)) {
+ return false;
+ }
+
+ try {
+ final MediaCodecInfo info = aCodec.getCodecInfo();
+ final MediaCodecInfo.CodecCapabilities capabilities = info.getCapabilitiesForType(aMimeType);
+ return capabilities != null
+ && capabilities.isFeatureSupported(
+ MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback);
+ } catch (final IllegalArgumentException e) {
+ Log.e(LOGTAG, "Retrieve codec information failed", e);
+ }
+ return false;
+ }
+
+ // See Bug1360626 and
+ // https://codereview.chromium.org/1869103002 for details.
+ private static boolean isAdaptivePlaybackBlacklisted(final String aMimeType) {
+ Log.d(LOGTAG, "The device ModelID is " + Build.MODEL);
+ if (!aMimeType.equals("video/avc") && !aMimeType.equals("video/avc1")) {
+ return false;
+ }
+
+ if (!Build.MANUFACTURER.toLowerCase(Locale.ROOT).equals("samsung")) {
+ return false;
+ }
+
+ for (final String model : adaptivePlaybackBlacklist) {
+ if (Build.MODEL.startsWith(model)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Check if a given MIME Type has HW decode or encode support.
+ public static boolean getHWCodecCapability(final String aMimeType, final boolean aIsEncoder) {
+ for (final MediaCodecInfo info : getCodecList()) {
+ if (info.isEncoder() != aIsEncoder) {
+ continue;
+ }
+ String name = null;
+ for (final String mimeType : info.getSupportedTypes()) {
+ if (mimeType.equals(aMimeType)) {
+ name = info.getName();
+ break;
+ }
+ }
+ if (name == null) {
+ continue; // No HW support in this codec; try the next one.
+ }
+ Log.d(LOGTAG, "Found candidate" + (aIsEncoder ? " encoder " : " decoder ") + name);
+
+ // Check if this is supported codec.
+ final String[] hwList = getSupportedHWCodecPrefixes(aMimeType, aIsEncoder);
+ if (hwList == null) {
+ continue;
+ }
+ boolean supportedCodec = false;
+ for (final String codecPrefix : hwList) {
+ if (name.startsWith(codecPrefix)) {
+ supportedCodec = true;
+ break;
+ }
+ }
+ if (!supportedCodec) {
+ continue;
+ }
+
+ // Check if codec supports either yuv420 or nv12.
+ final CodecCapabilities capabilities = info.getCapabilitiesForType(aMimeType);
+ for (final int colorFormat : capabilities.colorFormats) {
+ Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
+ }
+ if (Build.VERSION.SDK_INT >= 24) {
+ for (final MediaCodecInfo.CodecProfileLevel pl : capabilities.profileLevels) {
+ Log.v(
+ LOGTAG,
+ " Profile: 0x"
+ + Integer.toHexString(pl.profile)
+ + "/Level=0x"
+ + Integer.toHexString(pl.level));
+ }
+ }
+ final int codecColorFormat = getSupportsYUV420orNV12(capabilities);
+ if (codecColorFormat != COLOR_FORMAT_NOT_SUPPORTED) {
+ Log.d(
+ LOGTAG,
+ "Found target"
+ + (aIsEncoder ? " encoder " : " decoder ")
+ + name
+ + ". Color: 0x"
+ + Integer.toHexString(codecColorFormat));
+ return true;
+ }
+ }
+ // No HW codec.
+ return false;
+ }
+
+ // Check if codec supports YUV420 or NV12
+ private static int getSupportsYUV420orNV12(final CodecCapabilities aCodecCaps) {
+ for (final int supportedColorFormat : supportedColorList) {
+ for (final int codecColorFormat : aCodecCaps.colorFormats) {
+ if (codecColorFormat == supportedColorFormat) {
+ return codecColorFormat;
+ }
+ }
+ }
+ return COLOR_FORMAT_NOT_SUPPORTED;
+ }
+
+ // Check if MIME type string has HW prefix (encode or decode, VP8, VP9, and H264)
+ private static String[] getSupportedHWCodecPrefixes(
+ final String aMimeType, final boolean aIsEncoder) {
+ if (aMimeType.equals(H264_MIME_TYPE)) {
+ return supportedH264HwCodecPrefixes;
+ }
+ if (aMimeType.equals(VP9_MIME_TYPE)) {
+ return supportedVp9HwCodecPrefixes;
+ }
+ if (aMimeType.equals(VP8_MIME_TYPE)) {
+ return aIsEncoder ? supportedVp8HwEncCodecPrefixes : supportedVp8HwDecCodecPrefixes;
+ }
+ return null;
+ }
+
+ // Return list of HW codec prefixes (encode or decode, VP8, VP9, and H264)
+ private static String[] getAllSupportedHWCodecPrefixes(final boolean aIsEncoder) {
+ final Set<String> prefixes = new HashSet<>();
+ final String[] mimeTypes = {H264_MIME_TYPE, VP8_MIME_TYPE, VP9_MIME_TYPE};
+ for (final String mt : mimeTypes) {
+ prefixes.addAll(Arrays.asList(getSupportedHWCodecPrefixes(mt, aIsEncoder)));
+ }
+ return prefixes.toArray(new String[0]);
+ }
+
+ @WrapForJNI
+ public static boolean hasHWVP8(final boolean aIsEncoder) {
+ return getHWCodecCapability(VP8_MIME_TYPE, aIsEncoder);
+ }
+
+ @WrapForJNI
+ public static boolean hasHWVP9(final boolean aIsEncoder) {
+ return getHWCodecCapability(VP9_MIME_TYPE, aIsEncoder);
+ }
+
+ @WrapForJNI
+ public static boolean hasHWH264(final boolean aIsEncoder) {
+ return getHWCodecCapability(H264_MIME_TYPE, aIsEncoder);
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ public static boolean hasHWH264() {
+ return getHWCodecCapability(H264_MIME_TYPE, true)
+ && getHWCodecCapability(H264_MIME_TYPE, false);
+ }
+
+ @WrapForJNI
+ @SuppressLint("NewApi")
+ public static boolean decodes10Bit(final String aMimeType) {
+ if (Build.VERSION.SDK_INT < 24) {
+ // Be conservative when we cannot get supported profile.
+ return false;
+ }
+
+ final MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+ for (final MediaCodecInfo info : codecs.getCodecInfos()) {
+ if (info.isEncoder()) {
+ continue;
+ }
+ try {
+ for (final MediaCodecInfo.CodecProfileLevel pl :
+ info.getCapabilitiesForType(aMimeType).profileLevels) {
+ if ((aMimeType.equals(H264_MIME_TYPE)
+ && pl.profile == MediaCodecInfo.CodecProfileLevel.AVCProfileHigh10)
+ || (aMimeType.equals(VP9_MIME_TYPE) && is10BitVP9Profile(pl.profile))) {
+ return true;
+ }
+ }
+ } catch (final IllegalArgumentException e) {
+ // Type not supported.
+ continue;
+ }
+ }
+
+ return false;
+ }
+
+ @SuppressLint("NewApi")
+ private static boolean is10BitVP9Profile(final int profile) {
+ if (Build.VERSION.SDK_INT < 24) {
+ // Be conservative when we cannot get supported profile.
+ return false;
+ }
+
+ if ((profile == MediaCodecInfo.CodecProfileLevel.VP9Profile2)
+ || (profile == MediaCodecInfo.CodecProfileLevel.VP9Profile3)
+ || (profile == MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR)
+ || (profile == MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR)) {
+ return true;
+ }
+
+ return Build.VERSION.SDK_INT >= 29
+ && ((profile == MediaCodecInfo.CodecProfileLevel.VP9Profile2HDR10Plus)
+ || (profile == MediaCodecInfo.CodecProfileLevel.VP9Profile3HDR10Plus));
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
new file mode 100644
index 0000000000..bab64b92d4
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/HardwareUtils.java
@@ -0,0 +1,46 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+
+public final class HardwareUtils {
+ private static final String LOGTAG = "GeckoHardwareUtils";
+
+ private static volatile boolean sInited;
+
+ // These are all set once, during init.
+ private static volatile boolean sIsLargeTablet;
+ private static volatile boolean sIsSmallTablet;
+
+ private HardwareUtils() {}
+
+ public static synchronized void init(final Context context) {
+ if (sInited) {
+ return;
+ }
+
+ // Pre-populate common flags from the context.
+ final int screenLayoutSize =
+ context.getResources().getConfiguration().screenLayout
+ & Configuration.SCREENLAYOUT_SIZE_MASK;
+ if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
+ sIsLargeTablet = true;
+ } else if (screenLayoutSize == Configuration.SCREENLAYOUT_SIZE_LARGE) {
+ sIsSmallTablet = true;
+ }
+
+ sInited = true;
+ }
+
+ public static boolean isTablet(final Context context) {
+ if (!sInited) {
+ init(context);
+ }
+ return sIsLargeTablet || sIsSmallTablet;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IXPCOMEventTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IXPCOMEventTarget.java
new file mode 100644
index 0000000000..9f42d9bd85
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IXPCOMEventTarget.java
@@ -0,0 +1,12 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import java.util.concurrent.Executor;
+
+public interface IXPCOMEventTarget extends Executor {
+ boolean isOnCurrentThread();
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageDecoder.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageDecoder.java
new file mode 100644
index 0000000000..4ab330f182
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageDecoder.java
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.graphics.Bitmap;
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.geckoview.GeckoResult;
+
+/** Provides access to Gecko's Image processing library. */
+@AnyThread
+public class ImageDecoder {
+ private static ImageDecoder instance;
+
+ private ImageDecoder() {}
+
+ public static ImageDecoder instance() {
+ if (instance == null) {
+ instance = new ImageDecoder();
+ }
+
+ return instance;
+ }
+
+ @WrapForJNI(dispatchTo = "gecko", stubName = "Decode")
+ private static native void nativeDecode(
+ final String uri, final int desiredLength, GeckoResult<Bitmap> result);
+
+ /**
+ * Fetches and decodes an image at the specified location. This method supports SVG, PNG, Bitmap
+ * and other formats supported by Gecko.
+ *
+ * @param uri location of the image. Can be either a remote https:// location, file:/// if the
+ * file is local or a resource://android/ if the file is located inside the APK.
+ * <p>e.g. if the image file is locate at /assets/test.png inside the apk, set the uri to
+ * resource://android/assets/test.png.
+ * @return A {@link GeckoResult} to the decoded image.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> decode(final @NonNull String uri) {
+ return decode(uri, 0);
+ }
+
+ /**
+ * Fetches and decodes an image at the specified location and resizes it to the desired length.
+ * This method supports SVG, PNG, Bitmap and other formats supported by Gecko.
+ *
+ * <p>Note: The final size might differ slightly from the requested output.
+ *
+ * @param uri location of the image. Can be either a remote https:// location, file:/// if the
+ * file is local or a resource://android/ if the file is located inside the APK.
+ * <p>e.g. if the image file is locate at /assets/test.png inside the apk, set the uri to
+ * resource://android/assets/test.png.
+ * @param desiredLength Longest size for the image in device pixel units. The resulting image
+ * might be slightly different if the image cannot be resized efficiently. If desiredLength is
+ * 0 then the image will be decoded to its natural size.
+ * @return A {@link GeckoResult} to the decoded image.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> decode(final @NonNull String uri, final int desiredLength) {
+ if (uri == null) {
+ throw new IllegalArgumentException("Uri cannot be null");
+ }
+
+ final GeckoResult<Bitmap> result = new GeckoResult<>();
+
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
+ nativeDecode(uri, desiredLength, result);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.PROFILE_READY,
+ this,
+ "nativeDecode",
+ String.class,
+ uri,
+ int.class,
+ desiredLength,
+ GeckoResult.class,
+ result);
+ }
+
+ return result;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java
new file mode 100644
index 0000000000..d155ea951e
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ImageResource.java
@@ -0,0 +1,334 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import org.mozilla.geckoview.GeckoResult;
+
+/**
+ * Represents an Web API image resource as used in web app manifests and media session metadata.
+ *
+ * @see <a href="https://www.w3.org/TR/image-resource">Image Resource</a>
+ */
+@AnyThread
+public class ImageResource {
+ private static final String LOGTAG = "ImageResource";
+ private static final boolean DEBUG = false;
+
+ /** Represents the size of an image resource option. */
+ public static class Size {
+ /** The width in pixels. */
+ public final int width;
+
+ /** The height in pixels. */
+ public final int height;
+
+ /**
+ * Size contructor.
+ *
+ * @param width The width in pixels.
+ * @param height The height in pixels.
+ */
+ public Size(final int width, final int height) {
+ this.width = width;
+ this.height = height;
+ }
+ }
+
+ /** The URI of the image resource. */
+ public final @NonNull String src;
+
+ /** The MIME type of the image resource. */
+ public final @Nullable String type;
+
+ /** A {@link Size} array of supported images sizes. */
+ public final @Nullable Size[] sizes;
+
+ /**
+ * ImageResource constructor.
+ *
+ * @param src The URI string of the image resource.
+ * @param type The MIME type of the image resource.
+ * @param sizes The supported images {@link Size} array.
+ */
+ public ImageResource(
+ final @NonNull String src, final @Nullable String type, final @Nullable Size[] sizes) {
+ this.src = src;
+ this.type = type != null ? type.toLowerCase(Locale.ROOT) : null;
+ this.sizes = sizes;
+ }
+
+ /**
+ * ImageResource constructor.
+ *
+ * @param src The URI string of the image resource.
+ * @param type The MIME type of the image resource.
+ * @param sizes The supported images sizes string.
+ * @see <a href="https://html.spec.whatwg.org/multipage/semantics.html#dom-link-sizes">Attribute
+ * spec for sizes</a>
+ */
+ public ImageResource(
+ final @NonNull String src, final @Nullable String type, final @Nullable String sizes) {
+ this(src, type, parseSizes(sizes));
+ }
+
+ private static @Nullable Size[] parseSizes(final @Nullable String sizesStr) {
+ if (sizesStr == null || sizesStr.isEmpty()) {
+ return null;
+ }
+
+ final String[] sizesStrs = sizesStr.toLowerCase(Locale.ROOT).split(" ");
+ final List<Size> sizes = new ArrayList<Size>();
+
+ for (final String sizeStr : sizesStrs) {
+ if (sizesStr.equals("any")) {
+ // 0-width size will always be favored.
+ sizes.add(new Size(0, 0));
+ continue;
+ }
+ final String[] widthHeight = sizeStr.split("x");
+ if (widthHeight.length != 2) {
+ // Not spec-compliant size.
+ continue;
+ }
+ try {
+ sizes.add(new Size(Integer.valueOf(widthHeight[0]), Integer.valueOf(widthHeight[1])));
+ } catch (final NumberFormatException e) {
+ Log.e(LOGTAG, "Invalid image resource size", e);
+ }
+ }
+ if (sizes.isEmpty()) {
+ return null;
+ }
+ return sizes.toArray(new Size[0]);
+ }
+
+ public static @NonNull ImageResource fromBundle(final GeckoBundle bundle) {
+ return new ImageResource(
+ bundle.getString("src"), bundle.getString("type"), bundle.getString("sizes"));
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("ImageResource {");
+ builder
+ .append("src=")
+ .append(src)
+ .append("type=")
+ .append(type)
+ .append("sizes=")
+ .append(sizes)
+ .append("}");
+ return builder.toString();
+ }
+
+ /**
+ * Get the best version of this image for size <code>size</code>. Embedders are encouraged to
+ * cache the result of this method keyed with this instance.
+ *
+ * @param size pixel size at which this image will be displayed at.
+ * @return A {@link GeckoResult} that resolves to the bitmap when ready.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> getBitmap(final int size) {
+ return ImageDecoder.instance().decode(src, size);
+ }
+
+ /**
+ * Represents a collection of {@link ImageResource} options. Image resources are often used in a
+ * collection to provide multiple image options for various sizes. This data structure can be used
+ * to retrieve the best image resource for any given target image size.
+ */
+ public static class Collection {
+ private static class SizeIndexPair {
+ public final int width;
+ public final int idx;
+
+ public SizeIndexPair(final int width, final int idx) {
+ this.width = width;
+ this.idx = idx;
+ }
+ }
+
+ // The individual image resources, usually each with a unique src.
+ private final List<ImageResource> mImages;
+
+ // A sorted size-index list. The list is sorted based on the supported
+ // sizes of the images in ascending order.
+ private final List<SizeIndexPair> mSizeIndex;
+
+ /* package */ Collection() {
+ mImages = new ArrayList<>();
+ mSizeIndex = new ArrayList<>();
+ }
+
+ /** Builder class for the construction of a {@link Collection}. */
+ public static class Builder {
+ final Collection mCollection;
+
+ public Builder() {
+ mCollection = new Collection();
+ }
+
+ /**
+ * Add an image resource to the collection.
+ *
+ * @param image The {@link ImageResource} to be added.
+ * @return This builder instance.
+ */
+ public @NonNull Builder add(final ImageResource image) {
+ final int index = mCollection.mImages.size();
+
+ if (image.sizes == null) {
+ // Null-sizes are handled the same as `any`.
+ mCollection.mSizeIndex.add(new SizeIndexPair(0, index));
+ } else {
+ for (final Size size : image.sizes) {
+ mCollection.mSizeIndex.add(new SizeIndexPair(size.width, index));
+ }
+ }
+ mCollection.mImages.add(image);
+ return this;
+ }
+
+ /**
+ * Finalize the collection.
+ *
+ * @return The final collection.
+ */
+ public @NonNull Collection build() {
+ Collections.sort(mCollection.mSizeIndex, (a, b) -> Integer.compare(a.width, b.width));
+ return mCollection;
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("ImageResource.Collection {");
+ builder.append("images=[");
+
+ for (final ImageResource image : mImages) {
+ builder.append(image).append(", ");
+ }
+ builder.append("]}");
+ return builder.toString();
+ }
+
+ /**
+ * Returns the best suited {@link ImageResource} for the given size. This is usually determined
+ * based on the minimal difference between the given size and one of the supported widths of an
+ * image resource.
+ *
+ * @param size The target size for the image in pixels.
+ * @return The best {@link ImageResource} for the given size from this collection.
+ */
+ public @Nullable ImageResource getBest(final int size) {
+ if (mSizeIndex.isEmpty()) {
+ return null;
+ }
+ int bestMatchIdx = mSizeIndex.get(0).idx;
+ int lastDiff = size;
+ for (final SizeIndexPair sizeIndex : mSizeIndex) {
+ final int diff = Math.abs(sizeIndex.width - size);
+ if (lastDiff <= diff) {
+ // With increasing widths, the difference can only grow now.
+ // 0-width means "any", so we're finished at the first
+ // entry.
+ break;
+ }
+ lastDiff = diff;
+ bestMatchIdx = sizeIndex.idx;
+ }
+ return mImages.get(bestMatchIdx);
+ }
+
+ /**
+ * Get the best version of this image for size <code>size</code>. Embedders are encouraged to
+ * cache the result of this method keyed with this instance.
+ *
+ * @param size pixel size at which this image will be displayed at.
+ * @return A {@link GeckoResult} that resolves to the bitmap when ready.
+ */
+ @NonNull
+ public GeckoResult<Bitmap> getBitmap(final int size) {
+ final ImageResource image = getBest(size);
+ if (image == null) {
+ return GeckoResult.fromValue(null);
+ }
+ return image.getBitmap(size);
+ }
+
+ public static Collection fromSizeSrcBundle(final GeckoBundle bundle) {
+ final Builder builder = new Builder();
+
+ for (final String key : bundle.keys()) {
+ final Integer intKey = Integer.valueOf(key);
+ if (intKey == null) {
+ Log.e(LOGTAG, "Non-integer image key: " + intKey);
+
+ if (DEBUG) {
+ throw new RuntimeException("Non-integer image key: " + key);
+ }
+ continue;
+ }
+
+ final String src = getImageValue(bundle.get(key));
+ if (src != null) {
+ // Given the bundle structure, we don't have insight on
+ // individual image resources so we have to create an
+ // instance for each size entry.
+ final ImageResource image =
+ new ImageResource(src, null, new Size[] {new Size(intKey, intKey)});
+ builder.add(image);
+ }
+ }
+ return builder.build();
+ }
+
+ private static String getImageValue(final Object value) {
+ // The image value can either be an object containing images for
+ // each theme...
+ if (value instanceof GeckoBundle) {
+ // We don't support theme_images yet, so let's just return the
+ // default value.
+ final GeckoBundle themeImages = (GeckoBundle) value;
+ final Object defaultImages = themeImages.get("default");
+
+ if (!(defaultImages instanceof String)) {
+ if (DEBUG) {
+ throw new RuntimeException("Unexpected themed_icon value.");
+ }
+ Log.e(LOGTAG, "Unexpected themed_icon value.");
+ return null;
+ }
+
+ return (String) defaultImages;
+ }
+
+ // ... or just a URL.
+ if (value instanceof String) {
+ return (String) value;
+ }
+
+ // We never expect it to be something else, so let's error out here.
+ if (DEBUG) {
+ throw new RuntimeException("Unexpected image value: " + value);
+ }
+
+ Log.e(LOGTAG, "Unexpected image value.");
+ return null;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputDeviceUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputDeviceUtils.java
new file mode 100644
index 0000000000..e0a0d924a9
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/InputDeviceUtils.java
@@ -0,0 +1,20 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.view.InputDevice;
+
+public class InputDeviceUtils {
+ public static boolean isPointerTypeDevice(final InputDevice inputDevice) {
+ final int sources = inputDevice.getSources();
+ return (sources
+ & (InputDevice.SOURCE_CLASS_JOYSTICK
+ | InputDevice.SOURCE_CLASS_POINTER
+ | InputDevice.SOURCE_CLASS_POSITION
+ | InputDevice.SOURCE_CLASS_TRACKBALL))
+ != 0;
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
new file mode 100644
index 0000000000..36fde18a02
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/IntentUtils.java
@@ -0,0 +1,116 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.util;
+
+import android.content.Intent;
+import android.net.Uri;
+import java.net.URISyntaxException;
+import java.util.Locale;
+
+/** Utilities for Intents. */
+public class IntentUtils {
+ private IntentUtils() {}
+
+ /**
+ * Return a Uri instance which is equivalent to uri, but with a guaranteed-lowercase scheme as if
+ * the API level 16 method Uri.normalizeScheme had been called.
+ *
+ * @param uri The URI string to normalize.
+ * @return The corresponding normalized Uri.
+ */
+ private static Uri normalizeUriScheme(final Uri uri) {
+ final String scheme = uri.getScheme();
+ if (scheme == null) {
+ return uri;
+ }
+ final String lower = scheme.toLowerCase(Locale.ROOT);
+ if (lower.equals(scheme)) {
+ return uri;
+ }
+
+ // Otherwise, return a new URI with a normalized scheme.
+ return uri.buildUpon().scheme(lower).build();
+ }
+
+ /**
+ * Return a normalized Uri instance that corresponds to the given URI string with cross-API-level
+ * compatibility.
+ *
+ * @param aUri The URI string to normalize.
+ * @return The corresponding normalized Uri.
+ */
+ public static Uri normalizeUri(final String aUri) {
+ return normalizeUriScheme(
+ aUri.indexOf(':') >= 0 ? Uri.parse(aUri) : new Uri.Builder().scheme(aUri).build());
+ }
+
+ public static boolean isUriSafeForScheme(final String aUri) {
+ return isUriSafeForScheme(normalizeUri(aUri));
+ }
+
+ /**
+ * Verify whether the given URI is considered safe to load in respect to its scheme. Unsafe URIs
+ * should be blocked from further handling.
+ *
+ * @param aUri The URI instance to test.
+ * @return Whether the provided URI is considered safe in respect to its scheme.
+ */
+ public static boolean isUriSafeForScheme(final Uri aUri) {
+ final String scheme = aUri.getScheme();
+ if ("tel".equals(scheme) || "sms".equals(scheme)) {
+ // Bug 794034 - We don't want to pass MWI or USSD codes to the
+ // dialer, and ensure the Uri class doesn't parse a URI
+ // containing a fragment ('#')
+ final String number = aUri.getSchemeSpecificPart();
+ if (number.contains("#") || number.contains("*") || aUri.getFragment() != null) {
+ return false;
+ }
+ }
+
+ if (("intent".equals(scheme) || "android-app".equals(scheme))) {
+ // Bug 1356893 - Rject intents with file data schemes.
+ return getSafeIntent(aUri) != null;
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a safe intent for the given URI. Intents with file data schemes are considered unsafe.
+ *
+ * @param aUri The URI for the intent.
+ * @return A safe intent for the given URI or null if URI is considered unsafe.
+ */
+ public static Intent getSafeIntent(final Uri aUri) {
+ final Intent intent;
+ try {
+ intent = Intent.parseUri(aUri.toString(), 0);
+ } catch (final URISyntaxException e) {
+ return null;
+ }
+
+ final Uri data = intent.getData();
+ if (data != null && "file".equals(normalizeUriScheme(data).getScheme())) {
+ return null;
+ }
+
+ // Only open applications which can accept arbitrary data from a browser.
+ intent.addCategory(Intent.CATEGORY_BROWSABLE);
+
+ // Prevent site from explicitly opening our internal activities,
+ // which can leak data.
+ intent.setComponent(null);
+ nullIntentSelector(intent);
+
+ return intent;
+ }
+
+ // We create a separate method to better encapsulate the @TargetApi use.
+ private static void nullIntentSelector(final Intent intent) {
+ intent.setSelector(null);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java
new file mode 100644
index 0000000000..b8f15c04e3
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/NetworkUtils.java
@@ -0,0 +1,168 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.telephony.TelephonyManager;
+
+public class NetworkUtils {
+ /*
+ * Keep the below constants in sync with
+ * http://searchfox.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum ConnectionSubType {
+ CELL_2G("2g"),
+ CELL_3G("3g"),
+ CELL_4G("4g"),
+ ETHERNET("ethernet"),
+ WIFI("wifi"),
+ WIMAX("wimax"),
+ UNKNOWN("unknown");
+
+ public final String value;
+
+ ConnectionSubType(final String value) {
+ this.value = value;
+ }
+ }
+
+ /*
+ * Keep the below constants in sync with
+ * http://searchfox.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl
+ */
+ public enum NetworkStatus {
+ UP("up"),
+ DOWN("down"),
+ UNKNOWN("unknown");
+
+ public final String value;
+
+ NetworkStatus(final String value) {
+ this.value = value;
+ }
+ }
+
+ // Connection Type defined in Network Information API v3.
+ // See Bug 1270401 - current W3C Spec (Editor's Draft) is different, it also contains wimax,
+ // mixed, unknown.
+ // W3C spec: http://w3c.github.io/netinfo/#the-connectiontype-enum
+ public enum ConnectionType {
+ CELLULAR(0),
+ BLUETOOTH(1),
+ ETHERNET(2),
+ WIFI(3),
+ OTHER(4),
+ NONE(5);
+
+ public final int value;
+
+ ConnectionType(final int value) {
+ this.value = value;
+ }
+ }
+
+ public static boolean isConnected(final ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return false;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnected();
+ }
+
+ /** For mobile connections, maps particular connection subtype to a general 2G, 3G, 4G bucket. */
+ public static ConnectionSubType getConnectionSubType(
+ final ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+
+ if (networkInfo == null) {
+ return ConnectionSubType.UNKNOWN;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionSubType.ETHERNET;
+ case ConnectivityManager.TYPE_MOBILE:
+ return getGenericMobileSubtype(networkInfo.getSubtype());
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionSubType.WIMAX;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionSubType.WIFI;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+
+ public static ConnectionType getConnectionType(final ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return ConnectionType.NONE;
+ }
+
+ final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
+ if (networkInfo == null) {
+ return ConnectionType.NONE;
+ }
+
+ switch (networkInfo.getType()) {
+ case ConnectivityManager.TYPE_BLUETOOTH:
+ return ConnectionType.BLUETOOTH;
+ case ConnectivityManager.TYPE_ETHERNET:
+ return ConnectionType.ETHERNET;
+ // Fallthrough, MOBILE and WIMAX both map to CELLULAR.
+ case ConnectivityManager.TYPE_MOBILE:
+ case ConnectivityManager.TYPE_WIMAX:
+ return ConnectionType.CELLULAR;
+ case ConnectivityManager.TYPE_WIFI:
+ return ConnectionType.WIFI;
+ default:
+ return ConnectionType.OTHER;
+ }
+ }
+
+ public static NetworkStatus getNetworkStatus(final ConnectivityManager connectivityManager) {
+ if (connectivityManager == null) {
+ return NetworkStatus.UNKNOWN;
+ }
+
+ if (isConnected(connectivityManager)) {
+ return NetworkStatus.UP;
+ }
+ return NetworkStatus.DOWN;
+ }
+
+ private static ConnectionSubType getGenericMobileSubtype(final int subtype) {
+ switch (subtype) {
+ // 2G types: fallthrough 5x
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ return ConnectionSubType.CELL_2G;
+ // 3G types: fallthrough 9x
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ return ConnectionSubType.CELL_3G;
+ // 4G - just one type!
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return ConnectionSubType.CELL_4G;
+ default:
+ return ConnectionSubType.UNKNOWN;
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
new file mode 100644
index 0000000000..2fb4015f41
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ProxySelector.java
@@ -0,0 +1,149 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// This code is based on AOSP /libcore/luni/src/main/java/java/net/ProxySelectorImpl.java
+
+package org.mozilla.gecko.util;
+
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+
+public class ProxySelector {
+ public static URLConnection openConnectionWithProxy(final URI uri) throws IOException {
+ final java.net.ProxySelector ps = java.net.ProxySelector.getDefault();
+ Proxy proxy = Proxy.NO_PROXY;
+ if (ps != null) {
+ final List<Proxy> proxies = ps.select(uri);
+ if (proxies != null && !proxies.isEmpty()) {
+ proxy = proxies.get(0);
+ }
+ }
+
+ return uri.toURL().openConnection(proxy);
+ }
+
+ public ProxySelector() {}
+
+ public Proxy select(final String scheme, final String host) {
+ int port = -1;
+ Proxy proxy = null;
+ String nonProxyHostsKey = null;
+ boolean httpProxyOkay = true;
+ if ("http".equalsIgnoreCase(scheme)) {
+ port = 80;
+ nonProxyHostsKey = "http.nonProxyHosts";
+ proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ port = 443;
+ nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support this
+ proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("ftp".equalsIgnoreCase(scheme)) {
+ port = 80; // not 21 as you might guess
+ nonProxyHostsKey = "ftp.nonProxyHosts";
+ proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);
+ } else if ("socket".equalsIgnoreCase(scheme)) {
+ httpProxyOkay = false;
+ } else {
+ return Proxy.NO_PROXY;
+ }
+
+ if (nonProxyHostsKey != null && isNonProxyHost(host, System.getProperty(nonProxyHostsKey))) {
+ return Proxy.NO_PROXY;
+ }
+
+ if (proxy != null) {
+ return proxy;
+ }
+
+ if (httpProxyOkay) {
+ proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);
+ if (proxy != null) {
+ return proxy;
+ }
+ }
+
+ proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);
+ if (proxy != null) {
+ return proxy;
+ }
+
+ return Proxy.NO_PROXY;
+ }
+
+ /** Returns the proxy identified by the {@code hostKey} system property, or null. */
+ @Nullable
+ private Proxy lookupProxy(
+ final String hostKey, final String portKey, final Proxy.Type type, final int defaultPort) {
+ final String host = System.getProperty(hostKey);
+ if (TextUtils.isEmpty(host)) {
+ return null;
+ }
+
+ final int port = getSystemPropertyInt(portKey, defaultPort);
+ if (port == -1) {
+ // Port can be -1. See bug 1270529.
+ return null;
+ }
+
+ return new Proxy(type, InetSocketAddress.createUnresolved(host, port));
+ }
+
+ private int getSystemPropertyInt(final String key, final int defaultValue) {
+ final String string = System.getProperty(key);
+ if (string != null) {
+ try {
+ return Integer.parseInt(string);
+ } catch (final NumberFormatException ignored) {
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns true if the {@code nonProxyHosts} system property pattern exists and matches {@code
+ * host}.
+ */
+ private boolean isNonProxyHost(final String host, final String nonProxyHosts) {
+ if (host == null || nonProxyHosts == null) {
+ return false;
+ }
+
+ // construct pattern
+ final StringBuilder patternBuilder = new StringBuilder();
+ for (int i = 0; i < nonProxyHosts.length(); i++) {
+ final char c = nonProxyHosts.charAt(i);
+ switch (c) {
+ case '.':
+ patternBuilder.append("\\.");
+ break;
+ case '*':
+ patternBuilder.append(".*");
+ break;
+ default:
+ patternBuilder.append(c);
+ }
+ }
+ // check whether the host is the nonProxyHosts.
+ final String pattern = patternBuilder.toString();
+ return host.matches(pattern);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
new file mode 100644
index 0000000000..00625800c9
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/ThreadUtils.java
@@ -0,0 +1,145 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import org.mozilla.gecko.annotation.RobocopTarget;
+
+public final class ThreadUtils {
+ private static final String LOGTAG = "ThreadUtils";
+
+ /**
+ * Controls the action taken when a method like {@link
+ * ThreadUtils#assertOnUiThread(AssertBehavior)} detects a problem.
+ */
+ public enum AssertBehavior {
+ NONE,
+ THROW,
+ }
+
+ private static final Thread sUiThread = Looper.getMainLooper().getThread();
+ private static final Handler sUiHandler = new Handler(Looper.getMainLooper());
+
+ // Referenced directly from GeckoAppShell in highly performance-sensitive code (The extra
+ // function call of the getter was harming performance. (Bug 897123))
+ // Once Bug 709230 is resolved we should reconsider this as ProGuard should be able to optimise
+ // this out at compile time.
+ public static Handler sGeckoHandler;
+ public static volatile Thread sGeckoThread;
+
+ public static Thread getUiThread() {
+ return sUiThread;
+ }
+
+ public static Handler getUiHandler() {
+ return sUiHandler;
+ }
+
+ /**
+ * Runs the provided runnable on the UI thread. If this method is called on the UI thread the
+ * runnable will be executed synchronously.
+ *
+ * @param runnable the runnable to be executed.
+ */
+ public static void runOnUiThread(final Runnable runnable) {
+ // We're on the UI thread already, let's just run this
+ if (isOnUiThread()) {
+ runnable.run();
+ return;
+ }
+
+ postToUiThread(runnable);
+ }
+
+ public static void postToUiThread(final Runnable runnable) {
+ sUiHandler.post(runnable);
+ }
+
+ public static void postToUiThreadDelayed(final Runnable runnable, final long delayMillis) {
+ sUiHandler.postDelayed(runnable, delayMillis);
+ }
+
+ public static void removeUiThreadCallbacks(final Runnable runnable) {
+ sUiHandler.removeCallbacks(runnable);
+ }
+
+ public static Handler getBackgroundHandler() {
+ return GeckoBackgroundThread.getHandler();
+ }
+
+ public static void postToBackgroundThread(final Runnable runnable) {
+ GeckoBackgroundThread.post(runnable);
+ }
+
+ public static void assertOnUiThread(final AssertBehavior assertBehavior) {
+ assertOnThread(getUiThread(), assertBehavior);
+ }
+
+ public static void assertOnUiThread() {
+ assertOnThread(getUiThread(), AssertBehavior.THROW);
+ }
+
+ @RobocopTarget
+ public static void assertOnGeckoThread() {
+ assertOnThread(sGeckoThread, AssertBehavior.THROW);
+ }
+
+ public static void assertOnThread(final Thread expectedThread, final AssertBehavior behavior) {
+ assertOnThreadComparison(expectedThread, behavior, true);
+ }
+
+ private static void assertOnThreadComparison(
+ final Thread expectedThread, final AssertBehavior behavior, final boolean expected) {
+ final Thread currentThread = Thread.currentThread();
+ final long currentThreadId = currentThread.getId();
+ final long expectedThreadId = expectedThread.getId();
+
+ if ((currentThreadId == expectedThreadId) == expected) {
+ return;
+ }
+
+ final String message;
+ if (expected) {
+ message =
+ "Expected thread "
+ + expectedThreadId
+ + " (\""
+ + expectedThread.getName()
+ + "\"), but running on thread "
+ + currentThreadId
+ + " (\""
+ + currentThread.getName()
+ + "\")";
+ } else {
+ message =
+ "Expected anything but "
+ + expectedThreadId
+ + " (\""
+ + expectedThread.getName()
+ + "\"), but running there.";
+ }
+
+ final IllegalThreadStateException e = new IllegalThreadStateException(message);
+
+ switch (behavior) {
+ case THROW:
+ throw e;
+ default:
+ Log.e(LOGTAG, "Method called on wrong thread!", e);
+ }
+ }
+
+ public static boolean isOnUiThread() {
+ return isOnThread(getUiThread());
+ }
+
+ @RobocopTarget
+ public static boolean isOnThread(final Thread thread) {
+ return (Thread.currentThread().getId() == thread.getId());
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMError.jinja b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMError.jinja
new file mode 100644
index 0000000000..f704bbc775
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMError.jinja
@@ -0,0 +1,38 @@
+/* -*- Mode: Java; c-basic-offset: 2; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+public final class XPCOMError {
+ /** Check if the error code corresponds to a failure */
+ public static boolean failed(long err) {
+ return (err & 0x80000000L) != 0;
+ }
+
+ /** Check if the error code corresponds to a failure */
+ public static boolean succeeded(long err) {
+ return !failed(err);
+ }
+
+ /** Extract the error code part of the error message */
+ public static int getErrorCode(long err) {
+ return (int)(err & 0xffffL);
+ }
+
+ /** Extract the error module part of the error message */
+ public static int getErrorModule(long err) {
+ return (int)(((err >> 16) - NS_ERROR_MODULE_BASE_OFFSET) & 0x1fffL);
+ }
+
+ public static final int NS_ERROR_MODULE_BASE_OFFSET = {{ MODULE_BASE_OFFSET }};
+
+{% for mod, val in modules %}
+ public static final int NS_ERROR_MODULE_{{ mod }} = {{ val }};
+{% endfor %}
+
+{% for error, val in errors %}
+ public static final long {{ error }} = 0x{{ "%X" % val }}L;
+{% endfor %}
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMEventTarget.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMEventTarget.java
new file mode 100644
index 0000000000..f3e5248466
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/XPCOMEventTarget.java
@@ -0,0 +1,170 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.util;
+
+import androidx.annotation.NonNull;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.geckoview.BuildConfig;
+
+/**
+ * Wrapper for nsIEventTarget, enabling seamless dispatch of java runnables to Gecko event queues.
+ */
+@WrapForJNI
+public final class XPCOMEventTarget extends JNIObject implements IXPCOMEventTarget {
+ @Override
+ public void execute(final Runnable runnable) {
+ dispatchNative(new JNIRunnable(runnable));
+ }
+
+ public static synchronized IXPCOMEventTarget mainThread() {
+ if (mMainThread == null) {
+ mMainThread = new AsyncProxy("main");
+ }
+ return mMainThread;
+ }
+
+ private static IXPCOMEventTarget mMainThread = null;
+
+ public static synchronized IXPCOMEventTarget launcherThread() {
+ if (mLauncherThread == null) {
+ mLauncherThread = new AsyncProxy("launcher");
+ }
+ return mLauncherThread;
+ }
+
+ private static IXPCOMEventTarget mLauncherThread = null;
+
+ /**
+ * Runs the provided runnable on the launcher thread. If this method is called from the launcher
+ * thread itself, the runnable will be executed immediately and synchronously.
+ */
+ public static void runOnLauncherThread(@NonNull final Runnable runnable) {
+ final IXPCOMEventTarget launcherThread = launcherThread();
+ if (launcherThread.isOnCurrentThread()) {
+ // We're already on the launcher thread, just execute the runnable
+ runnable.run();
+ return;
+ }
+
+ launcherThread.execute(runnable);
+ }
+
+ public static void assertOnLauncherThread() {
+ if (BuildConfig.DEBUG_BUILD && !launcherThread().isOnCurrentThread()) {
+ throw new AssertionError("Expected to be running on XPCOM launcher thread");
+ }
+ }
+
+ public static void assertNotOnLauncherThread() {
+ if (BuildConfig.DEBUG_BUILD && launcherThread().isOnCurrentThread()) {
+ throw new AssertionError("Expected to not be running on XPCOM launcher thread");
+ }
+ }
+
+ private static synchronized IXPCOMEventTarget getTarget(final String name) {
+ if (name.equals("launcher")) {
+ return mLauncherThread;
+ } else if (name.equals("main")) {
+ return mMainThread;
+ } else {
+ throw new RuntimeException("Attempt to assign to unknown thread named " + name);
+ }
+ }
+
+ @WrapForJNI
+ private static synchronized void setTarget(final String name, final XPCOMEventTarget target) {
+ if (name.equals("main")) {
+ mMainThread = target;
+ } else if (name.equals("launcher")) {
+ mLauncherThread = target;
+ } else {
+ throw new RuntimeException("Attempt to assign to unknown thread named " + name);
+ }
+
+ // Ensure that we see the right name in the Java debugger. We don't do this for mMainThread
+ // because its name was already set (in this context, "main" is the GeckoThread).
+ if (mMainThread != target) {
+ target.execute(
+ () -> {
+ Thread.currentThread().setName(name);
+ });
+ }
+ }
+
+ @Override
+ public native boolean isOnCurrentThread();
+
+ private native void dispatchNative(final JNIRunnable runnable);
+
+ @WrapForJNI
+ private static synchronized void resolveAndDispatch(final String name, final Runnable runnable) {
+ getTarget(name).execute(runnable);
+ }
+
+ private static native void resolveAndDispatchNative(final String name, final Runnable runnable);
+
+ @Override
+ protected native void disposeNative();
+
+ @WrapForJNI
+ private static final class JNIRunnable {
+ JNIRunnable(final Runnable inner) {
+ mInner = inner;
+ }
+
+ @WrapForJNI
+ void run() {
+ mInner.run();
+ }
+
+ private Runnable mInner;
+ }
+
+ private static final class AsyncProxy implements IXPCOMEventTarget {
+ private String mTargetName;
+
+ public AsyncProxy(final String targetName) {
+ mTargetName = targetName;
+ }
+
+ @Override
+ public void execute(final Runnable runnable) {
+ final IXPCOMEventTarget target = XPCOMEventTarget.getTarget(mTargetName);
+
+ if (target instanceof XPCOMEventTarget) {
+ target.execute(runnable);
+ return;
+ }
+
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.JNI_READY,
+ XPCOMEventTarget.class,
+ "resolveAndDispatchNative",
+ String.class,
+ mTargetName,
+ Runnable.class,
+ runnable);
+ }
+
+ @Override
+ public boolean isOnCurrentThread() {
+ final IXPCOMEventTarget target = XPCOMEventTarget.getTarget(mTargetName);
+
+ // If target is not yet a XPCOMEventTarget then JNI is not
+ // initialized yet. If JNI is not initialized yet, then we cannot
+ // possibly be running on a target with an XPCOMEventTarget.
+ if (!(target instanceof XPCOMEventTarget)) {
+ return false;
+ }
+
+ // Otherwise we have a real XPCOMEventTarget, so we can delegate
+ // this call to it.
+ return target.isOnCurrentThread();
+ }
+ }
+}