Merge "Add support for customizing touchpad 3-finger tap" into main
diff --git a/core/java/android/hardware/input/AidlInputGestureData.aidl b/core/java/android/hardware/input/AidlInputGestureData.aidl
index 137f672..e33ec53 100644
--- a/core/java/android/hardware/input/AidlInputGestureData.aidl
+++ b/core/java/android/hardware/input/AidlInputGestureData.aidl
@@ -19,13 +19,26 @@
 /** @hide */
 @JavaDerive(equals=true)
 parcelable AidlInputGestureData {
-    int keycode;
-    int modifierState;
-    int gestureType;
+    Trigger trigger;
 
-    // App launch parameters: Only set if gestureType is KEY_GESTURE_TYPE_LAUNCH_APPLICATION
+    int gestureType;
+    // App launch parameters (Only set if gestureType is LAUNCH_APPLICATION)
     String appLaunchCategory;
     String appLaunchRole;
     String appLaunchPackageName;
     String appLaunchClassName;
+
+    parcelable KeyTrigger {
+        int keycode;
+        int modifierState;
+    }
+
+    parcelable TouchpadGestureTrigger {
+        int gestureType;
+    }
+
+    union Trigger {
+        KeyTrigger key;
+        TouchpadGestureTrigger touchpadGesture;
+    }
 }
diff --git a/core/java/android/hardware/input/InputGestureData.java b/core/java/android/hardware/input/InputGestureData.java
index 5ab73ce..ee0a2a9 100644
--- a/core/java/android/hardware/input/InputGestureData.java
+++ b/core/java/android/hardware/input/InputGestureData.java
@@ -35,20 +35,40 @@
  */
 public final class InputGestureData {
 
+    public static final int TOUCHPAD_GESTURE_TYPE_UNKNOWN = 0;
+    public static final int TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP = 1;
+
     @NonNull
     private final AidlInputGestureData mInputGestureData;
 
-    public InputGestureData(AidlInputGestureData inputGestureData) {
+    public InputGestureData(@NonNull AidlInputGestureData inputGestureData) {
         this.mInputGestureData = inputGestureData;
         validate();
     }
 
     /** Returns the trigger information for this input gesture */
     public Trigger getTrigger() {
-        if (mInputGestureData.keycode != KeyEvent.KEYCODE_UNKNOWN) {
-            return new KeyTrigger(mInputGestureData.keycode, mInputGestureData.modifierState);
+        switch (mInputGestureData.trigger.getTag()) {
+            case AidlInputGestureData.Trigger.Tag.key: {
+                AidlInputGestureData.KeyTrigger trigger = mInputGestureData.trigger.getKey();
+                if (trigger == null) {
+                    throw new RuntimeException("InputGestureData is corrupted, null key trigger!");
+                }
+                return createKeyTrigger(trigger.keycode, trigger.modifierState);
+            }
+            case AidlInputGestureData.Trigger.Tag.touchpadGesture: {
+                AidlInputGestureData.TouchpadGestureTrigger trigger =
+                        mInputGestureData.trigger.getTouchpadGesture();
+                if (trigger == null) {
+                    throw new RuntimeException(
+                            "InputGestureData is corrupted, null touchpad trigger!");
+                }
+                return createTouchpadTrigger(trigger.gestureType);
+            }
+            default:
+                throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!");
+
         }
-        throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!");
     }
 
     /** Returns the action to perform for this input gesture */
@@ -127,9 +147,15 @@
                         "No app launch data for system action launch application");
             }
             AidlInputGestureData data = new AidlInputGestureData();
+            data.trigger = new AidlInputGestureData.Trigger();
             if (mTrigger instanceof KeyTrigger keyTrigger) {
-                data.keycode = keyTrigger.getKeycode();
-                data.modifierState = keyTrigger.getModifierState();
+                data.trigger.setKey(new AidlInputGestureData.KeyTrigger());
+                data.trigger.getKey().keycode = keyTrigger.getKeycode();
+                data.trigger.getKey().modifierState = keyTrigger.getModifierState();
+            } else if (mTrigger instanceof TouchpadTrigger touchpadTrigger) {
+                data.trigger.setTouchpadGesture(new AidlInputGestureData.TouchpadGestureTrigger());
+                data.trigger.getTouchpadGesture().gestureType =
+                        touchpadTrigger.getTouchpadGestureType();
             } else {
                 throw new IllegalArgumentException("Invalid trigger type!");
             }
@@ -163,30 +189,12 @@
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         InputGestureData that = (InputGestureData) o;
-        return mInputGestureData.keycode == that.mInputGestureData.keycode
-                && mInputGestureData.modifierState == that.mInputGestureData.modifierState
-                && mInputGestureData.gestureType == that.mInputGestureData.gestureType
-                && Objects.equals(mInputGestureData.appLaunchCategory, that.mInputGestureData.appLaunchCategory)
-                && Objects.equals(mInputGestureData.appLaunchRole, that.mInputGestureData.appLaunchRole)
-                && Objects.equals(mInputGestureData.appLaunchPackageName, that.mInputGestureData.appLaunchPackageName)
-                && Objects.equals(mInputGestureData.appLaunchPackageName, that.mInputGestureData.appLaunchPackageName);
+        return Objects.equals(mInputGestureData, that.mInputGestureData);
     }
 
     @Override
     public int hashCode() {
-        int _hash = 1;
-        _hash = 31 * _hash + mInputGestureData.keycode;
-        _hash = 31 * _hash + mInputGestureData.modifierState;
-        _hash = 31 * _hash + mInputGestureData.gestureType;
-        _hash = 31 * _hash + (mInputGestureData.appLaunchCategory != null
-                ? mInputGestureData.appLaunchCategory.hashCode() : 0);
-        _hash = 31 * _hash + (mInputGestureData.appLaunchRole != null
-                ? mInputGestureData.appLaunchRole.hashCode() : 0);
-        _hash = 31 * _hash + (mInputGestureData.appLaunchPackageName != null
-                ? mInputGestureData.appLaunchPackageName.hashCode() : 0);
-        _hash = 31 * _hash + (mInputGestureData.appLaunchPackageName != null
-                ? mInputGestureData.appLaunchPackageName.hashCode() : 0);
-        return _hash;
+        return mInputGestureData.hashCode();
     }
 
     public interface Trigger {
@@ -197,6 +205,11 @@
         return new KeyTrigger(keycode, modifierState);
     }
 
