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

Commit e79e1f46 authored by Vaibhav Devmurari's avatar Vaibhav Devmurari
Browse files

Separate KeyGestures controlled in IMS and PWM to fix state issues

Earlier we let IMS handle gestures and fallback to PWM but some
key gestures spanning across keys (e.g. Alt+Tab) maintain state
and let key flow through, so we were maintaining state at both IMS
and PWM causing unpredictable results when doing Alt+Tab.

Test: atest WmTests
Bug: 358569822
Flag: com.android.hardware.input.use_key_gesture_event_handler
Change-Id: I308aa992764291a2b2a12fa88fa765db8c6030e8
parent 10790390
Loading
Loading
Loading
Loading
+62 −1
Original line number Diff line number Diff line
@@ -3453,8 +3453,16 @@ public class PhoneWindowManager implements WindowManagerPolicy {
    // NOTE: Please try not to add new Shortcut combinations here and instead use KeyGestureEvents.
    // Add shortcut trigger logic in {@code KeyGestureController} and add handling logic in
    // {@link handleKeyGesture()}
    @SuppressLint("MissingPermission")
    private boolean interceptSystemKeysAndShortcuts(IBinder focusedToken, KeyEvent event) {
        if (useKeyGestureEventHandler()) {
            return interceptSystemKeysAndShortcutsNew(focusedToken, event);
        } else {
            return interceptSystemKeysAndShortcutsOld(focusedToken, event);
        }
    }

    @SuppressLint("MissingPermission")
    private boolean interceptSystemKeysAndShortcutsOld(IBinder focusedToken, KeyEvent event) {
        final boolean keyguardOn = keyguardOn();
        final int keyCode = event.getKeyCode();
        final int repeatCount = event.getRepeatCount();
@@ -3878,6 +3886,59 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        return (metaState & KeyEvent.META_META_ON) != 0;
    }

    private boolean interceptSystemKeysAndShortcutsNew(IBinder focusedToken, KeyEvent event) {
        final int keyCode = event.getKeyCode();
        final int metaState = event.getMetaState();
        final boolean keyguardOn = keyguardOn();

        if (isUserSetupComplete() && !keyguardOn) {
            if (mModifierShortcutManager.interceptKey(event)) {
                dismissKeyboardShortcutsMenu();
                return true;
            }
        }
        switch (keyCode) {
            case KeyEvent.KEYCODE_HOME:
                return handleHomeShortcuts(focusedToken, event);
            case KeyEvent.KEYCODE_VOLUME_UP:
            case KeyEvent.KEYCODE_VOLUME_DOWN:
            case KeyEvent.KEYCODE_VOLUME_MUTE:
                if (mUseTvRouting || mHandleVolumeKeysInWM) {
                    // On TVs or when the configuration is enabled, volume keys never
                    // go to the foreground app.
                    dispatchDirectAudioEvent(event);
                    return true;
                }

                // If the device is in VR mode and keys are "internal" (e.g. on the side of the
                // device), then drop the volume keys and don't forward it to the
                // application/dispatch the audio event.
                if (mDefaultDisplayPolicy.isPersistentVrModeEnabled()) {
                    final InputDevice d = event.getDevice();
                    if (d != null && !d.isExternal()) {
                        return true;
                    }
                }
                break;
            case KeyEvent.KEYCODE_STEM_PRIMARY:
                if (prepareToSendSystemKeyToApplication(focusedToken, event)) {
                    // Send to app.
                    return false;
                } else {
                    // Intercepted.
                    sendSystemKeyToStatusBarAsync(event);
                    return true;
                }
        }
        if (isValidGlobalKey(keyCode)
                && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
            return true;
        }

        // Reserve all the META modifier combos for system behavior
        return (metaState & KeyEvent.META_META_ON) != 0;
    }

    @SuppressLint("MissingPermission")
    private void initKeyGestures() {
        if (!useKeyGestureEventHandler()) {
+140 −183

File changed.

Preview size limit exceeded, changes collapsed.

+15 −5
Original line number Diff line number Diff line
@@ -119,6 +119,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * ALT + TAB to show recent apps.
     */
    @Test
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testAltTab() {
        mPhoneWindowManager.overrideStatusBarManagerInternal();
        sendKeyCombination(new int[]{KEYCODE_ALT_LEFT, KEYCODE_TAB}, 0);
@@ -158,6 +159,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * META + CTRL+ S to take screenshot.
     */
    @Test
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testMetaCtrlS() {
        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_S}, 0);
        mPhoneWindowManager.assertTakeScreenshotCalled();
@@ -167,6 +169,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * META + N to expand notification panel.
     */
    @Test
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testMetaN() throws RemoteException {
        mPhoneWindowManager.overrideTogglePanel();
        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_N}, 0);
@@ -177,6 +180,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * META + SLASH to toggle shortcuts menu.
     */
    @Test
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testMetaSlash() {
        mPhoneWindowManager.overrideStatusBarManagerInternal();
        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_SLASH}, 0);
@@ -187,6 +191,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * META  + ALT to toggle Cap Lock.
     */
    @Test
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testMetaAlt() {
        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ALT_LEFT}, 0);
        mPhoneWindowManager.assertToggleCapsLock();
