From 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:47:29 +0200 Subject: Adding upstream version 115.8.0esr. Signed-off-by: Daniel Baumann --- .../org/mozilla/gecko/AndroidGamepadManager.java | 415 +++++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java') 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..44aa7bc461 --- /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 static enum Axis { + X(MotionEvent.AXIS_X), + Y(MotionEvent.AXIS_Y), + Z(MotionEvent.AXIS_Z), + RZ(MotionEvent.AXIS_RZ); + + public final int axis; + + private 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 static enum Trigger { + Left(6), + Right(7); + + public final int button; + + private 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 static 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; + + private DpadAxis(final int axis, final int negativeButton, final int positiveButton) { + this.axis = axis; + this.negativeButton = negativeButton; + this.positiveButton = positiveButton; + } + } + + private static 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; + + private 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 sGamepads = new SparseArray<>(); + private static final SparseArray> 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 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 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()); + 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; + } +} -- cgit v1.2.3