Skip to content

Commit

Permalink
gamepad: Add an Android mapping function for Stadia Controller
Browse files Browse the repository at this point in the history
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}
  • Loading branch information
nondebug authored and Chromium LUCI CQ committed May 22, 2021
1 parent 3bfae5c commit 76734f0
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -221,9 +231,18 @@ public void clearData() {
* @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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,17 @@ public static boolean isGamepadEvent(KeyEvent event) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -104,6 +109,9 @@ static GamepadMappings getMappings(int vendorId, int productId, int[] axes) {
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;
}

Expand Down Expand Up @@ -587,6 +595,36 @@ public void mapToStandardGamepad(float[] mappedAxes, float[] mappedButtons,
}
}

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -560,6 +560,43 @@ public void testIDroidConGamepadMappingsAnalog() {
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.
*/
Expand Down

0 comments on commit 76734f0

Please sign in to comment.