diff options
9 files changed, 645 insertions, 119 deletions
diff --git a/data/keyboards/Vendor_18d1_Product_0200.kcm b/data/keyboards/Vendor_18d1_Product_0200.kcm new file mode 100644 index 000000000000..231fac6b48b7 --- /dev/null +++ b/data/keyboards/Vendor_18d1_Product_0200.kcm @@ -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 +} + diff --git a/data/keyboards/Vendor_18d1_Product_0200.kl b/data/keyboards/Vendor_18d1_Product_0200.kl new file mode 100644 index 000000000000..d30bcc60e663 --- /dev/null +++ b/data/keyboards/Vendor_18d1_Product_0200.kl @@ -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 + diff --git a/media/java/android/media/tv/ITvRemoteServiceInput.aidl b/media/java/android/media/tv/ITvRemoteServiceInput.aidl index a0b6c9bfc8d8..0e6563a1ab13 100644 --- a/media/java/android/media/tv/ITvRemoteServiceInput.aidl +++ b/media/java/android/media/tv/ITvRemoteServiceInput.aidl @@ -39,4 +39,10 @@ oneway interface ITvRemoteServiceInput { void sendPointerUp(IBinder token, int pointerId); @UnsupportedAppUsage void sendPointerSync(IBinder token); -}
\ No newline at end of file + + // 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); +} diff --git a/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java index 0bf0f97d2c5e..b97ac26bb915 100644 --- a/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java +++ b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java @@ -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 " + finalName + + ": failure", re); + } + }); + return; + } + } + try { + 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 openRemoteInputBridge() for " + name + ": failure", re); + Log.e(TAG, "Delayed openGamepadBridge() for " + finalName + ": failure", + re); } }); return; } } try { - mRemoteServiceInput.openInputBridge(token, name, width, height, maxPointers); - Log.d(TAG, "openRemoteInputBridge() for " + name + ": success"); + 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) { diff --git a/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java b/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java index c9ce56138217..e6e39390962e 100644 --- a/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java +++ b/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java @@ -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()); + } } diff --git a/services/core/java/com/android/server/tv/TvRemoteServiceInput.java b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java index 8fe6da5e8dbe..390340a13e51 100644 --- a/services/core/java/com/android/server/tv/TvRemoteServiceInput.java +++ b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java @@ -88,6 +88,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) { Slog.d(TAG, "closeInputBridge(), token: " + token); @@ -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); + } + } + } } diff --git a/services/core/java/com/android/server/tv/UinputBridge.java b/services/core/java/com/android/server/tv/UinputBridge.java index a2fe5fcde8c2..1dc201d4ee6b 100644 --- a/services/core/java/com/android/server/tv/UinputBridge.java +++ b/services/core/java/com/android/server/tv/UinputBridge.java @@ -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); } diff --git a/services/core/jni/com_android_server_tv_GamepadKeys.h b/services/core/jni/com_android_server_tv_GamepadKeys.h index 11fc9031da3b..127010f907ff 100644 --- a/services/core/jni/com_android_server_tv_GamepadKeys.h +++ b/services/core/jni/com_android_server_tv_GamepadKeys.h @@ -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 +// Defines axis mapping information between android and +// uinput axis. +struct GamepadKey { + int32_t androidKeyCode; + int linuxUinputKeyCode; +}; + +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}, - // 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" + // 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}, - BTN_SELECT, // "Select/Back". Often "options" or similar - BTN_START, // "Start/forward". Often "hamburger" icon + // 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 - BTN_THUMBL, // "Left Joystick Pressed" - BTN_THUMBR, // "Right Joystick Pressed" + // Pressing on the joyticks themselves + {AKEYCODE_BUTTON_THUMBL, BTN_THUMBL}, + {AKEYCODE_BUTTON_THUMBR, BTN_THUMBR}, - // 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" + // 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}, - BTN_MODE, // "Main button" (Stadia/PS/XBOX/Home) + // "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}, - BTN_TRIGGER_HAPPY1, // Extra button: "Assistant" for Stadia - BTN_TRIGGER_HAPPY2, // Extra button: "Share" for Stadia + // Assignment to support global assistant for devices that support it. + {AKEYCODE_ASSIST, KEY_ASSISTANT}, + {AKEYCODE_VOICE_ASSIST, KEY_VOICECOMMAND}, }; -// Defines information for an axis. -struct Axis { - int number; - int rangeMin; - int rangeMax; +// 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 diff --git a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp index 0e96bd7ae47e..6e2e2c54518b 100644 --- a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp +++ b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp @@ -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) { |