+    /** Creates a input gesture trigger based on a touchpad gesture */
+    public static Trigger createTouchpadTrigger(int touchpadGestureType) {
+        return new TouchpadTrigger(touchpadGestureType);
+    }
+
     /** Key based input gesture trigger */
     public static class KeyTrigger implements Trigger {
         private static final int SHORTCUT_META_MASK =
@@ -242,6 +255,43 @@
         }
     }
 
+    /** Touchpad based input gesture trigger */
+    public static class TouchpadTrigger implements Trigger {
+        private final int mTouchpadGestureType;
+
+        private TouchpadTrigger(int touchpadGestureType) {
+            if (touchpadGestureType != TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP) {
+                throw new IllegalArgumentException(
+                        "Invalid touchpadGestureType = " + touchpadGestureType);
+            }
+            mTouchpadGestureType = touchpadGestureType;
+        }
+
+        public int getTouchpadGestureType() {
+            return mTouchpadGestureType;
+        }
+
+        @Override
+        public String toString() {
+            return "TouchpadTrigger{" +
+                    "mTouchpadGestureType=" + mTouchpadGestureType +
+                    '}';
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            TouchpadTrigger that = (TouchpadTrigger) o;
+            return mTouchpadGestureType == that.mTouchpadGestureType;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(mTouchpadGestureType);
+        }
+    }
+
     /** Data for action to perform when input gesture is triggered */
     public record Action(@KeyGestureEvent.KeyGestureType int keyGestureType,
                          @Nullable AppLaunchData appLaunchData) {
diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java
index cf1cdaf..a9c42c7 100644
--- a/services/core/java/com/android/server/input/InputGestureManager.java
+++ b/services/core/java/com/android/server/input/InputGestureManager.java
@@ -362,6 +362,22 @@
     }
 
     @Nullable
