gamepad: Add an Android mapping function for Stadia Controller
The default mapping function maps all buttons and axes correctly except for the Assistant and Capture buttons. This CL adds a new mapping function that maps these buttons correctly. On Linux, HID Button usages are mapped to scancodes in the BTN_GAMEPAD and BTN_TRIGGER_HAPPY ranges. Android translates the BTN_GAMEPAD scancodes into KeyEvent.KEYCODE_BUTTON_* keycodes, but BTN_TRIGGER_HAPPY scancodes do not have equivalent Android keycodes and are translated to KeyEvent.KEYCODE_UNKNOWN. This CL allows KeyEvents with BTN_TRIGGER_HAPPY scancodes to be handled as if they were generic gamepad keys. Bug: 1208042 Change-Id: Ie3b6850206cce43ec0d4dbd25d325673c9a2cf11 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2907058 Commit-Queue: Matt Reynolds <mattreynolds@chromium.org> Reviewed-by: James Hollyer <jameshollyer@chromium.org> Cr-Commit-Position: refs/heads/master@{#885748}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
3bfae5c832
commit
76734f014a
device/gamepad/android
java
src
org
chromium
junit
src
org
chromium
device
gamepad
@@ -33,6 +33,11 @@ class GamepadDevice {
|
|||||||
// Allow for devices that have more buttons than the Standard Gamepad.
|
// Allow for devices that have more buttons than the Standard Gamepad.
|
||||||
static final int MAX_BUTTON_INDEX = CanonicalButtonIndex.COUNT;
|
static final int MAX_BUTTON_INDEX = CanonicalButtonIndex.COUNT;
|
||||||
|
|
||||||
|
// Minimum and maximum scancodes for extra gamepad buttons. Android does not assign KeyEvent
|
||||||
|
// keycodes for these buttons.
|
||||||
|
static final int MIN_BTN_TRIGGER_HAPPY = 0x2c0;
|
||||||
|
static final int MAX_BTN_TRIGGER_HAPPY = 0x2cf;
|
||||||
|
|
||||||
/** Keycodes which might be mapped by {@link GamepadMappings}. Keep sorted by keycode. */
|
/** Keycodes which might be mapped by {@link GamepadMappings}. Keep sorted by keycode. */
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int RELEVANT_KEYCODES[] = {
|
static final int RELEVANT_KEYCODES[] = {
|
||||||
@@ -76,7 +81,12 @@ class GamepadDevice {
|
|||||||
// should correspond to "down" or "right".
|
// should correspond to "down" or "right".
|
||||||
private final float[] mAxisValues = new float[CanonicalAxisIndex.COUNT];
|
private final float[] mAxisValues = new float[CanonicalAxisIndex.COUNT];
|
||||||
|
|
||||||
private final float[] mButtonsValues = new float[MAX_BUTTON_INDEX + 1];
|
// Array of values for all buttons of the gamepad. All button values must be
|
||||||
|
// linearly normalized to the range [0.0 .. 1.0]. 0.0 should correspond to
|
||||||
|
// a neutral, unpressed state and 1.0 should correspond to a pressed state.
|
||||||
|
// Allocate enough room for all Standard Gamepad buttons plus two extra
|
||||||
|
// buttons.
|
||||||
|
private final float[] mButtonsValues = new float[MAX_BUTTON_INDEX + 2];
|
||||||
|
|
||||||
// When the user agent recognizes the attached inputDevice, it is recommended
|
// When the user agent recognizes the attached inputDevice, it is recommended
|
||||||
// that it be remapped to a canonical ordering when possible. Devices that are
|
// that it be remapped to a canonical ordering when possible. Devices that are
|
||||||
@@ -221,9 +231,18 @@ class GamepadDevice {
|
|||||||
* @return True if the key event from the gamepad device has been consumed.
|
* @return True if the key event from the gamepad device has been consumed.
|
||||||
*/
|
*/
|
||||||
public boolean handleKeyEvent(KeyEvent event) {
|
public boolean handleKeyEvent(KeyEvent event) {
|
||||||
// Ignore event if it is not for standard gamepad key.
|
// Extra gamepad and joystick buttons use Linux scancodes starting from BTN_TRIGGER_HAPPY
|
||||||
if (!GamepadList.isGamepadEvent(event)) return false;
|
// but don't have specific Android keycodes and are mapped as KEYCODE_UNKNOWN. Handle the
|
||||||
|
// first 16 extra buttons as if they had KEYCODE_BUTTON_# keycodes.
|
||||||
int keyCode = event.getKeyCode();
|
int keyCode = event.getKeyCode();
|
||||||
|
int scanCode = event.getScanCode();
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_UNKNOWN && scanCode >= MIN_BTN_TRIGGER_HAPPY
|
||||||
|
&& scanCode <= MAX_BTN_TRIGGER_HAPPY) {
|
||||||
|
keyCode = KeyEvent.KEYCODE_BUTTON_1 + scanCode - MIN_BTN_TRIGGER_HAPPY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore the event if it is not for a gamepad key.
|
||||||
|
if (!GamepadList.isGamepadEvent(event)) return false;
|
||||||
assert keyCode < MAX_RAW_BUTTON_VALUES;
|
assert keyCode < MAX_RAW_BUTTON_VALUES;
|
||||||
// Button value 0.0 must mean fully unpressed, and 1.0 must mean fully pressed.
|
// Button value 0.0 must mean fully unpressed, and 1.0 must mean fully pressed.
|
||||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||||
|
@@ -289,8 +289,17 @@ public class GamepadList {
|
|||||||
case KeyEvent.KEYCODE_MEDIA_RECORD:
|
case KeyEvent.KEYCODE_MEDIA_RECORD:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return KeyEvent.isGamepadButton(keyCode);
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the scancode is in the BTN_TRIGGER_HAPPY range it is an extra gamepad button.
|
||||||
|
int scanCode = event.getScanCode();
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_UNKNOWN && scanCode >= GamepadDevice.MIN_BTN_TRIGGER_HAPPY
|
||||||
|
&& scanCode <= GamepadDevice.MAX_BTN_TRIGGER_HAPPY) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEvent.isGamepadButton(keyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@CalledByNative
|
@CalledByNative
|
||||||
|
@@ -53,6 +53,11 @@ abstract class GamepadMappings {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final int SNAKEBYTE_IDROIDCON_PRODUCT_ID = 0x8502;
|
static final int SNAKEBYTE_IDROIDCON_PRODUCT_ID = 0x8502;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final int GOOGLE_VENDOR_ID = 0x18d1;
|
||||||
|
@VisibleForTesting
|
||||||
|
static final int STADIA_CONTROLLER_PRODUCT_ID = 0x9400;
|
||||||
|
|
||||||
private static final float BUTTON_AXIS_DEADZONE = 0.01f;
|
private static final float BUTTON_AXIS_DEADZONE = 0.01f;
|
||||||
|
|
||||||
public static GamepadMappings getMappings(InputDevice device, int[] axes, BitSet buttons) {
|
public static GamepadMappings getMappings(InputDevice device, int[] axes, BitSet buttons) {
|
||||||
@@ -104,6 +109,9 @@ abstract class GamepadMappings {
|
|||||||
if (vendorId == BROADCOM_VENDOR_ID && productId == SNAKEBYTE_IDROIDCON_PRODUCT_ID) {
|
if (vendorId == BROADCOM_VENDOR_ID && productId == SNAKEBYTE_IDROIDCON_PRODUCT_ID) {
|
||||||
return new SnakebyteIDroidConMappings(axes);
|
return new SnakebyteIDroidConMappings(axes);
|
||||||
}
|
}
|
||||||
|
if (vendorId == GOOGLE_VENDOR_ID && productId == STADIA_CONTROLLER_PRODUCT_ID) {
|
||||||
|
return new StadiaControllerMappings();
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,6 +595,36 @@ abstract class GamepadMappings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class StadiaControllerMappings extends GamepadMappings {
|
||||||
|
private static final int BUTTON_INDEX_ASSISTANT = CanonicalButtonIndex.COUNT;
|
||||||
|
private static final int BUTTON_INDEX_CAPTURE = CanonicalButtonIndex.COUNT + 1;
|
||||||
|
/**
|
||||||
|
* Method for mapping Stadia Controller axis and button values to
|
||||||
|
* standard gamepad button and axes values.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void mapToStandardGamepad(
|
||||||
|
float[] mappedAxes, float[] mappedButtons, float[] rawAxes, float[] rawButtons) {
|
||||||
|
mapCommonXYABButtons(mappedButtons, rawButtons);
|
||||||
|
mapTriggerButtonsToTopShoulder(mappedButtons, rawButtons);
|
||||||
|
mapPedalAxesToBottomShoulder(mappedButtons, rawAxes);
|
||||||
|
mapCommonThumbstickButtons(mappedButtons, rawButtons);
|
||||||
|
mapCommonStartSelectMetaButtons(mappedButtons, rawButtons);
|
||||||
|
mapHatAxisToDpadButtons(mappedButtons, rawAxes);
|
||||||
|
mappedButtons[BUTTON_INDEX_ASSISTANT] = rawButtons[KeyEvent.KEYCODE_BUTTON_1];
|
||||||
|
mappedButtons[BUTTON_INDEX_CAPTURE] = rawButtons[KeyEvent.KEYCODE_BUTTON_2];
|
||||||
|
|
||||||
|
mapXYAxes(mappedAxes, rawAxes);
|
||||||
|
mapZAndRZAxesToRightStick(mappedAxes, rawAxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getButtonsLength() {
|
||||||
|
// Include the Assistant and Capture buttons.
|
||||||
|
return CanonicalButtonIndex.COUNT + 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class UnknownGamepadMappings extends GamepadMappings {
|
private static class UnknownGamepadMappings extends GamepadMappings {
|
||||||
private int mLeftTriggerAxis = -1;
|
private int mLeftTriggerAxis = -1;
|
||||||
private int mRightTriggerAxis = -1;
|
private int mRightTriggerAxis = -1;
|
||||||
|
@@ -49,7 +49,7 @@ public class GamepadMappingsTest {
|
|||||||
* Set bits indicate that we don't expect the axis at mMappedAxes[index] to be mapped.
|
* Set bits indicate that we don't expect the axis at mMappedAxes[index] to be mapped.
|
||||||
*/
|
*/
|
||||||
private BitSet mUnmappedAxes = new BitSet(CanonicalAxisIndex.COUNT);
|
private BitSet mUnmappedAxes = new BitSet(CanonicalAxisIndex.COUNT);
|
||||||
private float[] mMappedButtons = new float[CanonicalButtonIndex.COUNT];
|
private float[] mMappedButtons = new float[CanonicalButtonIndex.COUNT + 2];
|
||||||
private float[] mMappedAxes = new float[CanonicalAxisIndex.COUNT];
|
private float[] mMappedAxes = new float[CanonicalAxisIndex.COUNT];
|
||||||
private float[] mRawButtons = new float[GamepadDevice.MAX_RAW_BUTTON_VALUES];
|
private float[] mRawButtons = new float[GamepadDevice.MAX_RAW_BUTTON_VALUES];
|
||||||
private float[] mRawAxes = new float[GamepadDevice.MAX_RAW_AXIS_VALUES];
|
private float[] mRawAxes = new float[GamepadDevice.MAX_RAW_AXIS_VALUES];
|
||||||
@@ -560,6 +560,43 @@ public class GamepadMappingsTest {
|
|||||||
assertMapping(mappings);
|
assertMapping(mappings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Feature({"Gamepad"})
|
||||||
|
public void testStadiaControllerMappings() {
|
||||||
|
int[] axes = {
|
||||||
|
MotionEvent.AXIS_X,
|
||||||
|
MotionEvent.AXIS_Y,
|
||||||
|
MotionEvent.AXIS_Z,
|
||||||
|
MotionEvent.AXIS_RZ,
|
||||||
|
MotionEvent.AXIS_HAT_X,
|
||||||
|
MotionEvent.AXIS_HAT_Y,
|
||||||
|
MotionEvent.AXIS_GAS,
|
||||||
|
MotionEvent.AXIS_BRAKE,
|
||||||
|
};
|
||||||
|
GamepadMappings mappings = GamepadMappings.getMappings(GamepadMappings.GOOGLE_VENDOR_ID,
|
||||||
|
GamepadMappings.STADIA_CONTROLLER_PRODUCT_ID, axes);
|
||||||
|
mappings.mapToStandardGamepad(mMappedAxes, mMappedButtons, mRawAxes, mRawButtons);
|
||||||
|
|
||||||
|
assertMappedCommonXYABButtons();
|
||||||
|
assertMappedTriggerButtonsToTopShoulder();
|
||||||
|
assertMappedPedalAxesToBottomShoulder();
|
||||||
|
assertMappedCommonStartSelectMetaButtons();
|
||||||
|
assertMappedCommonThumbstickButtons();
|
||||||
|
assertMappedHatAxisToDpadButtons();
|
||||||
|
assertMappedXYAxes();
|
||||||
|
assertMappedZAndRZAxesToRightStick();
|
||||||
|
|
||||||
|
// The Assistant and Capture buttons should be mapped after the last
|
||||||
|
// Standard Gamepad button index.
|
||||||
|
Assert.assertEquals(mappings.getButtonsLength(), CanonicalButtonIndex.COUNT + 2);
|
||||||
|
Assert.assertEquals(mMappedButtons[CanonicalButtonIndex.COUNT],
|
||||||
|
mRawButtons[KeyEvent.KEYCODE_BUTTON_1], ERROR_TOLERANCE);
|
||||||
|
Assert.assertEquals(mMappedButtons[CanonicalButtonIndex.COUNT + 1],
|
||||||
|
mRawButtons[KeyEvent.KEYCODE_BUTTON_2], ERROR_TOLERANCE);
|
||||||
|
|
||||||
|
assertMapping(mappings);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the current gamepad mapping being tested matches the shield mappings.
|
* Asserts that the current gamepad mapping being tested matches the shield mappings.
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user