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

Commit 3b92b968 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.

Merged-In: I49612fce5e74c4e00ca60c715c6c72954e73b7a3
Change-Id: I49612fce5e74c4e00ca60c715c6c72954e73b7a3
(cherry picked from commit 9b9f556a)
parent 4e177518
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);
}
+155 −8
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.
@@ -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) {
+48 −0
Original line number Diff line number Diff line
@@ -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());
    }
}
Loading