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 }