+    public InputGestureData getCustomGestureForTouchpadGesture(@UserIdInt int userId,
+            int touchpadGestureType) {
+        if (touchpadGestureType == InputGestureData.TOUCHPAD_GESTURE_TYPE_UNKNOWN) {
+            return null;
+        }
+        synchronized (mGestureLock) {
+            Map<InputGestureData.Trigger, InputGestureData> customGestures =
+                    mCustomInputGestures.get(userId);
+            if (customGestures == null) {
+                return null;
+            }
+            return customGestures.get(InputGestureData.createTouchpadTrigger(touchpadGestureType));
+        }
+    }
+
+    @Nullable
     public InputGestureData getSystemShortcutForKeyEvent(KeyEvent event) {
         final int keyCode = event.getKeyCode();
         if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 78e3b84..1bba331 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -64,6 +64,7 @@
 import android.hardware.input.IStickyModifierStateListener;
 import android.hardware.input.ITabletModeChangedListener;
 import android.hardware.input.InputDeviceIdentifier;
+import android.hardware.input.InputGestureData;
 import android.hardware.input.InputManager;
 import android.hardware.input.InputSensorInfo;
 import android.hardware.input.InputSettings;
@@ -2314,7 +2315,8 @@
     // Native callback.
     @SuppressWarnings("unused")
     private void notifyTouchpadThreeFingerTap() {
-        mKeyGestureController.handleTouchpadThreeFingerTap();
+        mKeyGestureController.handleTouchpadGesture(
+                InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP);
     }
 
     // Native callback.
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index e0991ec..fc10640 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -847,6 +847,13 @@
                 /* appLaunchData = */null);
     }
 
+    private void handleTouchpadGesture(@KeyGestureEvent.KeyGestureType int keyGestureType,
+            @Nullable AppLaunchData appLaunchData) {
+        handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, new int[0], /* modifierState= */0,
+                keyGestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                Display.DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData);
+    }
+
     @VisibleForTesting
     boolean handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
             @KeyGestureEvent.KeyGestureType int gestureType, int action, int displayId,
@@ -897,11 +904,18 @@
         handleKeyGesture(event, null /*focusedToken*/);
     }
 
