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

Commit 92cd5cd8 authored by Vaibhav Devmurari's avatar Vaibhav Devmurari
Browse files

Refactor Single key detector to provide start, complete and cancel

This is just a refactor internally to provide this capability, we
only use COMPLETE events which behave exaclty like the current
behaviours.

The reason was to provide ways to listen to START, COMPLETE and
CANCEL events for certain key gestures (e.g. Gemini animation when
long press POWER occurs). This would be as a subsequent CL.

Flag: EXEMPT refactor
Bug: 358569822
Test: atest WmTests
Change-Id: Icc987173564e5675629731b99b02aa7e3c10015b
parent b3eb540d
Loading
Loading
Loading
Loading
+80 −39
Original line number Diff line number Diff line
@@ -82,6 +82,10 @@ import static android.view.contentprotection.flags.Flags.createAccessibilityOver

import static com.android.hardware.input.Flags.enableNew25q2Keycodes;
import static com.android.hardware.input.Flags.hidBluetoothWakeup;
import static com.android.server.policy.SingleKeyGestureEvent.ACTION_COMPLETE;
import static com.android.server.policy.SingleKeyGestureEvent.SINGLE_KEY_GESTURE_TYPE_LONG_PRESS;
import static com.android.server.policy.SingleKeyGestureEvent.SINGLE_KEY_GESTURE_TYPE_PRESS;
import static com.android.server.policy.SingleKeyGestureEvent.SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS;
import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED;
import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT;
import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_UNCOVERED;
@@ -105,6 +109,7 @@ import static com.android.systemui.shared.Flags.enableLppAssistInvocationHapticE
import static com.android.window.flags.Flags.delegateBackGestureToShell;

