Merge "Add support for GamePad api in ITvRemoteServiceInput." into rvc-dev am: 7dd4f1afb7 am: f73ba98b10
Change-Id: I98275b0a423d1368d33c47f0a144c87709107314
This commit is contained in:
48
data/keyboards/Vendor_18d1_Product_0200.kcm
Normal file
48
data/keyboards/Vendor_18d1_Product_0200.kcm
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
71
data/keyboards/Vendor_18d1_Product_0200.kl
Normal file
71
data/keyboards/Vendor_18d1_Product_0200.kl
Normal 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
|
||||||
|
|
||||||
@@ -39,4 +39,10 @@ oneway interface ITvRemoteServiceInput {
|
|||||||
void sendPointerUp(IBinder token, int pointerId);
|
void sendPointerUp(IBinder token, int pointerId);
|
||||||
@UnsupportedAppUsage
|
@UnsupportedAppUsage
|
||||||
void sendPointerSync(IBinder token);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package com.android.media.tv.remoteprovider;
|
package com.android.media.tv.remoteprovider;
|
||||||
|
|
||||||
|
import android.annotation.FloatRange;
|
||||||
|
import android.annotation.NonNull;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.tv.ITvRemoteProvider;
|
import android.media.tv.ITvRemoteProvider;
|
||||||
import android.media.tv.ITvRemoteServiceInput;
|
import android.media.tv.ITvRemoteServiceInput;
|
||||||
@@ -24,6 +26,7 @@ import android.os.RemoteException;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for emote providers implemented in unbundled service.
|
* Base class for emote providers implemented in unbundled service.
|
||||||
@@ -124,27 +127,75 @@ public abstract class TvRemoteProvider {
|
|||||||
* @param maxPointers Maximum supported pointers
|
* @param maxPointers Maximum supported pointers
|
||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public void openRemoteInputBridge(IBinder token, String name, int width, int height,
|
public void openRemoteInputBridge(
|
||||||
int maxPointers) throws RuntimeException {
|
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) {
|
synchronized (mOpenBridgeRunnables) {
|
||||||
if (mRemoteServiceInput == null) {
|
if (mRemoteServiceInput == null) {
|
||||||
Log.d(TAG, "Delaying openRemoteInputBridge() for " + name);
|
Log.d(TAG, "Delaying openRemoteInputBridge() for " + finalName);
|
||||||
|
|
||||||
mOpenBridgeRunnables.add(() -> {
|
mOpenBridgeRunnables.add(() -> {
|
||||||
try {
|
try {
|
||||||
mRemoteServiceInput.openInputBridge(
|
mRemoteServiceInput.openInputBridge(
|
||||||
token, name, width, height, maxPointers);
|
finalToken, finalName, width, height, maxPointers);
|
||||||
Log.d(TAG, "Delayed openRemoteInputBridge() for " + name + ": success");
|
Log.d(TAG, "Delayed openRemoteInputBridge() for " + finalName
|
||||||
|
+ ": success");
|
||||||
} catch (RemoteException re) {
|
} catch (RemoteException re) {
|
||||||
Log.e(TAG, "Delayed openRemoteInputBridge() for " + name + ": failure", re);
|
Log.e(TAG, "Delayed openRemoteInputBridge() for " + finalName
|
||||||
|
+ ": failure", re);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
mRemoteServiceInput.openInputBridge(token, name, width, height, maxPointers);
|
mRemoteServiceInput.openInputBridge(finalToken, finalName, width, height, maxPointers);
|
||||||
Log.d(TAG, "openRemoteInputBridge() for " + name + ": success");
|
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) {
|
} catch (RemoteException re) {
|
||||||
throw re.rethrowFromSystemServer();
|
throw re.rethrowFromSystemServer();
|
||||||
}
|
}
|
||||||
@@ -157,6 +208,7 @@ public abstract class TvRemoteProvider {
|
|||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public void closeInputBridge(IBinder token) throws RuntimeException {
|
public void closeInputBridge(IBinder token) throws RuntimeException {
|
||||||
|
Objects.requireNonNull(token);
|
||||||
try {
|
try {
|
||||||
mRemoteServiceInput.closeInputBridge(token);
|
mRemoteServiceInput.closeInputBridge(token);
|
||||||
} catch (RemoteException re) {
|
} catch (RemoteException re) {
|
||||||
@@ -173,6 +225,7 @@ public abstract class TvRemoteProvider {
|
|||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public void clearInputBridge(IBinder token) throws RuntimeException {
|
public void clearInputBridge(IBinder token) throws RuntimeException {
|
||||||
|
Objects.requireNonNull(token);
|
||||||
if (DEBUG_KEYS) Log.d(TAG, "clearInputBridge() token " + token);
|
if (DEBUG_KEYS) Log.d(TAG, "clearInputBridge() token " + token);
|
||||||
try {
|
try {
|
||||||
mRemoteServiceInput.clearInputBridge(token);
|
mRemoteServiceInput.clearInputBridge(token);
|
||||||
@@ -190,6 +243,7 @@ public abstract class TvRemoteProvider {
|
|||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public void sendTimestamp(IBinder token, long timestamp) throws RuntimeException {
|
public void sendTimestamp(IBinder token, long timestamp) throws RuntimeException {
|
||||||
|
Objects.requireNonNull(token);
|
||||||
if (DEBUG_KEYS) Log.d(TAG, "sendTimestamp() token: " + token +
|
if (DEBUG_KEYS) Log.d(TAG, "sendTimestamp() token: " + token +
|
||||||
", timestamp: " + timestamp);
|
", timestamp: " + timestamp);
|
||||||
try {
|
try {
|
||||||
@@ -207,6 +261,7 @@ public abstract class TvRemoteProvider {
|
|||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public void sendKeyUp(IBinder token, int keyCode) 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);
|
if (DEBUG_KEYS) Log.d(TAG, "sendKeyUp() token: " + token + ", keyCode: " + keyCode);
|
||||||
try {
|
try {
|
||||||
mRemoteServiceInput.sendKeyUp(token, keyCode);
|
mRemoteServiceInput.sendKeyUp(token, keyCode);
|
||||||
@@ -223,6 +278,7 @@ public abstract class TvRemoteProvider {
|
|||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public void sendKeyDown(IBinder token, int keyCode) throws RuntimeException {
|
public void sendKeyDown(IBinder token, int keyCode) throws RuntimeException {
|
||||||
|
Objects.requireNonNull(token);
|
||||||
if (DEBUG_KEYS) Log.d(TAG, "sendKeyDown() token: " + token +
|
if (DEBUG_KEYS) Log.d(TAG, "sendKeyDown() token: " + token +
|
||||||
", keyCode: " + keyCode);
|
", keyCode: " + keyCode);
|
||||||
try {
|
try {
|
||||||
@@ -241,6 +297,7 @@ public abstract class TvRemoteProvider {
|
|||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public void sendPointerUp(IBinder token, int pointerId) throws RuntimeException {
|
public void sendPointerUp(IBinder token, int pointerId) throws RuntimeException {
|
||||||
|
Objects.requireNonNull(token);
|
||||||
if (DEBUG_KEYS) Log.d(TAG, "sendPointerUp() token: " + token +
|
if (DEBUG_KEYS) Log.d(TAG, "sendPointerUp() token: " + token +
|
||||||
", pointerId: " + pointerId);
|
", pointerId: " + pointerId);
|
||||||
try {
|
try {
|
||||||
@@ -262,6 +319,7 @@ public abstract class TvRemoteProvider {
|
|||||||
*/
|
*/
|
||||||
public void sendPointerDown(IBinder token, int pointerId, int x, int y)
|
public void sendPointerDown(IBinder token, int pointerId, int x, int y)
|
||||||
throws RuntimeException {
|
throws RuntimeException {
|
||||||
|
Objects.requireNonNull(token);
|
||||||
if (DEBUG_KEYS) Log.d(TAG, "sendPointerDown() token: " + token +
|
if (DEBUG_KEYS) Log.d(TAG, "sendPointerDown() token: " + token +
|
||||||
", pointerId: " + pointerId);
|
", pointerId: " + pointerId);
|
||||||
try {
|
try {
|
||||||
@@ -278,6 +336,7 @@ public abstract class TvRemoteProvider {
|
|||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public void sendPointerSync(IBinder token) throws RuntimeException {
|
public void sendPointerSync(IBinder token) throws RuntimeException {
|
||||||
|
Objects.requireNonNull(token);
|
||||||
if (DEBUG_KEYS) Log.d(TAG, "sendPointerSync() token: " + token);
|
if (DEBUG_KEYS) Log.d(TAG, "sendPointerSync() token: " + token);
|
||||||
try {
|
try {
|
||||||
mRemoteServiceInput.sendPointerSync(token);
|
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 {
|
private final class ProviderStub extends ITvRemoteProvider.Stub {
|
||||||
@Override
|
@Override
|
||||||
public void setRemoteServiceInputSink(ITvRemoteServiceInput tvServiceInput) {
|
public void setRemoteServiceInputSink(ITvRemoteServiceInput tvServiceInput) {
|
||||||
|
|||||||
@@ -83,4 +83,52 @@ public class TvRemoteProviderTest extends AndroidTestCase {
|
|||||||
|
|
||||||
assertTrue(tvProvider.verifyTokens());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
@Override
|
||||||
public void closeInputBridge(IBinder token) {
|
public void closeInputBridge(IBinder token) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@@ -96,6 +137,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
|
|||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
UinputBridge inputBridge = mBridgeMap.remove(token);
|
UinputBridge inputBridge = mBridgeMap.remove(token);
|
||||||
if (inputBridge == null) {
|
if (inputBridge == null) {
|
||||||
|
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +159,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
|
|||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
UinputBridge inputBridge = mBridgeMap.get(token);
|
UinputBridge inputBridge = mBridgeMap.get(token);
|
||||||
if (inputBridge == null) {
|
if (inputBridge == null) {
|
||||||
|
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +188,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
|
|||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
UinputBridge inputBridge = mBridgeMap.get(token);
|
UinputBridge inputBridge = mBridgeMap.get(token);
|
||||||
if (inputBridge == null) {
|
if (inputBridge == null) {
|
||||||
|
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +210,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
|
|||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
UinputBridge inputBridge = mBridgeMap.get(token);
|
UinputBridge inputBridge = mBridgeMap.get(token);
|
||||||
if (inputBridge == null) {
|
if (inputBridge == null) {
|
||||||
|
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +233,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
|
|||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
UinputBridge inputBridge = mBridgeMap.get(token);
|
UinputBridge inputBridge = mBridgeMap.get(token);
|
||||||
if (inputBridge == null) {
|
if (inputBridge == null) {
|
||||||
|
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +255,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
|
|||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
UinputBridge inputBridge = mBridgeMap.get(token);
|
UinputBridge inputBridge = mBridgeMap.get(token);
|
||||||
if (inputBridge == null) {
|
if (inputBridge == null) {
|
||||||
|
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +277,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub {
|
|||||||
synchronized (mLock) {
|
synchronized (mLock) {
|
||||||
UinputBridge inputBridge = mBridgeMap.get(token);
|
UinputBridge inputBridge = mBridgeMap.get(token);
|
||||||
if (inputBridge == null) {
|
if (inputBridge == null) {
|
||||||
|
Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
|
||||||
return;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,21 +42,27 @@ public final class UinputBridge {
|
|||||||
/** Opens a gamepad - will support gamepad key and axis sending */
|
/** Opens a gamepad - will support gamepad key and axis sending */
|
||||||
private static native long nativeGamepadOpen(String name, String uniqueId);
|
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:
|
* Send an axis value.
|
||||||
* - Left joystick X, axis == ABS_X == 0, range [0, 254]
|
*
|
||||||
* - Left joystick Y, axis == ABS_Y == 1, range [0, 254]
|
* Available axes are:
|
||||||
* - Right joystick X, axis == ABS_RX == 3, range [0, 254]
|
* <li> Left joystick: AXIS_X, AXIS_Y
|
||||||
* - Right joystick Y, axis == ABS_RY == 4, range [0, 254]
|
* <li> Right joystick: AXIS_Z, AXIS_RZ
|
||||||
* - Left trigger, axis == ABS_Z == 2, range [0, 254]
|
* <li> Analog triggers: AXIS_LTRIGGER, AXIS_RTRIGGER
|
||||||
* - Right trigger, axis == ABS_RZ == 5, range [0, 254]
|
* <li> DPad: AXIS_HAT_X, AXIS_HAT_Y
|
||||||
* - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1]
|
*
|
||||||
* - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1]
|
* @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)
|
public UinputBridge(IBinder token, String name, int width, int height, int maxPointers)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@@ -163,26 +169,19 @@ public final class UinputBridge {
|
|||||||
* @param keyIndex - the index of the w3-spec key
|
* @param keyIndex - the index of the w3-spec key
|
||||||
* @param down - is the key pressed ?
|
* @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)) {
|
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]
|
* Send a gamepad axis value.
|
||||||
* - 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]
|
|
||||||
*
|
*
|
||||||
* @param axis is the axis index
|
* @param axis is the axis code (MotionEvent.AXIS_*)
|
||||||
* @param value is the value to set for that 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)) {
|
if (isTokenValid(token)) {
|
||||||
nativeSendGamepadAxisValue(mPtr, axis, value);
|
nativeSendGamepadAxisValue(mPtr, axis, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,104 @@
|
|||||||
#ifndef ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_
|
#ifndef ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_
|
||||||
#define 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>
|
#include <linux/input.h>
|
||||||
|
|
||||||
namespace android {
|
namespace android {
|
||||||
|
|
||||||
// Follows the W3 spec for gamepad buttons and their corresponding mapping into
|
// The constant array below defines a mapping between "Android" IDs (key code
|
||||||
// Linux keycodes. Note that gamepads are generally not very well standardized
|
// within events) and what is being sent through /dev/uinput.
|
||||||
// and various controllers will result in different buttons. This mapping tries
|
|
||||||
// to be reasonable.
|
|
||||||
//
|
//
|
||||||
// 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
|
// data/keyboards/Vendor_18d1_Product_0200.kl
|
||||||
// has "Assistant" and "Share", PS4 has the touchpad button).
|
|
||||||
//
|
//
|
||||||
// To generate this list, PS4, XBox, Stadia and Nintendo Switch Pro were tested.
|
// MUST be kept in sync.
|
||||||
static const int GAMEPAD_KEY_CODES[19] = {
|
//
|
||||||
// Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar
|
// see https://source.android.com/devices/input/key-layout-files for documentation.
|
||||||
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
|
|
||||||
|
|
||||||
BTN_TL, // "Left Bumper" / "L1" - Nintendo sends BTN_WEST instead
|
// Defines axis mapping information between android and
|
||||||
BTN_TR, // "Right Bumper" / "R1" - Nintendo sends BTN_Z instead
|
// uinput axis.
|
||||||
|
struct GamepadKey {
|
||||||
// For triggers, gamepads vary:
|
int32_t androidKeyCode;
|
||||||
// - Stadia sends analog values over ABS_GAS/ABS_BRAKE and sends
|
int linuxUinputKeyCode;
|
||||||
// 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 information for an axis.
|
static const GamepadKey GAMEPAD_KEYS[] = {
|
||||||
struct Axis {
|
// Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar
|
||||||
int number;
|
{AKEYCODE_BUTTON_A, BTN_A},
|
||||||
int rangeMin;
|
{AKEYCODE_BUTTON_B, BTN_B},
|
||||||
int rangeMax;
|
{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
|
// List of all axes supported by a gamepad
|
||||||
static const Axis GAMEPAD_AXES[] = {
|
static const GamepadAxis GAMEPAD_AXES[] = {
|
||||||
{ABS_X, 0, 254}, // Left joystick X
|
{AMOTION_EVENT_AXIS_X, -1, 1, ABS_X, 0, 254}, // Left joystick X
|
||||||
{ABS_Y, 0, 254}, // Left joystick Y
|
{AMOTION_EVENT_AXIS_Y, -1, 1, ABS_Y, 0, 254}, // Left joystick Y
|
||||||
{ABS_RX, 0, 254}, // Right joystick X
|
{AMOTION_EVENT_AXIS_Z, -1, 1, ABS_Z, 0, 254}, // Right joystick X
|
||||||
{ABS_RY, 0, 254}, // Right joystick Y
|
{AMOTION_EVENT_AXIS_RZ, -1, 1, ABS_RZ, 0, 254}, // Right joystick Y
|
||||||
{ABS_Z, 0, 254}, // Left trigger
|
{AMOTION_EVENT_AXIS_LTRIGGER, 0, 1, ABS_GAS, 0, 254}, // Left trigger
|
||||||
{ABS_RZ, 0, 254}, // Right trigger
|
{AMOTION_EVENT_AXIS_RTRIGGER, 0, 1, ABS_BRAKE, 0, 254}, // Right trigger
|
||||||
{ABS_HAT0X, -1, 1}, // DPad X
|
{AMOTION_EVENT_AXIS_HAT_X, -1, 1, ABS_HAT0X, -1, 1}, // DPad X
|
||||||
{ABS_HAT0Y, -1, 1}, // DPad Y
|
{AMOTION_EVENT_AXIS_HAT_Y, -1, 1, ABS_HAT0Y, -1, 1}, // DPad Y
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace android
|
} // namespace android
|
||||||
|
|||||||
@@ -31,27 +31,38 @@
|
|||||||
#include <utils/String8.h>
|
#include <utils/String8.h>
|
||||||
|
|
||||||
#include <ctype.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 <fcntl.h>
|
||||||
|
#include <linux/input.h>
|
||||||
#include <linux/uinput.h>
|
#include <linux/uinput.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
#include <stdint.h>
|
||||||
#include <sys/inotify.h>
|
#include <sys/inotify.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <sys/time.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#define SLOT_UNKNOWN -1
|
#define SLOT_UNKNOWN -1
|
||||||
|
|
||||||
namespace android {
|
namespace android {
|
||||||
|
|
||||||
static std::map<int32_t,int> keysMap;
|
#define GOOGLE_VENDOR_ID 0x18d1
|
||||||
static std::map<int32_t,int32_t> slotsMap;
|
|
||||||
|
#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;
|
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() {
|
static void initKeysMap() {
|
||||||
if (keysMap.empty()) {
|
if (keysMap.empty()) {
|
||||||
for (size_t i = 0; i < NELEM(KEYS); i++) {
|
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) {
|
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()) {
|
if (it != keysMap.end()) {
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
return KEY_UNKNOWN;
|
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) {
|
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()) {
|
if (it != slotsMap.end()) {
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
@@ -107,7 +151,7 @@ public:
|
|||||||
|
|
||||||
// Open /dev/uinput and prepare to register
|
// Open /dev/uinput and prepare to register
|
||||||
// the device with the given name and unique Id
|
// 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
|
// Checks if the current file descriptor is valid
|
||||||
bool IsValid() const { return mFd != kInvalidFileDescriptor; }
|
bool IsValid() const { return mFd != kInvalidFileDescriptor; }
|
||||||
@@ -141,7 +185,7 @@ int UInputDescriptor::Detach() {
|
|||||||
return fd;
|
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()) {
|
if (IsValid()) {
|
||||||
ALOGE("UInput device already open");
|
ALOGE("UInput device already open");
|
||||||
return false;
|
return false;
|
||||||
@@ -161,6 +205,8 @@ bool UInputDescriptor::Open(const char* name, const char* uniqueId) {
|
|||||||
strlcpy(mUinputDescriptor.name, name, UINPUT_MAX_NAME_SIZE);
|
strlcpy(mUinputDescriptor.name, name, UINPUT_MAX_NAME_SIZE);
|
||||||
mUinputDescriptor.id.version = 1;
|
mUinputDescriptor.id.version = 1;
|
||||||
mUinputDescriptor.id.bustype = BUS_VIRTUAL;
|
mUinputDescriptor.id.bustype = BUS_VIRTUAL;
|
||||||
|
mUinputDescriptor.id.vendor = GOOGLE_VENDOR_ID;
|
||||||
|
mUinputDescriptor.id.product = product;
|
||||||
|
|
||||||
// All UInput devices we use process keys
|
// All UInput devices we use process keys
|
||||||
ioctl(mFd, UI_SET_EVBIT, EV_KEY);
|
ioctl(mFd, UI_SET_EVBIT, EV_KEY);
|
||||||
@@ -258,7 +304,7 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId,
|
|||||||
initKeysMap();
|
initKeysMap();
|
||||||
|
|
||||||
UInputDescriptor descriptor;
|
UInputDescriptor descriptor;
|
||||||
if (!descriptor.Open(name, uniqueId)) {
|
if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_REMOTE_PRODUCT_ID)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,21 +323,24 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId,
|
|||||||
NativeConnection* NativeConnection::openGamepad(const char* name, const char* uniqueId) {
|
NativeConnection* NativeConnection::openGamepad(const char* name, const char* uniqueId) {
|
||||||
ALOGI("Registering uinput device %s: gamepad", name);
|
ALOGI("Registering uinput device %s: gamepad", name);
|
||||||
|
|
||||||
|
initGamepadKeyMap();
|
||||||
|
|
||||||
UInputDescriptor descriptor;
|
UInputDescriptor descriptor;
|
||||||
if (!descriptor.Open(name, uniqueId)) {
|
if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_GAMEPAD_PROUCT_ID)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the keys mapped for gamepads
|
// set the keys mapped for gamepads
|
||||||
for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) {
|
for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) {
|
||||||
descriptor.EnableKey(GAMEPAD_KEY_CODES[i]);
|
descriptor.EnableKey(GAMEPAD_KEYS[i].linuxUinputKeyCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// define the axes that are required
|
// define the axes that are required
|
||||||
descriptor.EnableAxesEvents();
|
descriptor.EnableAxesEvents();
|
||||||
for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
|
for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
|
||||||
const Axis& axis = GAMEPAD_AXES[i];
|
const GamepadAxis& axis = GAMEPAD_AXES[i];
|
||||||
descriptor.EnableAxis(axis.number, axis.rangeMin, axis.rangeMax);
|
descriptor.EnableAxis(axis.linuxUinputAxis, axis.linuxUinputRangeMin,
|
||||||
|
axis.linuxUinputRangeMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!descriptor.Create()) {
|
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) {
|
jboolean down) {
|
||||||
NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
|
NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
|
||||||
|
|
||||||
@@ -359,16 +408,16 @@ static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((keyIndex < 0) || (keyIndex >= NELEM(GAMEPAD_KEY_CODES))) {
|
int linuxKeyCode = getGamepadkeyCode(keyCode);
|
||||||
ALOGE("Invalid gamepad key index: %d", keyIndex);
|
if (linuxKeyCode == KEY_UNKNOWN) {
|
||||||
|
ALOGE("Gamepad: received an unknown keycode of %d.", keyCode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
connection->sendEvent(EV_KEY, linuxKeyCode, down ? 1 : 0);
|
||||||
connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[keyIndex], down ? 1 : 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jint axis,
|
static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jint axis,
|
||||||
jint value) {
|
jfloat value) {
|
||||||
NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
|
NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
|
||||||
|
|
||||||
if (!connection->IsGamepad()) {
|
if (!connection->IsGamepad()) {
|
||||||
@@ -376,7 +425,25 @@ static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jin
|
|||||||
return;
|
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,
|
static void nativeSendPointerDown(JNIEnv* env, jclass clazz, jlong ptr,
|
||||||
@@ -441,18 +508,20 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) {
|
for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) {
|
||||||
connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[i], 0);
|
connection->sendEvent(EV_KEY, GAMEPAD_KEYS[i].linuxUinputKeyCode, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
|
for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
|
||||||
const Axis& axis = GAMEPAD_AXES[i];
|
const GamepadAxis& axis = GAMEPAD_AXES[i];
|
||||||
if ((axis.number == ABS_Z) || (axis.number == ABS_RZ)) {
|
|
||||||
|
if ((axis.linuxUinputAxis == ABS_Z) || (axis.linuxUinputAxis == ABS_RZ)) {
|
||||||
// Mark triggers unpressed
|
// Mark triggers unpressed
|
||||||
connection->sendEvent(EV_ABS, axis.number, 0);
|
connection->sendEvent(EV_ABS, axis.linuxUinputAxis, axis.linuxUinputRangeMin);
|
||||||
} else {
|
} else {
|
||||||
// Joysticks and dpad rests on center
|
// 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},
|
{"nativeClear", "(J)V", (void*)nativeClear},
|
||||||
{"nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync},
|
{"nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync},
|
||||||
{"nativeSendGamepadKey", "(JIZ)V", (void*)nativeSendGamepadKey},
|
{"nativeSendGamepadKey", "(JIZ)V", (void*)nativeSendGamepadKey},
|
||||||
{"nativeSendGamepadAxisValue", "(JII)V", (void*)nativeSendGamepadAxisValue},
|
{"nativeSendGamepadAxisValue", "(JIF)V", (void*)nativeSendGamepadAxisValue},
|
||||||
};
|
};
|
||||||
|
|
||||||
int register_android_server_tv_TvUinputBridge(JNIEnv* env) {
|
int register_android_server_tv_TvUinputBridge(JNIEnv* env) {
|
||||||
|
|||||||
Reference in New Issue
Block a user