-    public void handleTouchpadThreeFingerTap() {
-        // TODO(b/365063048): trigger a custom shortcut based on the three-finger tap.
-        if (DEBUG) {
-            Slog.d(TAG, "Three-finger touchpad tap occurred");
+    public void handleTouchpadGesture(int touchpadGestureType) {
+        // Handle custom shortcuts
+        InputGestureData customGesture;
+        synchronized (mUserLock) {
+            customGesture = mInputGestureManager.getCustomGestureForTouchpadGesture(mCurrentUserId,
+                    touchpadGestureType);
         }
+        if (customGesture == null) {
+            return;
+        }
+        handleTouchpadGesture(customGesture.getAction().keyGestureType(),
+                customGesture.getAction().appLaunchData());
     }
 
     @MainThread
@@ -1214,6 +1228,7 @@
     public void dump(IndentingPrintWriter ipw) {
         ipw.println("KeyGestureController:");
         ipw.increaseIndent();
+        ipw.println("mCurrentUserId = " + mCurrentUserId);
         ipw.println("mSystemPid = " + mSystemPid);
         ipw.println("mPendingMetaAction = " + mPendingMetaAction);
         ipw.println("mPendingCapsLockToggle = " + mPendingCapsLockToggle);
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 6c9f764..1574d1b 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -1260,24 +1260,102 @@
         testKeyGestureInternal(test)
     }
 
-    private fun testKeyGestureInternal(test: TestData) {
-        var handleEvents = mutableListOf<KeyGestureEvent>()
+    class TouchpadTestData(
+        val name: String,
+        val touchpadGestureType: Int,
+        val expectedKeyGestureType: Int,
+        val expectedAction: Int,
+        val expectedAppLaunchData: AppLaunchData? = null,
+    ) {
+        override fun toString(): String = name
+    }
+
+    @Keep
+    private fun customTouchpadGesturesTestArguments(): Array<TouchpadTestData> {
+        return arrayOf(
+            TouchpadTestData(
+                "3 Finger Tap -> Go Home",
+                InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP,
+                KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE
+            ),
+            TouchpadTestData(
+                "3 Finger Tap -> Launch app",
+                InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP,
+                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION,
+                KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+                AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest")
+            ),
+        )
+    }
+
+    @Test
+    @Parameters(method = "customTouchpadGesturesTestArguments")
+    fun testCustomTouchpadGesture(test: TouchpadTestData) {
+        setupKeyGestureController()
+        val builder = InputGestureData.Builder()
+            .setKeyGestureType(test.expectedKeyGestureType)
+            .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType))
+        if (test.expectedAppLaunchData != null) {
+            builder.setAppLaunchData(test.expectedAppLaunchData)
+        }
+        val inputGestureData = builder.build()
+
+        keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData)
+
+        val handledEvents = mutableListOf<KeyGestureEvent>()
         val handler = KeyGestureHandler { event, _ ->
-            handleEvents.add(KeyGestureEvent(event))
+            handledEvents.add(KeyGestureEvent(event))
             true
         }
         keyGestureController.registerKeyGestureHandler(handler, 0)
-        handleEvents.clear()
+        handledEvents.clear()
+
+        keyGestureController.handleTouchpadGesture(test.touchpadGestureType)
+
+        assertEquals(
+            "Test: $test doesn't produce correct number of key gesture events",
+            1,
+            handledEvents.size
+        )
+        val event = handledEvents[0]
+        assertEquals(
+            "Test: $test doesn't produce correct key gesture type",
+            test.expectedKeyGestureType,
+            event.keyGestureType
+        )
+        assertEquals(
+            "Test: $test doesn't produce correct key gesture action",
+            test.expectedAction,
+            event.action
+        )
+        assertEquals(
+            "Test: $test doesn't produce correct app launch data",
+            test.expectedAppLaunchData,
+            event.appLaunchData
+        )
+
+        keyGestureController.unregisterKeyGestureHandler(handler, 0)
+    }
+
+    private fun testKeyGestureInternal(test: TestData) {
+        val handledEvents = mutableListOf<KeyGestureEvent>()
+        val handler = KeyGestureHandler { event, _ ->
+            handledEvents.add(KeyGestureEvent(event))
+            true
+        }
+        keyGestureController.registerKeyGestureHandler(handler, 0)
+        handledEvents.clear()
 
         sendKeys(test.keys)
 
         assertEquals(
             "Test: $test doesn't produce correct number of key gesture events",
             test.expectedActions.size,
-            handleEvents.size
+            handledEvents.size
         )
-        for (i in handleEvents.indices) {
-            val event = handleEvents[i]
+        for (i in handledEvents.indices) {
+            val event = handledEvents[i]
             assertArrayEquals(
                 "Test: $test doesn't produce correct key gesture keycodes",
                 test.expectedKeys,
@@ -1309,16 +1387,16 @@
     }
 
     private fun testKeyGestureNotProduced(testName: String, testKeys: IntArray) {
-        var handleEvents = mutableListOf<KeyGestureEvent>()
+        var handledEvents = mutableListOf<KeyGestureEvent>()
         val handler = KeyGestureHandler { event, _ ->
-            handleEvents.add(KeyGestureEvent(event))
+            handledEvents.add(KeyGestureEvent(event))
             true
         }
         keyGestureController.registerKeyGestureHandler(handler, 0)
-        handleEvents.clear()
+        handledEvents.clear()
 
         sendKeys(testKeys)
-        assertEquals("Test: $testName should not produce Key gesture", 0, handleEvents.size)
+        assertEquals("Test: $testName should not produce Key gesture", 0, handledEvents.size)
     }
 
     private fun sendKeys(testKeys: IntArray, assertNotSentToApps: Boolean = false) {