Add support for GamePad api in ITvRemoteServiceInput.

Gamepad-specific API is a separtate input path from standard "remote"
service. Specifically it adds:
  - openGamepad that creates a virtual input device with
  gamepad-specific suport
  - send gamepad keys
  - send gamepad axis updates, which support joysticks, analog triggers
  and HAT axis (as an alternative to DPAD buttons).

Bug: 150764186

Test: atest media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java

Test: flashed a ADT-3 device after the changes. Android TV Remote
      on my phone still worked in controlling the UI.

Merged-In: I49612fce5e74c4e00ca60c715c6c72954e73b7a3
Change-Id: I49612fce5e74c4e00ca60c715c6c72954e73b7a3
(cherry picked from commit 9b9f556af1)
This commit is contained in:
Andrei Litvin
2020-03-16 10:26:14 -04:00
parent 4e17751804
commit 3b92b9682d
9 changed files with 651 additions and 125 deletions

View File

@@ -0,0 +1,48 @@
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
type FULL
key BUTTON_A {
base: fallback DPAD_CENTER
}
key BUTTON_B {
base: fallback BACK
}
key BUTTON_X {
base: fallback DPAD_CENTER
}
key BUTTON_Y {
base: fallback BACK
}
key BUTTON_THUMBL {
base: fallback DPAD_CENTER
}
key BUTTON_THUMBR {
base: fallback DPAD_CENTER
}
key BUTTON_SELECT {
base: fallback MENU
}
key BUTTON_MODE {
base: fallback MENU
}

View File

@@ -0,0 +1,71 @@
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Keyboard map for the android virtual remote running as a gamepad
#
key 0x130 BUTTON_A
key 0x131 BUTTON_B
key 0x133 BUTTON_X
key 0x134 BUTTON_Y
key 0x136 BUTTON_L2
key 0x137 BUTTON_R2
key 0x138 BUTTON_L1
key 0x139 BUTTON_R1
key 0x13a BUTTON_SELECT
key 0x13b BUTTON_START
key 0x13c BUTTON_MODE
key 0x13d BUTTON_THUMBL
key 0x13e BUTTON_THUMBR
key 103 DPAD_UP
key 108 DPAD_DOWN
key 105 DPAD_LEFT
key 106 DPAD_RIGHT
# Generic usage buttons
key 0x2c0 BUTTON_1
key 0x2c1 BUTTON_2
key 0x2c2 BUTTON_3
key 0x2c3 BUTTON_4
key 0x2c4 BUTTON_5
key 0x2c5 BUTTON_6
key 0x2c6 BUTTON_7
key 0x2c7 BUTTON_8
key 0x2c8 BUTTON_9
key 0x2c9 BUTTON_10
key 0x2ca BUTTON_11
key 0x2cb BUTTON_12
key 0x2cc BUTTON_13
key 0x2cd BUTTON_14
key 0x2ce BUTTON_15
key 0x2cf BUTTON_16
# assistant buttons
key 0x246 VOICE_ASSIST
key 0x247 ASSIST
axis 0x00 X
axis 0x01 Y
axis 0x02 Z
axis 0x05 RZ
axis 0x09 RTRIGGER
axis 0x0a LTRIGGER
axis 0x10 HAT_X
axis 0x11 HAT_Y

View File

@@ -39,4 +39,10 @@ oneway interface ITvRemoteServiceInput {
void sendPointerUp(IBinder token, int pointerId);
@UnsupportedAppUsage
void sendPointerSync(IBinder token);
}
// API specific to gamepads. Close gamepads with closeInputBridge
void openGamepadBridge(IBinder token, String name);
void sendGamepadKeyDown(IBinder token, int keyCode);
void sendGamepadKeyUp(IBinder token, int keyCode);
void sendGamepadAxisValue(IBinder token, int axis, float value);
}

View File

