Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(125)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/input/GamepadAdapter.java

Issue 165483003: Gamepad API for Android (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: address comments Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: content/public/android/java/src/org/chromium/content/browser/input/GamepadAdapter.java
diff --git a/content/public/android/java/src/org/chromium/content/browser/input/GamepadAdapter.java b/content/public/android/java/src/org/chromium/content/browser/input/GamepadAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9baf272db2e63b33f52eb52e65b8212c6d82b92
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/input/GamepadAdapter.java
@@ -0,0 +1,361 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.content.browser.input;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.view.InputDevice;
+import android.view.InputDevice.MotionRange;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import org.chromium.base.CalledByNative;
+import org.chromium.base.JNINamespace;
+import org.chromium.base.ThreadUtils;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Java counterpart of GamepadPlatformDataFetcherAndroid.
+ * Manages game input devices and feed Gamepad API with input data.
+ * GamepadPlatformDataFetcherAndroid is merely a wrepper around this.
+ * Native callable methods called by GamepadPlatformDataFetcherAndroid on the poller thread
+ * which is a native thread without a java looper. Events are processed on the UI thread.
+ */
+@JNINamespace("content")
+public class GamepadAdapter implements InputManager.InputDeviceListener {
+
+ private static final int NUM_WEB_GAMEPADS = 4;
+
+ private static GamepadAdapter instance;
+
+ private InputManager mInputManager;
+ private InputDeviceHandler[] mDeviceHandlers;
+ private final Object mDeviceHandlersLock = new Object();
+ private boolean mDataRequested;
+ private boolean mIsPaused;
+ private int mAttachedToWindowCounter;
+
+ private static void initializeInstance() {
+ if (instance == null) {
+ instance = new GamepadAdapter();
+ }
+ }
+
+ /**
+ * Notifies GamepadAdapter that a {@link ContentView} is attached to a window so it should
+ * be prepared for input. Must be called before {@link onMotionEvent} or {@link onKeyEvent}.
+ */
+ public static void onAttachedToWindow(Context context) {
+ assert ThreadUtils.runningOnUiThread();
+ initializeInstance();
+ instance.attachedToWindow(context);
+ }
+
+ private void attachedToWindow(Context context) {
+ if (mAttachedToWindowCounter++ == 0) {
+ mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
+ initializeDevices();
+ mInputManager.registerInputDeviceListener(this, null);
+ }
+ }
+
+ private static boolean isAttached() {
+ return instance != null && instance.mAttachedToWindowCounter > 0;
+ }
+
+ /**
+ * Notifies GamepadAdapter that a {@link ContentView} is detached from it's window.
+ */
+ public static void onDetachedFromWindow() {
+ assert ThreadUtils.runningOnUiThread();
+ assert isAttached();
+ instance.detachedFromWindow();
+ }
+
+ private void detachedFromWindow() {
+ if (--mAttachedToWindowCounter == 0) {
+ synchronized (mDeviceHandlersLock) {
+ for (int i = 0; i < NUM_WEB_GAMEPADS; i++) {
+ mDeviceHandlers[i] = null;
+ }
+ }
+ mInputManager.unregisterInputDeviceListener(this);
+ mInputManager = null;
+ }
+ }
+
+ private void initializeDevices() {
+ assert ThreadUtils.runningOnUiThread();
+ InputDeviceHandler[] handlers = new InputDeviceHandler[NUM_WEB_GAMEPADS];
+ int[] ids = mInputManager.getInputDeviceIds();
+ if (ids == null) return;
+
+ int activeDevices = 0;
+ for (int i = 0; i < ids.length && activeDevices < NUM_WEB_GAMEPADS; i++) {
+ InputDevice device = mInputManager.getInputDevice(ids[i]);
+ if (isGameDevice(device)) {
+ handlers[activeDevices++] = new InputDeviceHandler(device);
+ }
+ }
+ synchronized (mDeviceHandlersLock) {
+ mDeviceHandlers = handlers;
+ }
+ }
+
+ // ---------------------------------------------------
+ // Implementation of InputManager.InputDeviceListener.
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ ThreadUtils.assertOnUiThread();
+ InputDevice device = mInputManager.getInputDevice(deviceId);
+ if (!isGameDevice(device))
+ return;
+ int index = nextAvailableIndex();
+ if (index == -1)
+ return;
+ synchronized (mDeviceHandlersLock) {
+ mDeviceHandlers[index] = new InputDeviceHandler(device);
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ ThreadUtils.assertOnUiThread();
+ int index = indexForDeviceId(deviceId);
+ if (index == -1)
+ return;
+ synchronized (mDeviceHandlersLock) {
+ mDeviceHandlers[index] = null;
+ }
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ ThreadUtils.assertOnUiThread();
+ int index = indexForDeviceId(deviceId);
+ if (index == -1) {
+ index = nextAvailableIndex();
+ if (index == -1) return;
+ }
+ InputDevice device = mInputManager.getInputDevice(deviceId);
+ synchronized (mDeviceHandlersLock) {
+ mDeviceHandlers[index] = null;
+ if (isGameDevice(device)) {
+ mDeviceHandlers[index] = new InputDeviceHandler(device);
+ }
+ }
+ }
+ // ---------------------------------------------------
+
+ /**
+ * Handles motion events from gamepad devices.
+ *
+ * @return True if the event has been consumed.
+ */
+ public static boolean onMotionEvent(MotionEvent event) {
+ assert isAttached();
+ return instance.handleMotionEvent(event);
+ }
+
+ private boolean handleMotionEvent(MotionEvent event) {
+ if (!mDataRequested) return false;
+ InputDeviceHandler handler = handlerForDeviceId(event.getDeviceId());
+ if (handler == null) return false;
+ if (!isGameEvent(event)) return false;
+
+ handler.handleMotionEvent(event);
+ return true;
+ }
+
+ /**
+ * Handles key events from gamepad devices.
+ *
+ * @return True if the event has been consumed.
+ */
+ public static boolean onKeyEvent(KeyEvent event) {
+ assert isAttached();
+ return instance.handleKeyEvent(event);
+ }
+
+ private boolean handleKeyEvent(KeyEvent event) {
+ if (!mDataRequested) return false;
+ if (event.getAction() != KeyEvent.ACTION_DOWN
+ && event.getAction() != KeyEvent.ACTION_UP) {
+ return false;
+ }
+ InputDeviceHandler handler = handlerForDeviceId(event.getDeviceId());
+ if (handler == null) return false;
+ int keyCode = event.getKeyCode();
+ if (!isGameKey(keyCode)) return false;
+
+ boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN;
+ handler.handleKeyEvent(keyCode, isDown, event.getEventTime());
+ return true;
+ }
+
+ @CalledByNative
+ static void setDataRequested(boolean requested) {
+ initializeInstance();
+ instance.mDataRequested = requested;
+ }
+
+ // Called on polling thread.
+ @CalledByNative
+ static void getGamepadData(long gamepads) {
+ assert instance != null;
+ instance.reportGamepadData(gamepads);
+ }
+
+ private void reportGamepadData(long gamepads) {
+ if (!mDataRequested) {
+ // Clear input to avoid anomalies because we don't watch events when data is not
+ // requested. We can miss the release of a button and falsely report that it's pushed
+ // when data is requested again. We do this here after setting mDataRequested to
+ // true instead in setDataRequested when setting it to false because this
+ // way no locking is needed. This can race with onMotionEvent or onKeyEvent
+ // but it couldn't result in inconsistent data.
+ mDataRequested = true;
+ clearInput();
+ }
+
+ synchronized (mDeviceHandlersLock) {
+ for (int i = 0; i < NUM_WEB_GAMEPADS; i++) {
+ if (mDeviceHandlers[i] == null) {
+ nativeRefreshGamepad(gamepads, i, false, null, null, 0, null, null);
+ } else {
+ InputDeviceHandler handler = mDeviceHandlers[i];
+ WebGamepadData data = handler.produceWebData();
+ nativeRefreshGamepad(gamepads, i, true, data.id, data.mapping,
+ handler.getTimestamp(), data.axes, data.buttons);
+ }
+ }
+ }
+ }
+
+ void clearInput() {
+ for (int i = 0; i < mDeviceHandlers.length; i++) {
+ if (mDeviceHandlers[i] != null) {
+ mDeviceHandlers[i].clearInput();
+ }
+ }
+ }
+
+ private int nextAvailableIndex() {
+ for (int i = 0; i < mDeviceHandlers.length; i++) {
+ if (mDeviceHandlers[i] == null) return i;
+ }
+ return -1;
+ }
+
+ private int indexForDeviceId(int deviceId) {
+ for (int i = 0; i < mDeviceHandlers.length; i++) {
+ if (mDeviceHandlers[i] != null &&
+ mDeviceHandlers[i].getInputDevice().getId() == deviceId)
+ return i;
+ }
+ return -1;
+ }
+
+ private InputDeviceHandler handlerForDeviceId(int deviceId) {
+ int index = indexForDeviceId(deviceId);
+ return index == -1 ? null : mDeviceHandlers[index];
+ }
+
+ private static boolean isGameDevice(InputDevice device) {
+ return (device.getSources() & InputDevice.SOURCE_JOYSTICK) != 0;
+ }
+
+ private static boolean isGameEvent(MotionEvent event) {
+ return (event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0
+ && event.getAction() == MotionEvent.ACTION_MOVE;
+ }
+
+ private static boolean isGameKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ return true;
+ default:
+ return KeyEvent.isGamepadButton(keyCode);
+ }
+ }
+
+ private static class InputDeviceHandler {
+ private final InputDevice mDevice;
+ private long mTimestamp;
+ private final int[] mAxes;
+
+ // Apparently all axis id's and keycodes are less then 256. Given this the most effective
+ // representation of an associative array is simply an array.
+ private final float[] mAxisValues = new float[256];
+ private final boolean[] mButtonsPressedStates = new boolean[256];
+
+ private final GamepadDataMapper mMapper;
+ private final Object mLock = new Object();
+
+ InputDevice getInputDevice() { return mDevice; }
+ long getTimestamp() { return mTimestamp; }
+
+ InputDeviceHandler(InputDevice device) {
+ assert isGameDevice(device);
+ mDevice = device;
+ mMapper = GamepadDataMapper.createDataMapper(device.getName());
+
+ List<MotionRange> ranges = device.getMotionRanges();
+ mAxes = new int[ranges.size()];
+ int i = 0;
+ for (MotionRange range : ranges) {
+ if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
+ int axis = range.getAxis();
+ assert axis < 256;
+ mAxes[i++] = axis;
+ }
+ }
+ }
+
+ // Called on UI thread.
+ void handleMotionEvent(MotionEvent event) {
+ synchronized (mLock) {
+ mTimestamp = event.getEventTime();
+ for (int i = 0; i < mAxes.length; i++) {
+ int axis = mAxes[i];
+ mAxisValues[axis] = event.getAxisValue(axis);
+ }
+ }
+ }
+
+ // Called on UI thread.
+ void handleKeyEvent(int keyCode, boolean isDown, long timestamp) {
+ synchronized (mLock) {
+ mTimestamp = timestamp;
+ assert keyCode < 256;
+ mButtonsPressedStates[keyCode] = isDown;
+ }
+ }
+
+ // Called on polling thread.
+ WebGamepadData produceWebData() {
+ synchronized (mLock) {
+ return mMapper.map(mAxisValues, mButtonsPressedStates);
+ }
+ }
+
+ // Called on polling thread.
+ void clearInput() {
+ synchronized (mLock) {
+ Arrays.fill(mAxisValues, 0);
+ Arrays.fill(mButtonsPressedStates, false);
+ }
+ }
+ }
+
+ private native void nativeRefreshGamepad(long gamepads, int index, boolean connected,
+ String id, String mapping, long timestamp, float[] axes, float[] buttons);
+}

Powered by Google App Engine
This is Rietveld 408576698