@@ -196,6 +201,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * META + H to go to homescreen
     */
    @Test
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testMetaH() {
        mPhoneWindowManager.overrideLaunchHome();
        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_H}, 0);
@@ -206,6 +212,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * META + ENTER to go to homescreen
     */
    @Test
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testMetaEnter() {
        mPhoneWindowManager.overrideLaunchHome();
        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ENTER}, 0);
@@ -216,6 +223,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * Sends a KEYCODE_BRIGHTNESS_DOWN event and validates the brightness is decreased as expected;
     */
    @Test
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testKeyCodeBrightnessDown() {
        float[] currentBrightness = new float[]{0.1f, 0.05f, 0.0f};
        float[] newBrightness = new float[]{0.065738f, 0.0275134f, 0.0f};
@@ -231,9 +239,9 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled
     */
    @Test
    @EnableFlags(com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testTakeScreenshot_flagEnabled() {
        mSetFlagsRule.enableFlags(com.android.hardware.input.Flags
                .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
        sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
        mPhoneWindowManager.assertTakeScreenshotCalled();
    }
@@ -242,9 +250,9 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled
     */
    @Test
    @DisableFlags({com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE,
            com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER})
    public void testTakeScreenshot_flagDisabled() {
        mSetFlagsRule.disableFlags(com.android.hardware.input.Flags
                .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
        sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
        mPhoneWindowManager.assertTakeScreenshotNotCalled();
    }
@@ -254,6 +262,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     */
    @Test
    @EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
    @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
    public void testTakeBugReport_flagEnabled() throws RemoteException {
        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0);
        mPhoneWindowManager.assertTakeBugreport(true);
@@ -263,7 +272,8 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase {
     * META+CTRL+BACKSPACE for taking a bugreport does nothing when the flag is disabledd.
     */
    @Test
    @DisableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
    @DisableFlags({com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT,
            com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER})
    public void testTakeBugReport_flagDisabled() throws RemoteException {
        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0);
        mPhoneWindowManager.assertTakeBugreport(false);
+238 −42
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.input

import android.content.Context
import android.content.ContextWrapper
import android.content.res.Resources
import android.hardware.input.IInputManager
import android.hardware.input.AidlKeyGestureEvent
import android.hardware.input.IKeyGestureEventListener
@@ -25,15 +26,18 @@ import android.hardware.input.IKeyGestureHandler
import android.hardware.input.InputManager
import android.hardware.input.InputManagerGlobal
import android.hardware.input.KeyGestureEvent
import android.hardware.input.KeyGestureEvent.KeyGestureType
import android.os.IBinder
import android.os.Process
import android.os.SystemProperties
import android.os.test.TestLooper
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.Presubmit
import android.platform.test.flag.junit.SetFlagsRule
import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyEvent
import androidx.test.core.app.ApplicationProvider
import com.android.internal.R
import com.android.internal.annotations.Keep
import com.android.internal.util.FrameworkStatsLog
import com.android.modules.utils.testing.ExtendedMockitoRule
@@ -78,32 +82,43 @@ class KeyGestureControllerTests {
            KeyEvent.KEYCODE_META_LEFT to (KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON),
            KeyEvent.KEYCODE_META_RIGHT to (KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON),
        )
        const val SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH = 0
        const val SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1
        const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
        const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
        const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
    }

    @JvmField
    @Rule
    val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
        .mockStatic(FrameworkStatsLog::class.java).build()!!
        .mockStatic(FrameworkStatsLog::class.java)
        .mockStatic(SystemProperties::class.java).build()!!

    @JvmField
    @Rule
    val rule = SetFlagsRule()

    @Mock
    private lateinit var iInputManager: IInputManager

    @Mock
    private lateinit var resources: Resources

    private var currentPid = 0
    private lateinit var keyGestureController: KeyGestureController
    private lateinit var context: Context
    private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
    private lateinit var testLooper: TestLooper
    private var events = mutableListOf<KeyGestureEvent>()
    private var handleEvents = mutableListOf<KeyGestureEvent>()

    @Before
    fun setup() {
        context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
        Mockito.`when`(context.resources).thenReturn(resources)
        inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
        setupInputDevices()
        testLooper = TestLooper()
        currentPid = Process.myPid()
        keyGestureController = KeyGestureController(context, testLooper.looper)
    }

    private fun setupInputDevices() {
@@ -116,7 +131,7 @@ class KeyGestureControllerTests {
        Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
    }

    private fun notifyHomeGestureCompleted() {
    private fun notifyHomeGestureCompleted(keyGestureController: KeyGestureController) {
        keyGestureController.notifyKeyGestureCompleted(DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H),
            KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON,
            KeyGestureEvent.KEY_GESTURE_TYPE_HOME)
@@ -124,11 +139,12 @@ class KeyGestureControllerTests {

    @Test
    fun testKeyGestureEvent_registerUnregisterListener() {
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        val listener = KeyGestureEventListener()

        // Register key gesture event listener
        keyGestureController.registerKeyGestureEventListener(listener, 0)
        notifyHomeGestureCompleted()
        notifyHomeGestureCompleted(keyGestureController)
        testLooper.dispatchAll()
        assertEquals(
            "Listener should get callbacks on key gesture event completed",
@@ -144,7 +160,7 @@ class KeyGestureControllerTests {
        // Unregister listener
        events.clear()
        keyGestureController.unregisterKeyGestureEventListener(listener, 0)
        notifyHomeGestureCompleted()
        notifyHomeGestureCompleted(keyGestureController)
        testLooper.dispatchAll()
        assertEquals(
            "Listener should not get callback after being unregistered",
@@ -155,6 +171,8 @@ class KeyGestureControllerTests {

    @Test
    fun testKeyGestureEvent_multipleGestureHandlers() {
        val keyGestureController = KeyGestureController(context, testLooper.looper)

        // Set up two callbacks.
        var callbackCount1 = 0
        var callbackCount2 = 0
@@ -405,6 +423,14 @@ class KeyGestureControllerTests {
                KeyEvent.META_META_ON or KeyEvent.META_ALT_ON,
                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
            ),
            TestData(
                "META + / -> Open Shortcut Helper",
                intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SLASH),
                KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
                intArrayOf(KeyEvent.KEYCODE_SLASH),
                KeyEvent.META_META_ON,
                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
            ),
            TestData(
                "BRIGHTNESS_UP -> Brightness Up",
                intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_UP),
@@ -534,6 +560,188 @@ class KeyGestureControllerTests {
    @Test
    @Parameters(method = "keyGestureEventHandlerTestArguments")
    fun testKeyGestures(test: TestData) {
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        testKeyGestureInternal(keyGestureController, test)
    }

    @Test
    fun testKeycodesFullyConsumed_irrespectiveOfHandlers() {
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        val testKeys = intArrayOf(
            KeyEvent.KEYCODE_RECENT_APPS,
            KeyEvent.KEYCODE_APP_SWITCH,
            KeyEvent.KEYCODE_BRIGHTNESS_UP,
            KeyEvent.KEYCODE_BRIGHTNESS_DOWN,
            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN,
            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP,
            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE,
            KeyEvent.KEYCODE_ALL_APPS,
            KeyEvent.KEYCODE_NOTIFICATION,
            KeyEvent.KEYCODE_SETTINGS,
            KeyEvent.KEYCODE_LANGUAGE_SWITCH,
            KeyEvent.KEYCODE_SCREENSHOT,
            KeyEvent.KEYCODE_META_LEFT,
            KeyEvent.KEYCODE_META_RIGHT,
            KeyEvent.KEYCODE_ASSIST,
            KeyEvent.KEYCODE_VOICE_ASSIST,
            KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY,
            KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY,
            KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY,
            KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL,
        )

        val handler = KeyGestureHandler { _, _ -> false }
        keyGestureController.registerKeyGestureHandler(handler, 0)

        for (key in testKeys) {
            sendKeys(keyGestureController, intArrayOf(key), assertAllConsumed = true)
        }
    }

    @Test
    fun testSearchKeyGestures_defaultSearch() {
        Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior))
            .thenReturn(SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH)
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        testKeyGestureNotProduced(
            keyGestureController,
            "SEARCH -> Default Search",
            intArrayOf(KeyEvent.KEYCODE_SEARCH),
        )
    }

    @Test
    fun testSearchKeyGestures_searchActivity() {
        Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior))
            .thenReturn(SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY)
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        testKeyGestureInternal(
            keyGestureController,
            TestData(
                "SEARCH -> Launch Search Activity",
                intArrayOf(KeyEvent.KEYCODE_SEARCH),
                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
                intArrayOf(KeyEvent.KEYCODE_SEARCH),
                0,
                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
            )
        )
    }

    @Test
    fun testSettingKeyGestures_doNothing() {
        Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
            .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTHING)
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        testKeyGestureNotProduced(
            keyGestureController,
            "SETTINGS -> Do Nothing",
            intArrayOf(KeyEvent.KEYCODE_SETTINGS),
        )
    }

    @Test
    fun testSettingKeyGestures_settingsActivity() {
        Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
            .thenReturn(SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY)
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        testKeyGestureInternal(
            keyGestureController,
            TestData(
                "SETTINGS -> Launch Settings Activity",
                intArrayOf(KeyEvent.KEYCODE_SETTINGS),
                KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
                intArrayOf(KeyEvent.KEYCODE_SETTINGS),
                0,
                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
            )
        )
    }

    @Test
    fun testSettingKeyGestures_notificationPanel() {
        Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
            .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL)
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        testKeyGestureInternal(
            keyGestureController,
            TestData(
                "SETTINGS -> Toggle Notification Panel",
                intArrayOf(KeyEvent.KEYCODE_SETTINGS),
                KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
                intArrayOf(KeyEvent.KEYCODE_SETTINGS),
                0,
                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
            )
        )
    }

    @Test
    @EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
    fun testTriggerBugReport() {
        Mockito.`when`(SystemProperties.get("ro.debuggable")).thenReturn("1")
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        testKeyGestureInternal(
            keyGestureController,
            TestData(
                "META + CTRL + DEL -> Trigger Bug Report",
                intArrayOf(
                    KeyEvent.KEYCODE_META_LEFT,
                    KeyEvent.KEYCODE_CTRL_LEFT,
                    KeyEvent.KEYCODE_DEL
                ),
                KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT,
                intArrayOf(KeyEvent.KEYCODE_DEL),
                KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON,
                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
            )
        )
    }

    @Test
    @DisableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
    fun testTriggerBugReport_flagDisabled() {
        Mockito.`when`(SystemProperties.get("ro.debuggable")).thenReturn("1")
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        testKeyGestureInternal(
            keyGestureController,
            TestData(
                "META + CTRL + DEL -> Not Trigger Bug Report (Fallback to BACK)",
                intArrayOf(
                    KeyEvent.KEYCODE_META_LEFT,
                    KeyEvent.KEYCODE_CTRL_LEFT,
                    KeyEvent.KEYCODE_DEL
                ),
                KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
                intArrayOf(KeyEvent.KEYCODE_DEL),
                KeyEvent.META_META_ON,
                intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
            )
        )
    }

    @Test
    fun testCapsLockPressNotified() {
        val keyGestureController = KeyGestureController(context, testLooper.looper)
        val listener = KeyGestureEventListener()

        keyGestureController.registerKeyGestureEventListener(listener, 0)
        sendKeys(keyGestureController, intArrayOf(KeyEvent.KEYCODE_CAPS_LOCK))
        testLooper.dispatchAll()
        assertEquals(
            "Listener should get callbacks on key gesture event completed",
            1,
            events.size
        )
        assertEquals(
            "Listener should get callback for Toggle Caps Lock key gesture complete event",
            KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
            events[0].keyGestureType
        )
    }

    private fun testKeyGestureInternal(keyGestureController: KeyGestureController, test: TestData) {
        var handleEvents = mutableListOf<KeyGestureEvent>()
        val handler = KeyGestureHandler { event, _ ->
            handleEvents.add(KeyGestureEvent(event))
            true
@@ -541,7 +749,7 @@ class KeyGestureControllerTests {
        keyGestureController.registerKeyGestureHandler(handler, 0)
        handleEvents.clear()

        sendKeys(test.keys, /* assertAllConsumed = */ false)
        sendKeys(keyGestureController, test.keys)

        assertEquals(
            "Test: $test doesn't produce correct number of key gesture events",
@@ -575,45 +783,33 @@ class KeyGestureControllerTests {
        keyGestureController.unregisterKeyGestureHandler(handler, 0)
    }

    @Test
    fun testKeycodesFullyConsumed_irrespectiveOfHandlers() {
        val testKeys = intArrayOf(
            KeyEvent.KEYCODE_RECENT_APPS,
            KeyEvent.KEYCODE_APP_SWITCH,
            KeyEvent.KEYCODE_BRIGHTNESS_UP,
            KeyEvent.KEYCODE_BRIGHTNESS_DOWN,
            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN,
            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP,
            KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE,
            KeyEvent.KEYCODE_ALL_APPS,
            KeyEvent.KEYCODE_NOTIFICATION,
            KeyEvent.KEYCODE_SETTINGS,
            KeyEvent.KEYCODE_LANGUAGE_SWITCH,
            KeyEvent.KEYCODE_SCREENSHOT,
            KeyEvent.KEYCODE_META_LEFT,
            KeyEvent.KEYCODE_META_RIGHT,
            KeyEvent.KEYCODE_ASSIST,
            KeyEvent.KEYCODE_VOICE_ASSIST,
            KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY,
            KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY,
            KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY,
            KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL,
        )

        val handler = KeyGestureHandler { _, _ -> false }
    private fun testKeyGestureNotProduced(
        keyGestureController: KeyGestureController,
        testName: String,
        testKeys: IntArray
    ) {
        var handleEvents = mutableListOf<KeyGestureEvent>()
        val handler = KeyGestureHandler { event, _ ->
            handleEvents.add(KeyGestureEvent(event))
            true
        }
        keyGestureController.registerKeyGestureHandler(handler, 0)
        handleEvents.clear()

        for (key in testKeys) {
            sendKeys(intArrayOf(key), /* assertAllConsumed = */ true)
        }
        sendKeys(keyGestureController, testKeys)
        assertEquals("Test: $testName should not produce Key gesture", 0, handleEvents.size)
    }

    private fun sendKeys(testKeys: IntArray, assertAllConsumed: Boolean) {
    private fun sendKeys(
        keyGestureController: KeyGestureController,
        testKeys: IntArray,
        assertAllConsumed: Boolean = false
    ) {
        var metaState = 0
        for (key in testKeys) {
            val downEvent = KeyEvent(
                /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_DOWN, key,
                0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
                0 /*repeat*/, metaState, DEVICE_ID, 0 /*scancode*/,
                0 /*flags*/, InputDevice.SOURCE_KEYBOARD
            )
            val consumed =
@@ -633,7 +829,7 @@ class KeyGestureControllerTests {
        for (key in testKeys.reversed()) {
            val upEvent = KeyEvent(
                /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_UP, key,
                0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
                0 /*repeat*/, metaState, DEVICE_ID, 0 /*scancode*/,
                0 /*flags*/, InputDevice.SOURCE_KEYBOARD
            )
            val consumed =