tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

AndroidGamepadManager.java (12918B)


      1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 package org.mozilla.gecko;
      7 
      8 import android.content.Context;
      9 import android.hardware.input.InputManager;
     10 import android.util.SparseArray;
     11 import android.view.InputDevice;
     12 import android.view.KeyEvent;
     13 import android.view.MotionEvent;
     14 import java.util.ArrayList;
     15 import java.util.List;
     16 import java.util.Timer;
     17 import org.mozilla.gecko.annotation.WrapForJNI;
     18 import org.mozilla.gecko.util.ThreadUtils;
     19 
     20 public class AndroidGamepadManager {
     21  // This is completely arbitrary.
     22  private static final float TRIGGER_PRESSED_THRESHOLD = 0.25f;
     23  private static final long POLL_TIMER_PERIOD = 1000; // milliseconds
     24 
     25  private enum Axis {
     26    X(MotionEvent.AXIS_X),
     27    Y(MotionEvent.AXIS_Y),
     28    Z(MotionEvent.AXIS_Z),
     29    RZ(MotionEvent.AXIS_RZ);
     30 
     31    public final int axis;
     32 
     33    Axis(final int axis) {
     34      this.axis = axis;
     35    }
     36  }
     37 
     38  // A list of gamepad button mappings. Axes are determined at
     39  // runtime, as they vary by Android version.
     40  private enum Trigger {
     41    Left(6),
     42    Right(7);
     43 
     44    public final int button;
     45 
     46    Trigger(final int button) {
     47      this.button = button;
     48    }
     49  }
     50 
     51  private static final int FIRST_DPAD_BUTTON = 12;
     52 
     53  // A list of axis number, gamepad button mappings for negative, positive.
     54  // Button mappings are added to FIRST_DPAD_BUTTON.
     55  private enum DpadAxis {
     56    UpDown(MotionEvent.AXIS_HAT_Y, 0, 1),
     57    LeftRight(MotionEvent.AXIS_HAT_X, 2, 3);
     58 
     59    public final int axis;
     60    public final int negativeButton;
     61    public final int positiveButton;
     62 
     63    DpadAxis(final int axis, final int negativeButton, final int positiveButton) {
     64      this.axis = axis;
     65      this.negativeButton = negativeButton;
     66      this.positiveButton = positiveButton;
     67    }
     68  }
     69 
     70  private enum Button {
     71    A(KeyEvent.KEYCODE_BUTTON_A),
     72    B(KeyEvent.KEYCODE_BUTTON_B),
     73    X(KeyEvent.KEYCODE_BUTTON_X),
     74    Y(KeyEvent.KEYCODE_BUTTON_Y),
     75    L1(KeyEvent.KEYCODE_BUTTON_L1),
     76    R1(KeyEvent.KEYCODE_BUTTON_R1),
     77    L2(KeyEvent.KEYCODE_BUTTON_L2),
     78    R2(KeyEvent.KEYCODE_BUTTON_R2),
     79    SELECT(KeyEvent.KEYCODE_BUTTON_SELECT),
     80    START(KeyEvent.KEYCODE_BUTTON_START),
     81    THUMBL(KeyEvent.KEYCODE_BUTTON_THUMBL),
     82    THUMBR(KeyEvent.KEYCODE_BUTTON_THUMBR),
     83    DPAD_UP(KeyEvent.KEYCODE_DPAD_UP),
     84    DPAD_DOWN(KeyEvent.KEYCODE_DPAD_DOWN),
     85    DPAD_LEFT(KeyEvent.KEYCODE_DPAD_LEFT),
     86    DPAD_RIGHT(KeyEvent.KEYCODE_DPAD_RIGHT);
     87 
     88    public final int button;
     89 
     90    Button(final int button) {
     91      this.button = button;
     92    }
     93  }
     94 
     95  private static class Gamepad {
     96    // ID from GamepadService
     97    public byte[] handle;
     98    // Retain axis state so we can determine changes.
     99    public float axes[];
    100    public boolean dpad[];
    101    public int triggerAxes[];
    102    public float triggers[];
    103 
    104    public Gamepad(final byte[] handle, final int deviceId) {
    105      this.handle = handle;
    106      axes = new float[Axis.values().length];
    107      dpad = new boolean[4];
    108      triggers = new float[2];
    109 
    110      final InputDevice device = InputDevice.getDevice(deviceId);
    111      if (device != null) {
    112        // LTRIGGER/RTRIGGER don't seem to be exposed on older
    113        // versions of Android.
    114        if (device.getMotionRange(MotionEvent.AXIS_LTRIGGER) != null
    115            && device.getMotionRange(MotionEvent.AXIS_RTRIGGER) != null) {
    116          triggerAxes = new int[] {MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_RTRIGGER};
    117        } else if (device.getMotionRange(MotionEvent.AXIS_BRAKE) != null
    118            && device.getMotionRange(MotionEvent.AXIS_GAS) != null) {
    119          triggerAxes = new int[] {MotionEvent.AXIS_BRAKE, MotionEvent.AXIS_GAS};
    120        } else {
    121          triggerAxes = null;
    122        }
    123      }
    124    }
    125  }
    126 
    127  @WrapForJNI(calledFrom = "ui")
    128  private static native byte[] nativeAddGamepad(String aName);
    129 
    130  @WrapForJNI(calledFrom = "ui")
    131  private static native void nativeRemoveGamepad(byte[] aGamepadHandle);
    132 
    133  @WrapForJNI(calledFrom = "ui")
    134  private static native void onButtonChange(
    135      byte[] aGamepadHandle, int aButton, boolean aPressed, float aValue);
    136 
    137  @WrapForJNI(calledFrom = "ui")
    138  private static native void onAxisChange(byte[] aGamepadHandle, boolean[] aValid, float[] aValues);
    139 
    140  private static boolean sStarted;
    141  private static final SparseArray<Gamepad> sGamepads = new SparseArray<>();
    142  private static final SparseArray<List<KeyEvent>> sPendingGamepads = new SparseArray<>();
    143  private static InputManager.InputDeviceListener sListener;
    144  private static Timer sPollTimer;
    145 
    146  private AndroidGamepadManager() {}
    147 
    148  @WrapForJNI
    149  private static void start(final Context context) {
    150    ThreadUtils.runOnUiThread(
    151        new Runnable() {
    152          @Override
    153          public void run() {
    154            doStart(context);
    155          }
    156        });
    157  }
    158 
    159  /* package */ static void doStart(final Context context) {
    160    ThreadUtils.assertOnUiThread();
    161    if (!sStarted) {
    162      sStarted = true;
    163      scanForGamepads();
    164      addDeviceListener(context);
    165    }
    166  }
    167 
    168  @WrapForJNI
    169  private static void stop(final Context context) {
    170    ThreadUtils.runOnUiThread(
    171        new Runnable() {
    172          @Override
    173          public void run() {
    174            doStop(context);
    175          }
    176        });
    177  }
    178 
    179  /* package */ static void doStop(final Context context) {
    180    ThreadUtils.assertOnUiThread();
    181    if (sStarted) {
    182      removeDeviceListener(context);
    183      sPendingGamepads.clear();
    184      sGamepads.clear();
    185      sStarted = false;
    186    }
    187  }
    188 
    189  /* package */ static void handleGamepadAdded(final int deviceId, final byte[] gamepadHandle) {
    190    ThreadUtils.assertOnUiThread();
    191    if (!sStarted) {
    192      return;
    193    }
    194 
    195    final List<KeyEvent> pending = sPendingGamepads.get(deviceId);
    196    if (pending == null) {
    197      removeGamepad(deviceId);
    198      return;
    199    }
    200 
    201    sPendingGamepads.remove(deviceId);
    202    sGamepads.put(deviceId, new Gamepad(gamepadHandle, deviceId));
    203    // Handle queued KeyEvents
    204    for (final KeyEvent ev : pending) {
    205      handleKeyEvent(ev);
    206    }
    207  }
    208 
    209  private static float sDeadZoneThresholdOverride = 1e-2f;
    210 
    211  private static boolean isValueInDeadZone(final MotionEvent event, final int axis) {
    212    final float threshold;
    213    if (sDeadZoneThresholdOverride >= 0) {
    214      threshold = sDeadZoneThresholdOverride;
    215    } else {
    216      final InputDevice.MotionRange range = event.getDevice().getMotionRange(axis);
    217      threshold = range.getFlat() + range.getFuzz();
    218    }
    219    final float value = event.getAxisValue(axis);
    220    return (Math.abs(value) < threshold);
    221  }
    222 
    223  private static float deadZone(final MotionEvent ev, final int axis) {
    224    if (isValueInDeadZone(ev, axis)) {
    225      return 0.0f;
    226    }
    227    return ev.getAxisValue(axis);
    228  }
    229 
    230  private static void mapDpadAxis(
    231      final Gamepad gamepad, final boolean pressed, final float value, final int which) {
    232    if (pressed != gamepad.dpad[which]) {
    233      gamepad.dpad[which] = pressed;
    234      onButtonChange(gamepad.handle, FIRST_DPAD_BUTTON + which, pressed, Math.abs(value));
    235    }
    236  }
    237 
    238  public static boolean handleMotionEvent(final MotionEvent ev) {
    239    ThreadUtils.assertOnUiThread();
    240    if (!sStarted) {
    241      return false;
    242    }
    243 
    244    final Gamepad gamepad = sGamepads.get(ev.getDeviceId());
    245    if (gamepad == null) {
    246      // Not a device we care about.
    247      return false;
    248    }
    249 
    250    // First check the analog stick axes
    251    final boolean[] valid = new boolean[Axis.values().length];
    252    final float[] axes = new float[Axis.values().length];
    253    boolean anyValidAxes = false;
    254    for (final Axis axis : Axis.values()) {
    255      final float value = deadZone(ev, axis.axis);
    256      final int i = axis.ordinal();
    257      if (value != gamepad.axes[i]) {
    258        axes[i] = value;
    259        gamepad.axes[i] = value;
    260        valid[i] = true;
    261        anyValidAxes = true;
    262      }
    263    }
    264    if (anyValidAxes) {
    265      // Send an axismove event.
    266      onAxisChange(gamepad.handle, valid, axes);
    267    }
    268 
    269    // Map triggers to buttons.
    270    if (gamepad.triggerAxes != null) {
    271      for (final Trigger trigger : Trigger.values()) {
    272        final int i = trigger.ordinal();
    273        final int axis = gamepad.triggerAxes[i];
    274        final float value = deadZone(ev, axis);
    275        if (value != gamepad.triggers[i]) {
    276          gamepad.triggers[i] = value;
    277          final boolean pressed = value > TRIGGER_PRESSED_THRESHOLD;
    278          onButtonChange(gamepad.handle, trigger.button, pressed, value);
    279        }
    280      }
    281    }
    282    // Map d-pad to buttons.
    283    for (final DpadAxis dpadaxis : DpadAxis.values()) {
    284      final float value = deadZone(ev, dpadaxis.axis);
    285      mapDpadAxis(gamepad, value < 0.0f, value, dpadaxis.negativeButton);
    286      mapDpadAxis(gamepad, value > 0.0f, value, dpadaxis.positiveButton);
    287    }
    288    return true;
    289  }
    290 
    291  public static boolean handleKeyEvent(final KeyEvent ev) {
    292    ThreadUtils.assertOnUiThread();
    293    if (!sStarted) {
    294      return false;
    295    }
    296 
    297    final int deviceId = ev.getDeviceId();
    298    final List<KeyEvent> pendingGamepad = sPendingGamepads.get(deviceId);
    299    if (pendingGamepad != null) {
    300      // Queue up key events for pending devices.
    301      pendingGamepad.add(ev);
    302      return true;
    303    }
    304 
    305    if (sGamepads.get(deviceId) == null) {
    306      final InputDevice device = ev.getDevice();
    307      if (device != null
    308          && (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
    309        // This is a gamepad we haven't seen yet.
    310        addGamepad(device);
    311        sPendingGamepads.get(deviceId).add(ev);
    312        return true;
    313      }
    314      // Not a device we care about.
    315      return false;
    316    }
    317 
    318    int key = -1;
    319    for (final Button button : Button.values()) {
    320      if (button.button == ev.getKeyCode()) {
    321        key = button.ordinal();
    322        break;
    323      }
    324    }
    325    if (key == -1) {
    326      // Not a key we know how to handle.
    327      return false;
    328    }
    329    if (ev.getRepeatCount() > 0) {
    330      // We would handle this key, but we're not interested in
    331      // repeats. Eat it.
    332      return true;
    333    }
    334 
    335    final Gamepad gamepad = sGamepads.get(deviceId);
    336    final boolean pressed = ev.getAction() == KeyEvent.ACTION_DOWN;
    337    onButtonChange(gamepad.handle, key, pressed, pressed ? 1.0f : 0.0f);
    338    return true;
    339  }
    340 
    341  private static void scanForGamepads() {
    342    final int[] deviceIds = InputDevice.getDeviceIds();
    343    if (deviceIds == null) {
    344      return;
    345    }
    346    for (int i = 0; i < deviceIds.length; i++) {
    347      final InputDevice device = InputDevice.getDevice(deviceIds[i]);
    348      if (device == null) {
    349        continue;
    350      }
    351      if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD) {
    352        continue;
    353      }
    354      addGamepad(device);
    355    }
    356  }
    357 
    358  private static void addGamepad(final InputDevice device) {
    359    sPendingGamepads.put(device.getId(), new ArrayList<KeyEvent>());
    360    final byte[] gamepadId = nativeAddGamepad(device.getName());
    361    ThreadUtils.runOnUiThread(
    362        new Runnable() {
    363          @Override
    364          public void run() {
    365            handleGamepadAdded(device.getId(), gamepadId);
    366          }
    367        });
    368  }
    369 
    370  private static void removeGamepad(final int deviceId) {
    371    final Gamepad gamepad = sGamepads.get(deviceId);
    372    nativeRemoveGamepad(gamepad.handle);
    373    sGamepads.remove(deviceId);
    374  }
    375 
    376  private static void addDeviceListener(final Context context) {
    377    sListener =
    378        new InputManager.InputDeviceListener() {
    379          @Override
    380          public void onInputDeviceAdded(final int deviceId) {
    381            final InputDevice device = InputDevice.getDevice(deviceId);
    382            if (device == null) {
    383              return;
    384            }
    385            if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
    386              addGamepad(device);
    387            }
    388          }
    389 
    390          @Override
    391          public void onInputDeviceRemoved(final int deviceId) {
    392            if (sPendingGamepads.get(deviceId) != null) {
    393              // Got removed before Gecko's ack reached us.
    394              // gamepadAdded will deal with it.
    395              sPendingGamepads.remove(deviceId);
    396              return;
    397            }
    398            if (sGamepads.get(deviceId) != null) {
    399              removeGamepad(deviceId);
    400            }
    401          }
    402 
    403          @Override
    404          public void onInputDeviceChanged(final int deviceId) {}
    405        };
    406    final InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
    407    im.registerInputDeviceListener(sListener, ThreadUtils.getUiHandler());
    408  }
    409 
    410  private static void removeDeviceListener(final Context context) {
    411    final InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
    412    im.unregisterInputDeviceListener(sListener);
    413    sListener = null;
    414  }
    415 }