Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9b9f556a authored by Andrei Litvin's avatar Andrei Litvin
Browse files

Add support for GamePad api in ITvRemoteServiceInput.

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

Bug: 150764186

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

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

Change-Id: I49612fce5e74c4e00ca60c715c6c72954e73b7a3
parent 5c179060
Loading
Loading
Loading
Loading
+48 −0
Original line number Diff line number Diff line
# 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 −0
Original line number Diff line number Diff line
# 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
+7 −1
Original line number Diff line number Diff line
@@ -39,4 +39,10 @@ oneway interface ITvRemoteServiceInput {
    void sendPointerUp(IBinder token, int pointerId);
    @UnsupportedAppUsage
    void sendPointerSync(IBinder token);

    // API specific to gamepads. Close gamepads with closeInputBridge
    void openGamepadBridge(IBinder token, String name);
    void sendGamepadKeyDown(IBinder token, int keyCode);
    void sendGamepadKeyUp(IBinder token, int keyCode);
    void sendGamepadAxisValue(IBinder token, int axis, float value);
}
+14 −10
Original line number Diff line number Diff line
@@ -3,18 +3,22 @@ package com.android.media.tv.remoteprovider {

  public abstract class TvRemoteProvider {
    ctor public TvRemoteProvider(android.content.Context);
    method public void clearInputBridge(android.os.IBinder) throws java.lang.RuntimeException;
    method public void closeInputBridge(android.os.IBinder) throws java.lang.RuntimeException;
    method public void clearInputBridge(@NonNull android.os.IBinder) throws java.lang.RuntimeException;
    method public void closeInputBridge(@NonNull android.os.IBinder) throws java.lang.RuntimeException;
    method public android.os.IBinder getBinder();
    method public final android.content.Context getContext();
    method public void onInputBridgeConnected(android.os.IBinder);
    method public void openRemoteInputBridge(android.os.IBinder, String, int, int, int) throws java.lang.RuntimeException;
    method public void sendKeyDown(android.os.IBinder, int) throws java.lang.RuntimeException;
    method public void sendKeyUp(android.os.IBinder, int) throws java.lang.RuntimeException;
    method public void sendPointerDown(android.os.IBinder, int, int, int) throws java.lang.RuntimeException;
    method public void sendPointerSync(android.os.IBinder) throws java.lang.RuntimeException;
    method public void sendPointerUp(android.os.IBinder, int) throws java.lang.RuntimeException;
    method public void sendTimestamp(android.os.IBinder, long) throws java.lang.RuntimeException;
    method public void onInputBridgeConnected(@NonNull android.os.IBinder);
    method public void openGamepadBridge(@NonNull android.os.IBinder, @NonNull String) throws java.lang.RuntimeException;
    method public void openRemoteInputBridge(@NonNull android.os.IBinder, @NonNull String, int, int, int) throws java.lang.RuntimeException;
    method public void sendGamepadAxisValue(@NonNull android.os.IBinder, int, @FloatRange(from=-1.0F, to=1.0f) float) throws java.lang.RuntimeException;
    method public void sendGamepadKeyDown(@NonNull android.os.IBinder, int) throws java.lang.RuntimeException;
    method public void sendGamepadKeyUp(@NonNull android.os.IBinder, int) throws java.lang.RuntimeException;
    method public void sendKeyDown(@NonNull android.os.IBinder, int) throws java.lang.RuntimeException;
    method public void sendKeyUp(@NonNull android.os.IBinder, int) throws java.lang.RuntimeException;
    method public void sendPointerDown(@NonNull android.os.IBinder, int, int, int) throws java.lang.RuntimeException;
    method public void sendPointerSync(@NonNull android.os.IBinder) throws java.lang.RuntimeException;
    method public void sendPointerUp(@NonNull android.os.IBinder, int) throws java.lang.RuntimeException;
    method public void sendTimestamp(@NonNull android.os.IBinder, long) throws java.lang.RuntimeException;
    field public static final String SERVICE_INTERFACE = "com.android.media.tv.remoteprovider.TvRemoteProvider";
  }

+156 −17
Original line number Diff line number Diff line
@@ -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.
@@ -93,7 +96,7 @@ public abstract class TvRemoteProvider {
     *
     * @param token Identifier for the connection. Null, if failed.
     */
    public void onInputBridgeConnected(IBinder token) {
    public void onInputBridgeConnected(@NonNull IBinder token) {
    }

    /**
@@ -124,27 +127,73 @@ 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(
            @NonNull IBinder token, @NonNull String name, int width, int height, int maxPointers)
            throws RuntimeException {
        final IBinder finalToken = Objects.requireNonNull(token);
        final String finalName = Objects.requireNonNull(name);

        synchronized (mOpenBridgeRunnables) {
            if (mRemoteServiceInput == null) {
                Log.d(TAG, "Delaying openRemoteInputBridge() for " + name);
                Log.d(TAG, "Delaying openRemoteInputBridge() for " + finalName);

                mOpenBridgeRunnables.add(() -> {
                    try {
                        mRemoteServiceInput.openInputBridge(
                                token, name, width, height, maxPointers);
                        Log.d(TAG, "Delayed openRemoteInputBridge() for " + name + ": success");
                                finalToken, finalName, width, height, maxPointers);
                        Log.d(TAG, "Delayed openRemoteInputBridge() for " + finalName
                                + ": success");
                    } catch (RemoteException re) {
                        Log.e(TAG, "Delayed openRemoteInputBridge() for " + name + ": failure", re);
                        Log.e(TAG, "Delayed openRemoteInputBridge() for " + finalName
                                + ": failure", re);
                    }
                });
                return;
            }
        }
        try {
            mRemoteServiceInput.openInputBridge(token, name, width, height, maxPointers);
            Log.d(TAG, "openRemoteInputBridge() for " + name + ": success");
            mRemoteServiceInput.openInputBridge(finalToken, finalName, width, height, maxPointers);
            Log.d(TAG, "openRemoteInputBridge() for " + finalName + ": success");
        } catch (RemoteException re) {
            throw re.rethrowFromSystemServer();
        }
    }

    /**
     * Opens an input bridge as a gamepad device.
     * Clients should pass in a token that can be used to match this request with a token that
     * will be returned by {@link TvRemoteProvider#onInputBridgeConnected(IBinder token)}
     * <p>
     * The token should be used for subsequent calls.
     * </p>
     *
     * @param token       Identifier for this connection
     * @param name        Device name
     * @throws RuntimeException
     */
    public void openGamepadBridge(@NonNull IBinder token, @NonNull  String name)
            throws RuntimeException {
        final IBinder finalToken = Objects.requireNonNull(token);
        final String finalName = Objects.requireNonNull(name);
        synchronized (mOpenBridgeRunnables) {
            if (mRemoteServiceInput == null) {
                Log.d(TAG, "Delaying openGamepadBridge() for " + finalName);

                mOpenBridgeRunnables.add(() -> {
                    try {
                        mRemoteServiceInput.openGamepadBridge(finalToken, finalName);
                        Log.d(TAG, "Delayed openGamepadBridge() for " + finalName + ": success");
                    } catch (RemoteException re) {
                        Log.e(TAG, "Delayed openGamepadBridge() for " + finalName + ": failure",
                                re);
                    }
                });
                return;
            }
        }
        try {
            mRemoteServiceInput.openGamepadBridge(token, finalName);
            Log.d(TAG, "openGamepadBridge() for " + finalName + ": success");
        } catch (RemoteException re) {
            throw re.rethrowFromSystemServer();
        }
@@ -156,7 +205,8 @@ public abstract class TvRemoteProvider {
     * @param token identifier for this connection
     * @throws RuntimeException
     */
    public void closeInputBridge(IBinder token) throws RuntimeException {
    public void closeInputBridge(@NonNull IBinder token) throws RuntimeException {
        Objects.requireNonNull(token);
        try {
            mRemoteServiceInput.closeInputBridge(token);
        } catch (RemoteException re) {
@@ -172,7 +222,8 @@ public abstract class TvRemoteProvider {
     * @param token identifier for this connection
     * @throws RuntimeException
     */
    public void clearInputBridge(IBinder token) throws RuntimeException {
    public void clearInputBridge(@NonNull IBinder token) throws RuntimeException {
        Objects.requireNonNull(token);
        if (DEBUG_KEYS) Log.d(TAG, "clearInputBridge() token " + token);
        try {
            mRemoteServiceInput.clearInputBridge(token);
@@ -189,7 +240,8 @@ public abstract class TvRemoteProvider {
     *                  {@link android.os.SystemClock#uptimeMillis} time base
     * @throws RuntimeException
     */
    public void sendTimestamp(IBinder token, long timestamp) throws RuntimeException {
    public void sendTimestamp(@NonNull IBinder token, long timestamp) throws RuntimeException {
        Objects.requireNonNull(token);
        if (DEBUG_KEYS) Log.d(TAG, "sendTimestamp() token: " + token +
                ", timestamp: " + timestamp);
        try {
@@ -206,7 +258,8 @@ public abstract class TvRemoteProvider {
     * @param keyCode Key code to be sent
     * @throws RuntimeException
     */
    public void sendKeyUp(IBinder token, int keyCode) throws RuntimeException {
    public void sendKeyUp(@NonNull 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);
@@ -222,7 +275,8 @@ public abstract class TvRemoteProvider {
     * @param keyCode Key code to be sent
     * @throws RuntimeException
     */
    public void sendKeyDown(IBinder token, int keyCode) throws RuntimeException {
    public void sendKeyDown(@NonNull IBinder token, int keyCode) throws RuntimeException {
        Objects.requireNonNull(token);
        if (DEBUG_KEYS) Log.d(TAG, "sendKeyDown() token: " + token +
                ", keyCode: " + keyCode);
        try {
@@ -240,7 +294,8 @@ public abstract class TvRemoteProvider {
     *                  to {@link MotionEvent#getPointerCount()} -1
     * @throws RuntimeException
     */
    public void sendPointerUp(IBinder token, int pointerId) throws RuntimeException {
    public void sendPointerUp(@NonNull IBinder token, int pointerId) throws RuntimeException {
        Objects.requireNonNull(token);
        if (DEBUG_KEYS) Log.d(TAG, "sendPointerUp() token: " + token +
                ", pointerId: " + pointerId);
        try {
@@ -260,8 +315,9 @@ public abstract class TvRemoteProvider {
     * @param y         Y co-ordinates in display pixels
     * @throws RuntimeException
     */
    public void sendPointerDown(IBinder token, int pointerId, int x, int y)
    public void sendPointerDown(@NonNull 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 {
@@ -277,7 +333,8 @@ public abstract class TvRemoteProvider {
     * @param token identifier for the device
     * @throws RuntimeException
     */
    public void sendPointerSync(IBinder token) throws RuntimeException {
    public void sendPointerSync(@NonNull IBinder token) throws RuntimeException {
        Objects.requireNonNull(token);
        if (DEBUG_KEYS) Log.d(TAG, "sendPointerSync() token: " + token);
        try {
            mRemoteServiceInput.sendPointerSync(token);
@@ -286,6 +343,88 @@ 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)
     */
    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
     */
    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
     */
    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) {
Loading