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); |
+} |