0

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:
Matt Reynolds
2021-05-22 04:26:19 +00:00
committed by Chromium LUCI CQ
parent 3bfae5c832
commit 76734f014a
4 changed files with 108 additions and 5 deletions
device/gamepad/android
java
junit
src
org
chromium

@ -33,6 +33,11 @@ class GamepadDevice {
// Allow for devices that have more buttons than the Standard Gamepad.
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. */
@VisibleForTesting
static final int RELEVANT_KEYCODES[] = {
@ -76,7 +81,12 @@ class GamepadDevice {
// should correspond to "down" or "right".
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
// 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.
*/
public boolean handleKeyEvent(KeyEvent event) {
// Ignore event if it is not for standard gamepad key.
if (!GamepadList.isGamepadEvent(event)) return false;
// Extra gamepad and joystick buttons use Linux scancodes starting from BTN_TRIGGER_HAPPY
// 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 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;
// Button value 0.0 must mean fully unpressed, and 1.0 must mean fully pressed.
if (event.getAction() == KeyEvent.ACTION_DOWN) {

@ -289,8 +289,17 @@ public class GamepadList {
case KeyEvent.KEYCODE_MEDIA_RECORD:
return true;
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

@ -53,6 +53,11 @@ abstract class GamepadMappings {
@VisibleForTesting
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;
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) {
return new SnakebyteIDroidConMappings(axes);
}
if (vendorId == GOOGLE_VENDOR_ID && productId == STADIA_CONTROLLER_PRODUCT_ID) {
return new StadiaControllerMappings();
}
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 int mLeftTriggerAxis = -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.
*/
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[] mRawButtons = new float[GamepadDevice.MAX_RAW_BUTTON_VALUES];
private float[] mRawAxes = new float[GamepadDevice.MAX_RAW_AXIS_VALUES];
@ -560,6 +560,43 @@ public class GamepadMappingsTest {
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.
*/