import android.accessibilityservice.AccessibilityService;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
@@ -2496,7 +2501,31 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        }

        @Override
        void onPress(long downTime, int displayId) {
        void onKeyGesture(@NonNull SingleKeyGestureEvent event) {
            final long startTime = event.getStartTime();
            final int displayId = event.getDisplayId();
            final int pressCount = event.getPressCount();
            if (event.getAction() != ACTION_COMPLETE) {
                return;
            }
            switch (event.getType()) {
                case SINGLE_KEY_GESTURE_TYPE_PRESS:
                    if (event.getPressCount() > 1) {
                        onMultiPress(startTime, pressCount, displayId);
                    } else {
                        onPress(startTime, displayId);
                    }
                    break;
                case SINGLE_KEY_GESTURE_TYPE_LONG_PRESS:
                    onLongPress(startTime);
                    break;
                case SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS:
                    onVeryLongPress();
                    break;
            }
        }

        private void onPress(long downTime, int displayId) {
            if (mShouldEarlyShortPressOnPower) {
                return;
            }
@@ -2512,32 +2541,29 @@ public class PhoneWindowManager implements WindowManagerPolicy {
            }
        }

        @Override
        void onLongPress(long eventTime) {
        private void onLongPress(long downTime) {
            if (mSingleKeyGestureDetector.beganFromNonInteractive()
                    && !mSupportLongPressPowerWhenNonInteractive) {
                Slog.v(TAG, "Not support long press power when device is not interactive.");
                return;
            }

            powerLongPress(eventTime);
            powerLongPress(downTime);
        }

        @Override
        void onVeryLongPress(long eventTime) {
        private void onVeryLongPress() {
            mActivityManagerInternal.prepareForPossibleShutdown();
            powerVeryLongPress();
        }

        @Override
        void onMultiPress(long downTime, int count, int displayId) {
        private void onMultiPress(long downTime, int count, int displayId) {
            powerPress(downTime, count, displayId);
        }

        @Override
        void onKeyUp(long eventTime, int count, int displayId, int deviceId, int metaState) {
        void onKeyUp(int count, KeyEvent event) {
            if (mShouldEarlyShortPressOnPower && count == 1) {
                powerPress(eventTime, 1 /*pressCount*/, displayId);
                powerPress(event.getDownTime(), 1 /*pressCount*/, event.getDisplayId());
            }
        }
    }
@@ -2556,18 +2582,20 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        }

        @Override
        int getMaxMultiPressCount() {
            return 1;
        void onKeyGesture(@NonNull SingleKeyGestureEvent event) {
            if (event.getAction() != ACTION_COMPLETE) {
                return;
            }

        @Override
        void onPress(long downTime, int unusedDisplayId) {
            switch (event.getType()) {
                case SINGLE_KEY_GESTURE_TYPE_PRESS:
                    if (event.getPressCount() == 1) {
                        mBackKeyHandled |= backKeyPress();
                    }

        @Override
        void onLongPress(long downTime) {
                    break;
                case SINGLE_KEY_GESTURE_TYPE_LONG_PRESS:
                    backLongPress();
                    break;
            }
        }
    }

@@ -2590,7 +2618,27 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        }

        @Override
        void onPress(long downTime, int unusedDisplayId) {
        void onKeyGesture(@NonNull SingleKeyGestureEvent event) {
            final long startTime = event.getStartTime();
            final int pressCount = event.getPressCount();
            if (event.getAction() != ACTION_COMPLETE) {
                return;
            }
            switch (event.getType()) {
                case SINGLE_KEY_GESTURE_TYPE_PRESS:
                    if (event.getPressCount() > 1) {
                        onMultiPress(startTime, pressCount);
                    } else {
                        onPress(startTime);
                    }
                    break;
                case SINGLE_KEY_GESTURE_TYPE_LONG_PRESS:
                    onLongPress(startTime);
                    break;
            }
        }

        private void onPress(long downTime) {
            if (shouldHandleStemPrimaryEarlyShortPress()) {
                return;
            }
@@ -2599,22 +2647,20 @@ public class PhoneWindowManager implements WindowManagerPolicy {
                    KeyEvent.KEYCODE_STEM_PRIMARY, downTime, () -> stemPrimaryPress(1 /*count*/));
        }

        @Override
        void onLongPress(long eventTime) {
        private void onLongPress(long downTime) {
            if (mLongPressOnStemPrimaryBehavior == LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT) {
                // Long-press to assistant gesture is not overridable by apps.
                stemPrimaryLongPress(eventTime);
                stemPrimaryLongPress(downTime);
            } else {
                // Other long-press actions should be triggered only if app doesn't handle it.
                mDeferredKeyActionExecutor.queueKeyAction(
                        KeyEvent.KEYCODE_STEM_PRIMARY,
                        eventTime,
                        () -> stemPrimaryLongPress(eventTime));
                        downTime,
                        () -> stemPrimaryLongPress(downTime));
            }
        }

        @Override
        void onMultiPress(long downTime, int count, int unusedDisplayId) {
        private void onMultiPress(long downTime, int count) {
            // Triple-press stem to toggle accessibility gesture should always be triggered
            // regardless of if app handles it.
            if (count == 3
@@ -2655,7 +2701,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        }

        @Override
        void onKeyUp(long eventTime, int count, int displayId, int deviceId, int metaState) {
        void onKeyUp(int count, KeyEvent event) {
            if (count == 1) {
                // Save info about the most recent task on the first press of the stem key. This
                // may be used later to switch to the most recent app using double press gesture.
@@ -2672,7 +2718,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
                    // Key-up gesture should be triggered only if app doesn't handle it.
                    mDeferredKeyActionExecutor.queueKeyAction(
                            KeyEvent.KEYCODE_STEM_PRIMARY,
                            eventTime,
                            event.getDownTime(),
                            () -> {
                                Slog.d(TAG, "StemPrimaryKeyRule: executing deferred onKeyUp");
                                // Save the info of the focused task on screen. This may be used
@@ -2720,18 +2766,13 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        }

        @Override
        void onPress(long downTime, int displayId) {

        }

        @Override
        void onKeyUp(long eventTime, int pressCount, int displayId, int deviceId, int metaState) {
        void onKeyUp(int pressCount, KeyEvent event) {
            if (pressCount != 1) {
                return;
            }
            // Single press on tail button triggers the open notes gesture.
            handleKeyGestureInKeyGestureController(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES,
                    deviceId, KEYCODE_STYLUS_BUTTON_TAIL, metaState);
                    event.getDeviceId(), KEYCODE_STYLUS_BUTTON_TAIL, event.getMetaState());
        }
    }

+164 −101

File changed.

Preview size limit exceeded, changes collapsed.

+585 −0

File added.

Preview size limit exceeded, changes collapsed.

+285 −19
Original line number Diff line number Diff line
@@ -18,12 +18,19 @@ package com.android.server.policy;

import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.KEYCODE_A;
import static android.view.KeyEvent.KEYCODE_BACK;
import static android.view.KeyEvent.KEYCODE_POWER;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static com.android.hardware.input.Flags.FLAG_ABORT_SLOW_MULTI_PRESS;
import static com.android.server.policy.SingleKeyGestureEvent.ACTION_CANCEL;
import static com.android.server.policy.SingleKeyGestureEvent.ACTION_COMPLETE;
import static com.android.server.policy.SingleKeyGestureEvent.ACTION_START;
import static com.android.server.policy.SingleKeyGestureEvent.SINGLE_KEY_GESTURE_TYPE_LONG_PRESS;
import static com.android.server.policy.SingleKeyGestureEvent.SINGLE_KEY_GESTURE_TYPE_PRESS;
import static com.android.server.policy.SingleKeyGestureEvent.SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -43,6 +50,9 @@ import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.KeyEvent;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -72,7 +82,7 @@ public class SingleKeyGestureTests {
    private CountDownLatch mVeryLongPressed = new CountDownLatch(1);
    private CountDownLatch mMultiPressed = new CountDownLatch(1);
    private BlockingQueue<KeyUpData> mKeyUpQueue = new LinkedBlockingQueue<>();

    private RandomKeyRule mRandomGestureRule = new RandomKeyRule(KEYCODE_A);
    private final Instrumentation mInstrumentation = getInstrumentation();
    private final Context mContext = mInstrumentation.getTargetContext();
    private long mWaitTimeout;
@@ -101,6 +111,7 @@ public class SingleKeyGestureTests {
    }

    private void initSingleKeyGestureRules() {
        // Similar to current POWER key rules defined in PhoneWindowManager
        mDetector.addRule(
                new SingleKeyGestureDetector.SingleKeyRule(KEYCODE_POWER) {
                    @Override
@@ -119,15 +130,36 @@ public class SingleKeyGestureTests {
                    }

                    @Override
                    public void onPress(long downTime, int displayId) {
                    void onKeyGesture(@NonNull SingleKeyGestureEvent event) {
                        final int pressCount = event.getPressCount();
                        if (event.getAction() != ACTION_COMPLETE) {
                            return;
                        }
                        switch (event.getType()) {
                            case SINGLE_KEY_GESTURE_TYPE_PRESS:
                                if (event.getPressCount() > 1) {
                                    onMultiPress(pressCount);
                                } else {
                                    onPress();
                                }
                                break;
                            case SINGLE_KEY_GESTURE_TYPE_LONG_PRESS:
                                onLongPress();
                                break;
                            case SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS:
                                onVeryLongPress();
                                break;
                        }
                    }

                    private void onPress() {
                        if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
                            return;
                        }
                        mShortPressed.countDown();
                    }

                    @Override
                    void onLongPress(long downTime) {
                    private void onLongPress() {
                        if (mDetector.beganFromNonInteractive()
                                && !mAllowNonInteractiveForLongPress) {
                            return;
@@ -135,13 +167,11 @@ public class SingleKeyGestureTests {
                        mLongPressed.countDown();
                    }

                    @Override
                    void onVeryLongPress(long downTime) {
                    private void onVeryLongPress() {
                        mVeryLongPressed.countDown();
                    }

                    @Override
                    void onMultiPress(long downTime, int count, int displayId) {
                    private void onMultiPress(int count) {
                        if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
                            return;
                        }
@@ -151,12 +181,12 @@ public class SingleKeyGestureTests {
                    }

                    @Override
                    void onKeyUp(long eventTime, int multiPressCount, int displayId, int deviceId,
                            int metaState) {
                    void onKeyUp(int multiPressCount, KeyEvent event) {
                        mKeyUpQueue.add(new KeyUpData(KEYCODE_POWER, multiPressCount));
                    }
                });

        // Similar to current POWER key rules defined in PhoneWindowManager
        mDetector.addRule(
                new SingleKeyGestureDetector.SingleKeyRule(KEYCODE_BACK) {
                    @Override
@@ -170,15 +200,35 @@ public class SingleKeyGestureTests {
                    }

                    @Override
                    public void onPress(long downTime, int displayId) {
                    void onKeyGesture(@NonNull SingleKeyGestureEvent event) {
                        final long eventTime = event.getEventTime();
                        final int displayId = event.getDisplayId();
                        final int pressCount = event.getPressCount();
                        if (event.getAction() != ACTION_COMPLETE) {
                            return;
                        }
                        switch (event.getType()) {
                            case SINGLE_KEY_GESTURE_TYPE_PRESS:
                                if (event.getPressCount() > 1) {
                                    onMultiPress(pressCount);
                                } else {
                                    onPress();
                                }
                                break;
                            case SINGLE_KEY_GESTURE_TYPE_LONG_PRESS:
                                onLongPress();
                                break;
                        }
                    }

                    private void onPress() {
                        if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
                            return;
                        }
                        mShortPressed.countDown();
                    }

                    @Override
                    void onMultiPress(long downTime, int count, int displayId) {
                    private void onMultiPress(int count) {
                        if (mDetector.beganFromNonInteractive() && !mAllowNonInteractiveForPress) {
                            return;
                        }
@@ -188,16 +238,16 @@ public class SingleKeyGestureTests {
                    }

                    @Override
                    void onKeyUp(long eventTime, int multiPressCount, int displayId, int deviceId,
                            int metaState) {
                    void onKeyUp(int multiPressCount, KeyEvent event) {
                        mKeyUpQueue.add(new KeyUpData(KEYCODE_BACK, multiPressCount));
                    }

                    @Override
                    void onLongPress(long downTime) {
                    private void onLongPress() {
                        mLongPressed.countDown();
                    }
                });

        mDetector.addRule(mRandomGestureRule);
    }

    private static class KeyUpData {
@@ -448,9 +498,12 @@ public class SingleKeyGestureTests {
        final SingleKeyGestureDetector.SingleKeyRule rule =
                new SingleKeyGestureDetector.SingleKeyRule(KEYCODE_POWER) {
                    @Override
                    void onPress(long downTime, int displayId) {
                    void onKeyGesture(@NonNull SingleKeyGestureEvent event) {
                        if (event.getType() == SINGLE_KEY_GESTURE_TYPE_PRESS
                                && event.getPressCount() == 1) {
                            mShortPressed.countDown();
                        }
                    }
                };

        mDetector.removeRule(rule);
@@ -473,4 +526,217 @@ public class SingleKeyGestureTests {
        assertEquals(mVeryLongPressed.getCount(), 1);
        assertEquals(mShortPressed.getCount(), 1);
    }

    @Test
    public void testRandomRuleLongPress() throws InterruptedException {
        // The current flow of events based on implementation is:
        // - long-press(START)
        // - very long press (START)
        // - long press(COMPLETE)
        // - very long press(CANCEL)
        pressKey(KEYCODE_A, mLongPressTime + 50);

        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_START);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_START);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_COMPLETE);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_CANCEL);
    }

    @Test
    public void testRandomRuleVeryLongPress() throws InterruptedException {
        // The current flow of events based on implementation is:
        // - long-press(START)
        // - very long press (START)
        // - long press(COMPLETE)
        // - very long press(COMPLETE)
        pressKey(KEYCODE_A, mVeryLongPressTime + 50);

        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_START);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_START);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_COMPLETE);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_COMPLETE);
    }

    @Test
    public void testRandomRuleShortPress() throws InterruptedException {
        // The current flow of events based on implementation is:
        // - long-press(START)
        // - very long press (START)
        // - long press(CANCEL)
        // - very long press(CANCEL)
        // - 1-press(START)
        // - 1-press(COMPLETE)
        pressKey(KEYCODE_A, 0);

        // Long press and very long press gestures are started on key down
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_START);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_START);

        // Long press and very long press gestures are cancelled on key up (duration < thresholds)
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_CANCEL);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_CANCEL);

        // On key up start single press
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_START, /* pressCount = */1);
        // After waiting for multi-press timeout single press gesture is completed
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_COMPLETE, /* pressCount = */1);
    }

    @Test
    public void testRandomRuleDoublePress() throws InterruptedException {
        // The current flow of events based on implementation is:
        // - long-press(START)
        // - very long press (START)
        // - long press(CANCEL)
        // - very long press(CANCEL)
        // - 1-press(START)
        // - 1-press(CANCEL)
        // - 2-press(START)
        // - 2-press(COMPLETE)
        pressKey(KEYCODE_A, 0);
        pressKey(KEYCODE_A, 0);

        // Long press and very long press gestures are started on key down and cancelled on key up
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_START);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_START);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_CANCEL);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_CANCEL);

        // First press will start single press gesture
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_START, /* pressCount = */1);

        // Single press is cancelled on second key down
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_CANCEL, /* pressCount = */1);

        // On second key up start double press
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_START, /* pressCount = */2);
        // After waiting for multi-press timeout double press gesture is completed
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_COMPLETE, /* pressCount = */2);
    }

    @Test
    public void testRandomRuleTriplePress() throws InterruptedException {
        // The current flow of events based on implementation is:
        // - long-press(START)
        // - very long press (START)
        // - long press(CANCEL)
        // - very long press(CANCEL)
        // - 1-press(START)
        // - 1-press(CANCEL)
        // - 2-press(START)
        // - 2-press(CANCEL)
        // - 3-press(COMPLETE)
        pressKey(KEYCODE_A, 0);
        pressKey(KEYCODE_A, 0);
        pressKey(KEYCODE_A, 0);

        // Long press and very long press gestures are started on key down and cancelled on key up
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_START);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_START);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_LONG_PRESS, ACTION_CANCEL);
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_VERY_LONG_PRESS,
                ACTION_CANCEL);

        // First press will start single press gesture
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_START, /* pressCount = */1);

        // Single press is cancelled on second key down
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_CANCEL, /* pressCount = */1);

        // On second key up start double press
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_START, /* pressCount = */2);
        // Double press is cancelled on third key down
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_CANCEL, /* pressCount = */2);

        // Triple press is completed on third key down (since max press count is 3 no need to wait)
        mRandomGestureRule.assertEventReceived(SINGLE_KEY_GESTURE_TYPE_PRESS,
                ACTION_COMPLETE, /* pressCount = */3);
    }

    private class RandomKeyRule extends SingleKeyGestureDetector.SingleKeyRule {

        private final BlockingQueue<SingleKeyGestureEvent> mEvents = new LinkedBlockingQueue<>();

        private final int mKeyCode;

        RandomKeyRule(int keyCode) {
            super(keyCode);
            mKeyCode = keyCode;
        }

        @Override
        boolean supportLongPress() {
            return true;
        }

        @Override
        boolean supportVeryLongPress() {
            return true;
        }

        @Override
        int getMaxMultiPressCount() {
            return mMaxMultiPressCount;
        }

        @Override
        long getLongPressTimeoutMs() {
            return mLongPressTime;
        }

        @Override
        long getVeryLongPressTimeoutMs() {
            return mVeryLongPressTime;
        }

        @Override
        void onKeyGesture(@NonNull SingleKeyGestureEvent event) {
            if (event.getKeyCode() != mKeyCode) {
                throw new IllegalArgumentException(
                        "Rule generated a gesture for " + KeyEvent.keyCodeToString(
                                event.getKeyCode()) + " but the rule was made for "
                                + KeyEvent.keyCodeToString(mKeyCode));
            }
            mEvents.add(event);
        }

        @Nullable
        SingleKeyGestureEvent getEvent() throws InterruptedException {
            return mEvents.poll(500, TimeUnit.MILLISECONDS);
        }

        void assertEventReceived(int type, int action) throws InterruptedException {
            SingleKeyGestureEvent event = getEvent();
            assertNotNull(event);
            assertEquals("Type mismatch", type, event.getType());
            assertEquals("Action mismatch", action, event.getAction());
        }

        void assertEventReceived(int type, int action, int pressCount) throws InterruptedException {
            SingleKeyGestureEvent event = getEvent();
            assertNotNull(event);
            assertEquals("Type mismatch", type, event.getType());
            assertEquals("Action mismatch", action, event.getAction());
            assertEquals("Count mismatch", pressCount, event.getPressCount());
        }
    }
}