summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java425
1 files changed, 425 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 000000000..54e1b0931
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/AndroidGamepadManager.java
@@ -0,0 +1,425 @@
+/* -*- 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 java.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+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;
+
+
+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(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(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(int axis, int negativeButton, 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(int button) {
+ this.button = button;
+ }
+ }
+
+ private static class Gamepad {
+ // ID from GamepadService
+ public int id;
+ // Retain axis state so we can determine changes.
+ public float axes[];
+ public boolean dpad[];
+ public int triggerAxes[];
+ public float triggers[];
+
+ public Gamepad(int serviceId, int deviceId) {
+ id = serviceId;
+ axes = new float[Axis.values().length];
+ dpad = new boolean[4];
+ triggers = new float[2];
+
+ 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", dispatchTo = "gecko")
+ private static native void onGamepadChange(int id, boolean added);
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onButtonChange(int id, int button, boolean pressed, float value);
+ @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko")
+ private static native void onAxisChange(int id, boolean[] valid, float[] values);
+
+ 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() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doStart();
+ }
+ });
+ }
+
+ /* package */ static void doStart() {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ scanForGamepads();
+ addDeviceListener();
+ sStarted = true;
+ }
+ }
+
+ @WrapForJNI
+ private static void stop() {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ doStop();
+ }
+ });
+ }
+
+ /* package */ static void doStop() {
+ ThreadUtils.assertOnUiThread();
+ if (sStarted) {
+ removeDeviceListener();
+ sPendingGamepads.clear();
+ sGamepads.clear();
+ sStarted = false;
+ }
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void onGamepadAdded(final int device_id, final int service_id) {
+ ThreadUtils.postToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ handleGamepadAdded(device_id, service_id);
+ }
+ });
+ }
+
+ /* package */ static void handleGamepadAdded(int deviceId, int serviceId) {
+ 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(serviceId, deviceId));
+ // Handle queued KeyEvents
+ for (KeyEvent ev : pending) {
+ handleKeyEvent(ev);
+ }
+ }
+
+ private static float deadZone(MotionEvent ev, int axis) {
+ if (GamepadUtils.isValueInDeadZone(ev, axis)) {
+ return 0.0f;
+ }
+ return ev.getAxisValue(axis);
+ }
+
+ private static void mapDpadAxis(Gamepad gamepad,
+ boolean pressed,
+ float value,
+ int which) {
+ if (pressed != gamepad.dpad[which]) {
+ gamepad.dpad[which] = pressed;
+ onButtonChange(gamepad.id, FIRST_DPAD_BUTTON + which, pressed, Math.abs(value));
+ }
+ }
+
+ public static boolean handleMotionEvent(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
+ boolean[] valid = new boolean[Axis.values().length];
+ float[] axes = new float[Axis.values().length];
+ boolean anyValidAxes = false;
+ for (Axis axis : Axis.values()) {
+ float value = deadZone(ev, axis.axis);
+ 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.id, valid, axes);
+ }
+
+ // Map triggers to buttons.
+ if (gamepad.triggerAxes != null) {
+ for (Trigger trigger : Trigger.values()) {
+ int i = trigger.ordinal();
+ int axis = gamepad.triggerAxes[i];
+ float value = deadZone(ev, axis);
+ if (value != gamepad.triggers[i]) {
+ gamepad.triggers[i] = value;
+ boolean pressed = value > TRIGGER_PRESSED_THRESHOLD;
+ onButtonChange(gamepad.id, trigger.button, pressed, value);
+ }
+ }
+ }
+ // Map d-pad to buttons.
+ for (DpadAxis dpadaxis : DpadAxis.values()) {
+ 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(KeyEvent ev) {
+ ThreadUtils.assertOnUiThread();
+ if (!sStarted) {
+ return false;
+ }
+
+ 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) {
+ 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 (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;
+ }
+
+ Gamepad gamepad = sGamepads.get(deviceId);
+ boolean pressed = ev.getAction() == KeyEvent.ACTION_DOWN;
+ onButtonChange(gamepad.id, key, pressed, pressed ? 1.0f : 0.0f);
+ return true;
+ }
+
+ private static void scanForGamepads() {
+ int[] deviceIds = InputDevice.getDeviceIds();
+ if (deviceIds == null) {
+ return;
+ }
+ for (int i = 0; i < deviceIds.length; i++) {
+ 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(InputDevice device) {
+ sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
+ onGamepadChange(device.getId(), true);
+ }
+
+ private static void removeGamepad(int deviceId) {
+ Gamepad gamepad = sGamepads.get(deviceId);
+ onGamepadChange(gamepad.id, false);
+ sGamepads.remove(deviceId);
+ }
+
+ private static void addDeviceListener() {
+ if (Versions.preJB) {
+ // Poll known gamepads to see if they've disappeared.
+ sPollTimer = new Timer();
+ sPollTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ for (int i = 0; i < sGamepads.size(); ++i) {
+ final int deviceId = sGamepads.keyAt(i);
+ if (InputDevice.getDevice(deviceId) == null) {
+ removeGamepad(deviceId);
+ }
+ }
+ }
+ }, POLL_TIMER_PERIOD, POLL_TIMER_PERIOD);
+ return;
+ }
+ sListener = new InputManager.InputDeviceListener() {
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ InputDevice device = InputDevice.getDevice(deviceId);
+ if (device == null) {
+ return;
+ }
+ if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ addGamepad(device);
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(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(int deviceId) {
+ }
+ };
+ ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).registerInputDeviceListener(sListener, ThreadUtils.getUiHandler());
+ }
+
+ private static void removeDeviceListener() {
+ if (Versions.preJB) {
+ if (sPollTimer != null) {
+ sPollTimer.cancel();
+ sPollTimer = null;
+ }
+ return;
+ }
+ ((InputManager)GeckoAppShell.getContext().getSystemService(Context.INPUT_SERVICE)).unregisterInputDeviceListener(sListener);
+ sListener = null;
+ }
+}