@@ -16,6 +16,8 @@
package com.android.media.tv.remoteprovider;
import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.content.Context;
import android.media.tv.ITvRemoteProvider;
import android.media.tv.ITvRemoteServiceInput;
@@ -24,6 +26,7 @@ import android.os.RemoteException;
import android.util.Log;
import java.util.LinkedList;
import java.util.Objects;
/**
* Base class for emote providers implemented in unbundled service.
@@ -124,27 +127,75 @@ public abstract class TvRemoteProvider {
* @param maxPointers Maximum supported pointers
* @throws RuntimeException
*/
public void openRemoteInputBridge(IBinder token, String name, int width, int height,
int maxPointers) throws RuntimeException {
public void openRemoteInputBridge(
IBinder token, String name, int width, int height, int maxPointers)
throws RuntimeException {
final IBinder finalToken = Objects.requireNonNull(token);
final String finalName = Objects.requireNonNull(name);
synchronized (mOpenBridgeRunnables) {
if (mRemoteServiceInput == null) {
Log.d(TAG, "Delaying openRemoteInputBridge() for " + name);
Log.d(TAG, "Delaying openRemoteInputBridge() for " + finalName);
mOpenBridgeRunnables.add(() -> {
try {
mRemoteServiceInput.openInputBridge(
token, name, width, height, maxPointers);
Log.d(TAG, "Delayed openRemoteInputBridge() for " + name + ": success");
finalToken, finalName, width, height, maxPointers);
Log.d(TAG, "Delayed openRemoteInputBridge() for " + finalName
+ ": success");
} catch (RemoteException re) {
Log.e(TAG, "Delayed openRemoteInputBridge() for " + name + ": failure", re);
Log.e(TAG, "Delayed openRemoteInputBridge() for " + finalName
+ ": failure", re);
}
});
return;
}
}
try {
mRemoteServiceInput.openInputBridge(token, name, width, height, maxPointers);
Log.d(TAG, "openRemoteInputBridge() for " + name + ": success");
mRemoteServiceInput.openInputBridge(finalToken, finalName, width, height, maxPointers);
Log.d(TAG, "openRemoteInputBridge() for " + finalName + ": success");
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
/**
* Opens an input bridge as a gamepad device.
* Clients should pass in a token that can be used to match this request with a token that
* will be returned by {@link TvRemoteProvider#onInputBridgeConnected(IBinder token)}
* <p>
* The token should be used for subsequent calls.
* </p>
*
* @param token Identifier for this connection
* @param name Device name
* @throws RuntimeException
*
* @hide
*/
public void openGamepadBridge(@NonNull IBinder token, @NonNull String name)
throws RuntimeException {
final IBinder finalToken = Objects.requireNonNull(token);
final String finalName = Objects.requireNonNull(name);
synchronized (mOpenBridgeRunnables) {
if (mRemoteServiceInput == null) {
Log.d(TAG, "Delaying openGamepadBridge() for " + finalName);
mOpenBridgeRunnables.add(() -> {
try {
mRemoteServiceInput.openGamepadBridge(finalToken, finalName);
Log.d(TAG, "Delayed openGamepadBridge() for " + finalName + ": success");
} catch (RemoteException re) {
Log.e(TAG, "Delayed openGamepadBridge() for " + finalName + ": failure",
re);
}
});
return;
}
}
try {
mRemoteServiceInput.openGamepadBridge(token, finalName);
Log.d(TAG, "openGamepadBridge() for " + finalName + ": success");
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
@@ -157,6 +208,7 @@ public abstract class TvRemoteProvider {
* @throws RuntimeException
*/
public void closeInputBridge(IBinder token) throws RuntimeException {
Objects.requireNonNull(token);
try {
mRemoteServiceInput.closeInputBridge(token);
} catch (RemoteException re) {
@@ -173,6 +225,7 @@ public abstract class TvRemoteProvider {
* @throws RuntimeException
*/
public void clearInputBridge(IBinder token) throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) Log.d(TAG, "clearInputBridge() token " + token);
try {
mRemoteServiceInput.clearInputBridge(token);
@@ -190,6 +243,7 @@ public abstract class TvRemoteProvider {
* @throws RuntimeException
*/
public void sendTimestamp(IBinder token, long timestamp) throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) Log.d(TAG, "sendTimestamp() token: " + token +
", timestamp: " + timestamp);
try {
@@ -207,6 +261,7 @@ public abstract class TvRemoteProvider {
* @throws RuntimeException
*/
public void sendKeyUp(IBinder token, int keyCode) throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) Log.d(TAG, "sendKeyUp() token: " + token + ", keyCode: " + keyCode);
try {
mRemoteServiceInput.sendKeyUp(token, keyCode);
@@ -223,6 +278,7 @@ public abstract class TvRemoteProvider {
* @throws RuntimeException
*/
public void sendKeyDown(IBinder token, int keyCode) throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) Log.d(TAG, "sendKeyDown() token: " + token +
", keyCode: " + keyCode);
try {
@@ -241,6 +297,7 @@ public abstract class TvRemoteProvider {
* @throws RuntimeException
*/
public void sendPointerUp(IBinder token, int pointerId) throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) Log.d(TAG, "sendPointerUp() token: " + token +
", pointerId: " + pointerId);
try {
@@ -262,6 +319,7 @@ public abstract class TvRemoteProvider {
*/
public void sendPointerDown(IBinder token, int pointerId, int x, int y)
throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) Log.d(TAG, "sendPointerDown() token: " + token +
", pointerId: " + pointerId);
try {
@@ -278,6 +336,7 @@ public abstract class TvRemoteProvider {
* @throws RuntimeException
*/
public void sendPointerSync(IBinder token) throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) Log.d(TAG, "sendPointerSync() token: " + token);
try {
mRemoteServiceInput.sendPointerSync(token);
@@ -286,6 +345,94 @@ public abstract class TvRemoteProvider {
}
}
/**
* Send a notification that a gamepad key was pressed.
*
* Supported buttons are:
* <ul>
* <li> Right-side buttons: BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y
* <li> Digital Triggers and bumpers: BUTTON_L1, BUTTON_R1, BUTTON_L2, BUTTON_R2
* <li> Thumb buttons: BUTTON_THUMBL, BUTTON_THUMBR
* <li> DPad buttons: DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT
* <li> Gamepad buttons: BUTTON_SELECT, BUTTON_START, BUTTON_MODE
* <li> Generic buttons: BUTTON_1, BUTTON_2, ...., BUTTON16
* <li> Assistant: ASSIST, VOICE_ASSIST
* </ul>
*
* @param token identifier for the device
* @param keyCode the gamepad key that was pressed (like BUTTON_A)
*
* @hide
*/
public void sendGamepadKeyDown(@NonNull IBinder token, int keyCode) throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) {
Log.d(TAG, "sendGamepadKeyDown() token: " + token);
}
try {
mRemoteServiceInput.sendGamepadKeyDown(token, keyCode);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
/**
* Send a notification that a gamepad key was released.
*
* @see sendGamepadKeyDown for supported key codes.
*
* @param token identifier for the device
* @param keyCode the gamepad key that was pressed
*
* @hide
*/
public void sendGamepadKeyUp(@NonNull IBinder token, int keyCode) throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) {
Log.d(TAG, "sendGamepadKeyUp() token: " + token);
}
try {
mRemoteServiceInput.sendGamepadKeyUp(token, keyCode);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
/**
* Send a gamepad axis value.
*
* Supported axes:
* <li> Left Joystick: AXIS_X, AXIS_Y
* <li> Right Joystick: AXIS_Z, AXIS_RZ
* <li> Triggers: AXIS_LTRIGGER, AXIS_RTRIGGER
* <li> DPad: AXIS_HAT_X, AXIS_HAT_Y
*
* For non-trigger axes, the range of acceptable values is [-1, 1]. The trigger axes support
* values [0, 1].
*
* @param token identifier for the device
* @param axis MotionEvent axis
* @param value the value to send
*
* @hide
*/
public void sendGamepadAxisValue(
@NonNull IBinder token, int axis, @FloatRange(from = -1.0f, to = 1.0f) float value)
throws RuntimeException {
Objects.requireNonNull(token);
if (DEBUG_KEYS) {
Log.d(TAG, "sendGamepadAxisValue() token: " + token);
}
try {
mRemoteServiceInput.sendGamepadAxisValue(token, axis, value);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
private final class ProviderStub extends ITvRemoteProvider.Stub {
@Override
public void setRemoteServiceInputSink(ITvRemoteServiceInput tvServiceInput) {

View File

@@ -83,4 +83,52 @@ public class TvRemoteProviderTest extends AndroidTestCase {
assertTrue(tvProvider.verifyTokens());
}
@SmallTest
public void testOpenGamepadRemoteInputBridge() throws Exception {
Binder tokenA = new Binder();
Binder tokenB = new Binder();
Binder tokenC = new Binder();
class LocalTvRemoteProvider extends TvRemoteProvider {
private final ArrayList<IBinder> mTokens = new ArrayList<IBinder>();
LocalTvRemoteProvider(Context context) {
super(context);
}
@Override
public void onInputBridgeConnected(IBinder token) {
mTokens.add(token);
}
public boolean verifyTokens() {
return mTokens.size() == 3 && mTokens.contains(tokenA) && mTokens.contains(tokenB)
&& mTokens.contains(tokenC);
}
}
LocalTvRemoteProvider tvProvider = new LocalTvRemoteProvider(getContext());
ITvRemoteProvider binder = (ITvRemoteProvider) tvProvider.getBinder();
ITvRemoteServiceInput tvServiceInput = mock(ITvRemoteServiceInput.class);
doAnswer((i) -> {
binder.onInputBridgeConnected(i.getArgument(0));
return null;
})
.when(tvServiceInput)
.openGamepadBridge(any(), any());
tvProvider.openGamepadBridge(tokenA, "A");
tvProvider.openGamepadBridge(tokenB, "B");
binder.setRemoteServiceInputSink(tvServiceInput);
tvProvider.openGamepadBridge(tokenC, "C");
verify(tvServiceInput).openGamepadBridge(tokenA, "A");
verify(tvServiceInput).openGamepadBridge(tokenB, "B");
verify(tvServiceInput).openGamepadBridge(tokenC, "C");
verifyNoMoreInteractions(tvServiceInput);
assertTrue(tvProvider.verifyTokens());
}
}

View File

@@ -87,6 +87,47 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
}
}
@Override
public void openGamepadBridge(IBinder token, String name) throws RemoteException {
if (DEBUG) {
Slog.d(TAG, String.format("openGamepadBridge(), token: %s, name: %s", token, name));
}
synchronized (mLock) {
if (mBridgeMap.containsKey(token)) {
if (DEBUG) {
Slog.d(TAG, "InputBridge already exists");
}
} else {
final long idToken = Binder.clearCallingIdentity();
try {
mBridgeMap.put(token, UinputBridge.openGamepad(token, name));
token.linkToDeath(new IBinder.DeathRecipient() {
@Override
public void binderDied() {
closeInputBridge(token);
}
}, 0);
} catch (IOException e) {
Slog.e(TAG, "Cannot create device for " + name);
return;
} catch (RemoteException e) {
Slog.e(TAG, "Token is already dead");
closeInputBridge(token);
return;
} finally {
Binder.restoreCallingIdentity(idToken);
}
}
}
try {
mProvider.onInputBridgeConnected(token);
} catch (RemoteException e) {
Slog.e(TAG, "Failed remote call to onInputBridgeConnected");
}
}
@Override
public void closeInputBridge(IBinder token) {
if (DEBUG) {
@@ -96,6 +137,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.remove(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
@@ -117,6 +159,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.get(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
@@ -145,6 +188,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.get(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
@@ -166,6 +210,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.get(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
@@ -188,6 +233,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.get(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
@@ -209,6 +255,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.get(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
@@ -230,6 +277,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.get(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
@@ -241,4 +289,67 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
}
}
}
@Override
public void sendGamepadKeyUp(IBinder token, int keyIndex) {
if (DEBUG_KEYS) {
Slog.d(TAG, String.format("sendGamepadKeyUp(), token: %s", token));
}
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.get(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
final long idToken = Binder.clearCallingIdentity();
try {
inputBridge.sendGamepadKey(token, keyIndex, false);
} finally {
Binder.restoreCallingIdentity(idToken);
}
}
}
@Override
public void sendGamepadKeyDown(IBinder token, int keyCode) {
if (DEBUG_KEYS) {
Slog.d(TAG, String.format("sendGamepadKeyDown(), token: %s", token));
}
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.get(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
final long idToken = Binder.clearCallingIdentity();
try {
inputBridge.sendGamepadKey(token, keyCode, true);
} finally {
Binder.restoreCallingIdentity(idToken);
}
}
}
@Override
public void sendGamepadAxisValue(IBinder token, int axis, float value) {
if (DEBUG_KEYS) {
Slog.d(TAG, String.format("sendGamepadAxisValue(), token: %s", token));
}
synchronized (mLock) {
UinputBridge inputBridge = mBridgeMap.get(token);
if (inputBridge == null) {
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
return;
}
final long idToken = Binder.clearCallingIdentity();
try {
inputBridge.sendGamepadAxisValue(token, axis, value);
} finally {
Binder.restoreCallingIdentity(idToken);
}
}
}
}

View File

@@ -42,21 +42,27 @@ public final class UinputBridge {
/** Opens a gamepad - will support gamepad key and axis sending */
private static native long nativeGamepadOpen(String name, String uniqueId);
/** Marks the specified key up/down for a gamepad */
private static native void nativeSendGamepadKey(long ptr, int keyIndex, boolean down);
/**
* Marks the specified key up/down for a gamepad.
*
* @param keyCode - a code like BUTTON_MODE, BUTTON_A, BUTTON_B, ...
*/
private static native void nativeSendGamepadKey(long ptr, int keyCode, boolean down);
/**
* Gamepads pre-define the following axes:
* - Left joystick X, axis == ABS_X == 0, range [0, 254]
* - Left joystick Y, axis == ABS_Y == 1, range [0, 254]
* - Right joystick X, axis == ABS_RX == 3, range [0, 254]
* - Right joystick Y, axis == ABS_RY == 4, range [0, 254]
* - Left trigger, axis == ABS_Z == 2, range [0, 254]
* - Right trigger, axis == ABS_RZ == 5, range [0, 254]
* - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1]
* - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1]
* Send an axis value.
*
* Available axes are:
* <li> Left joystick: AXIS_X, AXIS_Y
* <li> Right joystick: AXIS_Z, AXIS_RZ
* <li> Analog triggers: AXIS_LTRIGGER, AXIS_RTRIGGER
* <li> DPad: AXIS_HAT_X, AXIS_HAT_Y
*
* @param axis is a MotionEvent.AXIS_* value.
* @param value is a value between -1 and 1 (inclusive)
*
*/
private static native void nativeSendGamepadAxisValue(long ptr, int axis, int value);
private static native void nativeSendGamepadAxisValue(long ptr, int axis, float value);
public UinputBridge(IBinder token, String name, int width, int height, int maxPointers)
throws IOException {
@@ -163,26 +169,19 @@ public final class UinputBridge {
* @param keyIndex - the index of the w3-spec key
* @param down - is the key pressed ?
*/
public void sendGamepadKey(IBinder token, int keyIndex, boolean down) {
public void sendGamepadKey(IBinder token, int keyCode, boolean down) {
if (isTokenValid(token)) {
nativeSendGamepadKey(mPtr, keyIndex, down);
nativeSendGamepadKey(mPtr, keyCode, down);
}
}
/** Send a gamepad axis value.
* - Left joystick X, axis == ABS_X == 0, range [0, 254]
* - Left joystick Y, axis == ABS_Y == 1, range [0, 254]
* - Right joystick X, axis == ABS_RX == 3, range [0, 254]
* - Right joystick Y, axis == ABS_RY == 4, range [0, 254]
* - Left trigger, axis == ABS_Z == 2, range [0, 254]
* - Right trigger, axis == ABS_RZ == 5, range [0, 254]
* - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1]
* - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1]
/**
* Send a gamepad axis value.
*
* @param axis is the axis index
* @param value is the value to set for that axis
* @param axis is the axis code (MotionEvent.AXIS_*)
* @param value is the value to set for that axis in [-1, 1]
*/
public void sendGamepadAxisValue(IBinder token, int axis, int value) {
public void sendGamepadAxisValue(IBinder token, int axis, float value) {
if (isTokenValid(token)) {
nativeSendGamepadAxisValue(mPtr, axis, value);
}

View File

@@ -1,77 +1,104 @@
#ifndef ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_
#define ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_
#include <android/input.h>
#include <android/keycodes.h>
#include <linux/input.h>
namespace android {
// Follows the W3 spec for gamepad buttons and their corresponding mapping into
// Linux keycodes. Note that gamepads are generally not very well standardized
// and various controllers will result in different buttons. This mapping tries
// to be reasonable.
// The constant array below defines a mapping between "Android" IDs (key code
// within events) and what is being sent through /dev/uinput.
//
// W3 Button spec: https://www.w3.org/TR/gamepad/#remapping
// The translation back from uinput key codes into android key codes is done through
// the corresponding key layout files. This file and
//
// Standard gamepad keycodes are added plus 2 additional buttons (e.g. Stadia
// has "Assistant" and "Share", PS4 has the touchpad button).
// data/keyboards/Vendor_18d1_Product_0200.kl
//
// To generate this list, PS4, XBox, Stadia and Nintendo Switch Pro were tested.
static const int GAMEPAD_KEY_CODES[19] = {
// Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar
BTN_A, // "South", A, GAMEPAD and SOUTH have the same constant
BTN_B, // "East", BTN_B, BTN_EAST have the same constant
BTN_X, // "West", Note that this maps to X and NORTH in constants
BTN_Y, // "North", Note that this maps to Y and WEST in constants
// MUST be kept in sync.
//
// see https://source.android.com/devices/input/key-layout-files for documentation.
BTN_TL, // "Left Bumper" / "L1" - Nintendo sends BTN_WEST instead
BTN_TR, // "Right Bumper" / "R1" - Nintendo sends BTN_Z instead
// For triggers, gamepads vary:
// - Stadia sends analog values over ABS_GAS/ABS_BRAKE and sends
// TriggerHappy3/4 as digital presses
// - PS4 and Xbox send analog values as ABS_Z/ABS_RZ
// - Nintendo Pro sends BTN_TL/BTN_TR (since bumpers behave differently)
// As placeholders we chose the stadia trigger-happy values since TL/TR are
// sent for bumper button presses
BTN_TRIGGER_HAPPY4, // "Left Trigger" / "L2"
BTN_TRIGGER_HAPPY3, // "Right Trigger" / "R2"
BTN_SELECT, // "Select/Back". Often "options" or similar
BTN_START, // "Start/forward". Often "hamburger" icon
BTN_THUMBL, // "Left Joystick Pressed"
BTN_THUMBR, // "Right Joystick Pressed"
// For DPads, gamepads generally only send axis changes
// on ABS_HAT0X and ABS_HAT0Y.
KEY_UP, // "Digital Pad up"
KEY_DOWN, // "Digital Pad down"
KEY_LEFT, // "Digital Pad left"
KEY_RIGHT, // "Digital Pad right"
BTN_MODE, // "Main button" (Stadia/PS/XBOX/Home)
BTN_TRIGGER_HAPPY1, // Extra button: "Assistant" for Stadia
BTN_TRIGGER_HAPPY2, // Extra button: "Share" for Stadia
// Defines axis mapping information between android and
// uinput axis.
struct GamepadKey {
int32_t androidKeyCode;
int linuxUinputKeyCode;
};
// Defines information for an axis.
struct Axis {
int number;
int rangeMin;
int rangeMax;
static const GamepadKey GAMEPAD_KEYS[] = {
// Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar
{AKEYCODE_BUTTON_A, BTN_A},
{AKEYCODE_BUTTON_B, BTN_B},
{AKEYCODE_BUTTON_X, BTN_X},
{AKEYCODE_BUTTON_Y, BTN_Y},
// Bumper buttons and digital triggers. Triggers generally have
// both analog versions (GAS and BRAKE output) and digital ones
{AKEYCODE_BUTTON_L1, BTN_TL2},
{AKEYCODE_BUTTON_L2, BTN_TL},
{AKEYCODE_BUTTON_R1, BTN_TR2},
{AKEYCODE_BUTTON_R2, BTN_TR},
// general actions for controllers
{AKEYCODE_BUTTON_SELECT, BTN_SELECT}, // Options or "..."
{AKEYCODE_BUTTON_START, BTN_START}, // Menu/Hamburger menu
{AKEYCODE_BUTTON_MODE, BTN_MODE}, // "main" button
// Pressing on the joyticks themselves
{AKEYCODE_BUTTON_THUMBL, BTN_THUMBL},
{AKEYCODE_BUTTON_THUMBR, BTN_THUMBR},
// DPAD digital keys. HAT axis events are generally also sent.
{AKEYCODE_DPAD_UP, KEY_UP},
{AKEYCODE_DPAD_DOWN, KEY_DOWN},
{AKEYCODE_DPAD_LEFT, KEY_LEFT},
{AKEYCODE_DPAD_RIGHT, KEY_RIGHT},
// "Extra" controller buttons: some devices have "share" and "assistant"
{AKEYCODE_BUTTON_1, BTN_TRIGGER_HAPPY1},
{AKEYCODE_BUTTON_2, BTN_TRIGGER_HAPPY2},
{AKEYCODE_BUTTON_3, BTN_TRIGGER_HAPPY3},
{AKEYCODE_BUTTON_4, BTN_TRIGGER_HAPPY4},
{AKEYCODE_BUTTON_5, BTN_TRIGGER_HAPPY5},
{AKEYCODE_BUTTON_6, BTN_TRIGGER_HAPPY6},
{AKEYCODE_BUTTON_7, BTN_TRIGGER_HAPPY7},
{AKEYCODE_BUTTON_8, BTN_TRIGGER_HAPPY8},
{AKEYCODE_BUTTON_9, BTN_TRIGGER_HAPPY9},
{AKEYCODE_BUTTON_10, BTN_TRIGGER_HAPPY10},
{AKEYCODE_BUTTON_11, BTN_TRIGGER_HAPPY11},
{AKEYCODE_BUTTON_12, BTN_TRIGGER_HAPPY12},
{AKEYCODE_BUTTON_13, BTN_TRIGGER_HAPPY13},
{AKEYCODE_BUTTON_14, BTN_TRIGGER_HAPPY14},
{AKEYCODE_BUTTON_15, BTN_TRIGGER_HAPPY15},
{AKEYCODE_BUTTON_16, BTN_TRIGGER_HAPPY16},
// Assignment to support global assistant for devices that support it.
{AKEYCODE_ASSIST, KEY_ASSISTANT},
{AKEYCODE_VOICE_ASSIST, KEY_VOICECOMMAND},
};
// Defines axis mapping information between android and
// uinput axis.
struct GamepadAxis {
int32_t androidAxis;
float androidRangeMin;
float androidRangeMax;
int linuxUinputAxis;
int linuxUinputRangeMin;
int linuxUinputRangeMax;
};
// List of all axes supported by a gamepad
static const Axis GAMEPAD_AXES[] = {
{ABS_X, 0, 254}, // Left joystick X
{ABS_Y, 0, 254}, // Left joystick Y
{ABS_RX, 0, 254}, // Right joystick X
{ABS_RY, 0, 254}, // Right joystick Y
{ABS_Z, 0, 254}, // Left trigger
{ABS_RZ, 0, 254}, // Right trigger
{ABS_HAT0X, -1, 1}, // DPad X
{ABS_HAT0Y, -1, 1}, // DPad Y
static const GamepadAxis GAMEPAD_AXES[] = {
{AMOTION_EVENT_AXIS_X, -1, 1, ABS_X, 0, 254}, // Left joystick X
{AMOTION_EVENT_AXIS_Y, -1, 1, ABS_Y, 0, 254}, // Left joystick Y
{AMOTION_EVENT_AXIS_Z, -1, 1, ABS_Z, 0, 254}, // Right joystick X
{AMOTION_EVENT_AXIS_RZ, -1, 1, ABS_RZ, 0, 254}, // Right joystick Y
{AMOTION_EVENT_AXIS_LTRIGGER, 0, 1, ABS_GAS, 0, 254}, // Left trigger
{AMOTION_EVENT_AXIS_RTRIGGER, 0, 1, ABS_BRAKE, 0, 254}, // Right trigger
{AMOTION_EVENT_AXIS_HAT_X, -1, 1, ABS_HAT0X, -1, 1}, // DPad X
{AMOTION_EVENT_AXIS_HAT_Y, -1, 1, ABS_HAT0Y, -1, 1}, // DPad Y
};
} // namespace android

View File

@@ -31,27 +31,38 @@
#include <utils/String8.h>
#include <ctype.h>
#include <linux/input.h>
#include <unistd.h>
#include <sys/time.h>
#include <time.h>
#include <stdint.h>
#include <map>
#include <fcntl.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include <signal.h>
#include <stdint.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <unordered_map>
#define SLOT_UNKNOWN -1
namespace android {
static std::map<int32_t,int> keysMap;
static std::map<int32_t,int32_t> slotsMap;
#define GOOGLE_VENDOR_ID 0x18d1
#define GOOGLE_VIRTUAL_REMOTE_PRODUCT_ID 0x0100
#define GOOGLE_VIRTUAL_GAMEPAD_PROUCT_ID 0x0200
static std::unordered_map<int32_t, int> keysMap;
static std::unordered_map<int32_t, int32_t> slotsMap;
static BitSet32 mtSlots;
// Maps android key code to linux key code.
static std::unordered_map<int32_t, int> gamepadAndroidToLinuxKeyMap;
// Maps an android gamepad axis to the index within the GAMEPAD_AXES array.
static std::unordered_map<int32_t, int> gamepadAndroidAxisToIndexMap;
static void initKeysMap() {
if (keysMap.empty()) {
for (size_t i = 0; i < NELEM(KEYS); i++) {
@@ -60,16 +71,49 @@ static void initKeysMap() {
}
}
static void initGamepadKeyMap() {
if (gamepadAndroidToLinuxKeyMap.empty()) {
for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) {
gamepadAndroidToLinuxKeyMap[GAMEPAD_KEYS[i].androidKeyCode] =
GAMEPAD_KEYS[i].linuxUinputKeyCode;
}
}
if (gamepadAndroidAxisToIndexMap.empty()) {
for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
gamepadAndroidAxisToIndexMap[GAMEPAD_AXES[i].androidAxis] = i;
}
}
}
static int32_t getLinuxKeyCode(int32_t androidKeyCode) {
std::map<int,int>::iterator it = keysMap.find(androidKeyCode);
std::unordered_map<int, int>::iterator it = keysMap.find(androidKeyCode);
if (it != keysMap.end()) {
return it->second;
}
return KEY_UNKNOWN;
}
static int getGamepadkeyCode(int32_t androidKeyCode) {
std::unordered_map<int32_t, int>::iterator it =
gamepadAndroidToLinuxKeyMap.find(androidKeyCode);
if (it != gamepadAndroidToLinuxKeyMap.end()) {
return it->second;
}
return KEY_UNKNOWN;
}
static const GamepadAxis* getGamepadAxis(int32_t androidAxisCode) {
std::unordered_map<int32_t, int>::iterator it =
gamepadAndroidAxisToIndexMap.find(androidAxisCode);
if (it == gamepadAndroidToLinuxKeyMap.end()) {
return nullptr;
}
return &GAMEPAD_AXES[it->second];
}
static int findSlot(int32_t pointerId) {
std::map<int,int>::iterator it = slotsMap.find(pointerId);
std::unordered_map<int, int>::iterator it = slotsMap.find(pointerId);
if (it != slotsMap.end()) {
return it->second;
}
@@ -107,7 +151,7 @@ public:
// Open /dev/uinput and prepare to register
// the device with the given name and unique Id
bool Open(const char* name, const char* uniqueId);
bool Open(const char* name, const char* uniqueId, uint16_t product);
// Checks if the current file descriptor is valid
bool IsValid() const { return mFd != kInvalidFileDescriptor; }
@@ -141,7 +185,7 @@ int UInputDescriptor::Detach() {
return fd;
}
bool UInputDescriptor::Open(const char* name, const char* uniqueId) {
bool UInputDescriptor::Open(const char* name, const char* uniqueId, uint16_t product) {
if (IsValid()) {
ALOGE("UInput device already open");
return false;
@@ -161,6 +205,8 @@ bool UInputDescriptor::Open(const char* name, const char* uniqueId) {
strlcpy(mUinputDescriptor.name, name, UINPUT_MAX_NAME_SIZE);
mUinputDescriptor.id.version = 1;
mUinputDescriptor.id.bustype = BUS_VIRTUAL;
mUinputDescriptor.id.vendor = GOOGLE_VENDOR_ID;
mUinputDescriptor.id.product = product;
// All UInput devices we use process keys
ioctl(mFd, UI_SET_EVBIT, EV_KEY);
@@ -258,7 +304,7 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId,
initKeysMap();
UInputDescriptor descriptor;
if (!descriptor.Open(name, uniqueId)) {
if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_REMOTE_PRODUCT_ID)) {
return nullptr;
}
@@ -277,21 +323,24 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId,
NativeConnection* NativeConnection::openGamepad(const char* name, const char* uniqueId) {
ALOGI("Registering uinput device %s: gamepad", name);
initGamepadKeyMap();
UInputDescriptor descriptor;
if (!descriptor.Open(name, uniqueId)) {
if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_GAMEPAD_PROUCT_ID)) {
return nullptr;
}
// set the keys mapped for gamepads
for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) {
descriptor.EnableKey(GAMEPAD_KEY_CODES[i]);
for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) {
descriptor.EnableKey(GAMEPAD_KEYS[i].linuxUinputKeyCode);
}
// define the axes that are required
descriptor.EnableAxesEvents();
for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
const Axis& axis = GAMEPAD_AXES[i];
descriptor.EnableAxis(axis.number, axis.rangeMin, axis.rangeMax);
const GamepadAxis& axis = GAMEPAD_AXES[i];
descriptor.EnableAxis(axis.linuxUinputAxis, axis.linuxUinputRangeMin,
axis.linuxUinputRangeMax);
}
if (!descriptor.Create()) {
@@ -350,7 +399,7 @@ static void nativeSendKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jb
}
}
static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyIndex,
static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode,
jboolean down) {
NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
@@ -359,16 +408,16 @@ static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyI
return;
}
if ((keyIndex < 0) || (keyIndex >= NELEM(GAMEPAD_KEY_CODES))) {
ALOGE("Invalid gamepad key index: %d", keyIndex);
int linuxKeyCode = getGamepadkeyCode(keyCode);
if (linuxKeyCode == KEY_UNKNOWN) {
ALOGE("Gamepad: received an unknown keycode of %d.", keyCode);
return;
}
connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[keyIndex], down ? 1 : 0);
connection->sendEvent(EV_KEY, linuxKeyCode, down ? 1 : 0);
}
static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jint axis,
jint value) {
jfloat value) {
NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
if (!connection->IsGamepad()) {
@@ -376,7 +425,25 @@ static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jin
return;
}
connection->sendEvent(EV_ABS, axis, value);
const GamepadAxis* axisInfo = getGamepadAxis(axis);
if (axisInfo == nullptr) {
ALOGE("Invalid axis: %d", axis);
return;
}
if (value > axisInfo->androidRangeMax) {
value = axisInfo->androidRangeMax;
} else if (value < axisInfo->androidRangeMin) {
value = axisInfo->androidRangeMin;
}
// Converts the android range into the device range
float movementPercent = (value - axisInfo->androidRangeMin) /
(axisInfo->androidRangeMax - axisInfo->androidRangeMin);
int axisRawValue = axisInfo->linuxUinputRangeMin +
movementPercent * (axisInfo->linuxUinputRangeMax - axisInfo->linuxUinputRangeMin);
connection->sendEvent(EV_ABS, axisInfo->linuxUinputAxis, axisRawValue);
}
static void nativeSendPointerDown(JNIEnv* env, jclass clazz, jlong ptr,
@@ -441,18 +508,20 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) {
}
}
} else {
for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) {
connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[i], 0);
for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) {
connection->sendEvent(EV_KEY, GAMEPAD_KEYS[i].linuxUinputKeyCode, 0);
}
for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
const Axis& axis = GAMEPAD_AXES[i];
if ((axis.number == ABS_Z) || (axis.number == ABS_RZ)) {
const GamepadAxis& axis = GAMEPAD_AXES[i];
if ((axis.linuxUinputAxis == ABS_Z) || (axis.linuxUinputAxis == ABS_RZ)) {
// Mark triggers unpressed
connection->sendEvent(EV_ABS, axis.number, 0);
connection->sendEvent(EV_ABS, axis.linuxUinputAxis, axis.linuxUinputRangeMin);
} else {
// Joysticks and dpad rests on center
connection->sendEvent(EV_ABS, axis.number, (axis.rangeMin + axis.rangeMax) / 2);
connection->sendEvent(EV_ABS, axis.linuxUinputAxis,
(axis.linuxUinputRangeMin + axis.linuxUinputRangeMax) / 2);
}
}
}
@@ -475,7 +544,7 @@ static JNINativeMethod gUinputBridgeMethods[] = {
{"nativeClear", "(J)V", (void*)nativeClear},
{"nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync},
{"nativeSendGamepadKey", "(JIZ)V", (void*)nativeSendGamepadKey},
{"nativeSendGamepadAxisValue", "(JII)V", (void*)nativeSendGamepadAxisValue},
{"nativeSendGamepadAxisValue", "(JIF)V", (void*)nativeSendGamepadAxisValue},
};
int register_android_server_tv_TvUinputBridge(JNIEnv* env) {