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

Commit 2188e105 authored by Arthur Hung's avatar Arthur Hung
Browse files

Introduce SingleKeyGestureDetector to PhoneWindowManager

The SingleKeyGestureDetector is used to detect the single key gesture
such as short press, long press or multi presses.

If the key has been handled by other policy, it should call 'reset'
to reset all gesture states.

This patch also refactor the power key and back key if the long press
behavior is enabled.

The detector can detect below gestures:
- single press
- long press
- very long press
- multiple presses (press count > 1)

Bug: 169058831
Bug: 127687575
Test: atest KeyEventTest SingleKeyGestureTests
Test: atest GlobalActionsImeTest PowerButtonTest
Test: manual (short press power, long press power, double tap power)
Change-Id: I28ff4373ba57b16e580b8eaaca26a3fa81770d0f
parent af76755f
Loading
Loading
Loading
Loading
+10 −6
Original line number Original line Diff line number Diff line
@@ -102,9 +102,11 @@ public class KeyCombinationManager {
    }
    }


    /**
    /**
     * Check if the key event could be triggered by combine key rule before dispatching to a window.
     * Check if the key event could be intercepted by combination key rule before it is dispatched
     * to a window.
     * Return true if any active rule could be triggered by the key event, otherwise false.
     */
     */
    void interceptKey(KeyEvent event, boolean interactive) {
    boolean interceptKey(KeyEvent event, boolean interactive) {
        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
        final int keyCode = event.getKeyCode();
        final int keyCode = event.getKeyCode();
        final int count = mActiveRules.size();
        final int count = mActiveRules.size();
@@ -117,9 +119,9 @@ public class KeyCombinationManager {
                    // exceed time from first key down.
                    // exceed time from first key down.
                    forAllRules(mActiveRules, (rule)-> rule.cancel());
                    forAllRules(mActiveRules, (rule)-> rule.cancel());
                    mActiveRules.clear();
                    mActiveRules.clear();
                    return;
                    return false;
                } else if (count == 0) { // has some key down but no active rule exist.
                } else if (count == 0) { // has some key down but no active rule exist.
                    return;
                    return false;
                }
                }
            }
            }


@@ -127,7 +129,7 @@ public class KeyCombinationManager {
                mDownTimes.put(keyCode, eventTime);
                mDownTimes.put(keyCode, eventTime);
            } else {
            } else {
                // ignore old key, maybe a repeat key.
                // ignore old key, maybe a repeat key.
                return;
                return false;
            }
            }


            if (mDownTimes.size() == 1) {
            if (mDownTimes.size() == 1) {
@@ -141,7 +143,7 @@ public class KeyCombinationManager {
            } else {
            } else {
                // Ignore if rule already triggered.
                // Ignore if rule already triggered.
                if (mTriggeredRule != null) {
                if (mTriggeredRule != null) {
                    return;
                    return true;
                }
                }


                // check if second key can trigger rule, or remove the non-match rule.
                // check if second key can trigger rule, or remove the non-match rule.
@@ -156,6 +158,7 @@ public class KeyCombinationManager {
                mActiveRules.clear();
                mActiveRules.clear();
                if (mTriggeredRule != null) {
                if (mTriggeredRule != null) {
                    mActiveRules.add(mTriggeredRule);
                    mActiveRules.add(mTriggeredRule);
                    return true;
                }
                }
            }
            }
        } else {
        } else {
@@ -168,6 +171,7 @@ public class KeyCombinationManager {
                }
                }
            }
            }
        }
        }
        return false;
    }
    }


    /**
    /**
+172 −196

File changed.

Preview size limit exceeded, changes collapsed.

+362 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2021 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.
 */

package com.android.server.policy;

import android.annotation.IntDef;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.ViewConfiguration;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;

/**
 * Detect single key gesture: press, long press, very long press and multi press.
 *
 * Call {@link #reset} if current {@link KeyEvent} has been handled by another policy
 */

public final class SingleKeyGestureDetector {
    private static final String TAG = "SingleKeyGesture";
    private static final boolean DEBUG = false;

    private static final int MSG_KEY_LONG_PRESS = 0;
    private static final int MSG_KEY_VERY_LONG_PRESS = 1;
    private static final int MSG_KEY_DELAYED_PRESS = 2;

    private final long mLongPressTimeout;
    private final long mVeryLongPressTimeout;

    private volatile int mKeyPressCounter;

    private final ArrayList<SingleKeyRule> mRules = new ArrayList();
    private SingleKeyRule mActiveRule = null;

    // Key code of current key down event, reset when key up.
    private int mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN;
    private volatile boolean mHandledByLongPress = false;
    private final Handler mHandler;
    private static final long MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout();


    /** Supported gesture flags */
    public static final int KEY_LONGPRESS = 1 << 1;
    public static final int KEY_VERYLONGPRESS = 1 << 2;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = { "KEY_" }, value = {
            KEY_LONGPRESS,
            KEY_VERYLONGPRESS,
    })
    public @interface KeyGestureFlag {}

    /**
     *  Rule definition for single keys gesture.
     *  E.g : define power key.
     *  <pre class="prettyprint">
     *  SingleKeyRule rule =
     *      new SingleKeyRule(KEYCODE_POWER, KEY_LONGPRESS|KEY_VERYLONGPRESS) {
     *           int getMaxMultiPressCount() { // maximum multi press count. }
     *           void onPress(long downTime) { // short press behavior. }
     *           void onLongPress(long eventTime) { // long press behavior. }
     *           void onVeryLongPress(long eventTime) { // very long press behavior. }
     *           void onMultiPress(long downTime, int count) { // multi press behavior.  }
     *       };
     *  </pre>
     */
    abstract static class SingleKeyRule {
        private final int mKeyCode;
        private final int mSupportedGestures;

        SingleKeyRule(int keyCode, @KeyGestureFlag int supportedGestures) {
            mKeyCode = keyCode;
            mSupportedGestures = supportedGestures;
        }

        /**
         *  True if the rule could intercept the key.
         */
        private boolean shouldInterceptKey(int keyCode) {
            return keyCode == mKeyCode;
        }

        /**
         *  True if the rule support long press.
         */
        private boolean supportLongPress() {
            return (mSupportedGestures & KEY_LONGPRESS) != 0;
        }

        /**
         *  True if the rule support very long press.
         */
        private boolean supportVeryLongPress() {
            return (mSupportedGestures & KEY_VERYLONGPRESS) != 0;
        }

        /**
         *  Maximum count of multi presses.
         *  Return 1 will trigger onPress immediately when {@link KeyEvent.ACTION_UP}.
         *  Otherwise trigger onMultiPress immediately when reach max count when
         *  {@link KeyEvent.ACTION_DOWN}.
         */
        int getMaxMultiPressCount() {
            return 1;
        }

        /**
         *  Called when short press has been detected.
         */
        abstract void onPress(long downTime);
        /**
         *  Callback when multi press (>= 2) has been detected.
         */
        void onMultiPress(long downTime, int count) {}
        /**
         *  Callback when long press has been detected.
         */
        void onLongPress(long eventTime) {}
        /**
         *  Callback when very long press has been detected.
         */
        void onVeryLongPress(long eventTime) {}

        @Override
        public String toString() {
            return "KeyCode = " + KeyEvent.keyCodeToString(mKeyCode)
                    + ", long press : " + supportLongPress()
                    + ", very Long press : " + supportVeryLongPress()
                    + ", max multi press count : " + getMaxMultiPressCount();
        }
    }

    public SingleKeyGestureDetector(Context context) {
        mLongPressTimeout = ViewConfiguration.get(context).getDeviceGlobalActionKeyTimeout();
        mVeryLongPressTimeout = context.getResources().getInteger(
                com.android.internal.R.integer.config_veryLongPressTimeout);
        mHandler = new KeyHandler();
    }

    void addRule(SingleKeyRule rule) {
        mRules.add(rule);
    }

    void interceptKey(KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            interceptKeyDown(event);
        } else {
            interceptKeyUp(event);
        }
    }

    private void interceptKeyDown(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        // same key down.
        if (mDownKeyCode == keyCode) {
            if (mActiveRule != null && (event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0
                    && mActiveRule.supportLongPress() && !mHandledByLongPress) {
                if (DEBUG) {
                    Log.i(TAG, "Long press key " + KeyEvent.keyCodeToString(keyCode));
                }
                mHandledByLongPress = true;
                mHandler.removeMessages(MSG_KEY_LONG_PRESS);
                mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
                mActiveRule.onLongPress(event.getEventTime());
            }
            return;
        }

        // When a different key is pressed, stop processing gestures for the currently active key.
        if (mDownKeyCode != KeyEvent.KEYCODE_UNKNOWN
                || (mActiveRule != null && !mActiveRule.shouldInterceptKey(keyCode))) {
            if (DEBUG) {
                Log.i(TAG, "Press another key " + KeyEvent.keyCodeToString(keyCode));
            }
            reset();
        }
        mDownKeyCode = keyCode;

        // Picks a new rule, return if no rule picked.
        if (mActiveRule == null) {
            final int count = mRules.size();
            for (int index = 0; index < count; index++) {
                final SingleKeyRule rule = mRules.get(index);
                if (rule.shouldInterceptKey(keyCode)) {
                    if (DEBUG) {
                        Log.i(TAG, "Intercept key by rule " + rule);
                    }
                    mActiveRule = rule;
                    break;
                }
            }
        }
        if (mActiveRule == null) {
            return;
        }

        final long eventTime = event.getEventTime();
        if (mKeyPressCounter == 0) {
            if (mActiveRule.supportLongPress()) {
                final Message msg = mHandler.obtainMessage(MSG_KEY_LONG_PRESS, keyCode, 0,
                        eventTime);
                msg.setAsynchronous(true);
                mHandler.sendMessageDelayed(msg, mLongPressTimeout);
            }

            if (mActiveRule.supportVeryLongPress()) {
                final Message msg = mHandler.obtainMessage(MSG_KEY_VERY_LONG_PRESS, keyCode, 0,
                        eventTime);
                msg.setAsynchronous(true);
                mHandler.sendMessageDelayed(msg, mVeryLongPressTimeout);
            }
        } else {
            mHandler.removeMessages(MSG_KEY_LONG_PRESS);
            mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
            mHandler.removeMessages(MSG_KEY_DELAYED_PRESS);

            // Trigger multi press immediately when reach max count.( > 1)
            if (mKeyPressCounter == mActiveRule.getMaxMultiPressCount() - 1) {
                if (DEBUG) {
                    Log.i(TAG, "Trigger multi press " + mActiveRule.toString() + " for it"
                            + " reach the max count " + mKeyPressCounter);
                }
                mActiveRule.onMultiPress(eventTime, mKeyPressCounter + 1);
                mKeyPressCounter = 0;
            }
        }
    }

    private boolean interceptKeyUp(KeyEvent event) {
        mHandler.removeMessages(MSG_KEY_LONG_PRESS);
        mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
        mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN;
        if (mActiveRule == null) {
            return false;
        }

        if (mHandledByLongPress) {
            mHandledByLongPress = false;
            mKeyPressCounter = 0;
            return true;
        }

        final long downTime = event.getDownTime();
        if (event.getKeyCode() == mActiveRule.mKeyCode) {
            // Directly trigger short press when max count is 1.
            if (mActiveRule.getMaxMultiPressCount() == 1) {
                if (DEBUG) {
                    Log.i(TAG, "press key " + KeyEvent.keyCodeToString(event.getKeyCode()));
                }
                mActiveRule.onPress(downTime);
                return true;
            }

            // This could be a multi-press.  Wait a little bit longer to confirm.
            mKeyPressCounter++;
            Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, mActiveRule.mKeyCode,
                    mKeyPressCounter, downTime);
            msg.setAsynchronous(true);
            mHandler.sendMessageDelayed(msg, MULTI_PRESS_TIMEOUT);
            return true;
        }
        reset();
        return false;
    }

    int getKeyPressCounter(int keyCode) {
        if (mActiveRule != null && mActiveRule.mKeyCode == keyCode) {
            return mKeyPressCounter;
        } else {
            return 0;
        }
    }

    void reset() {
        if (mActiveRule != null) {
            if (mDownKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
                mHandler.removeMessages(MSG_KEY_LONG_PRESS);
                mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
            }

            if (mKeyPressCounter > 0) {
                mHandler.removeMessages(MSG_KEY_DELAYED_PRESS);
                mKeyPressCounter = 0;
            }
            mActiveRule = null;
        }

        mHandledByLongPress = false;
        mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN;
    }

    boolean isKeyIntercepted(int keyCode) {
        if (mActiveRule != null && mActiveRule.shouldInterceptKey(keyCode)) {
            return mHandledByLongPress;
        }
        return false;
    }

    private class KeyHandler extends Handler {
        KeyHandler() {
            super(Looper.getMainLooper());
        }

        @Override
        public void handleMessage(Message msg) {
            if (mActiveRule == null) {
                return;
            }
            final int keyCode = msg.arg1;
            final long eventTime = (long) msg.obj;
            switch(msg.what) {
                case MSG_KEY_LONG_PRESS:
                    if (DEBUG) {
                        Log.i(TAG, "Detect long press " + KeyEvent.keyCodeToString(keyCode));
                    }
                    mHandledByLongPress = true;
                    mActiveRule.onLongPress(eventTime);
                    break;
                case MSG_KEY_VERY_LONG_PRESS:
                    if (DEBUG) {
                        Log.i(TAG, "Detect very long press "
                                + KeyEvent.keyCodeToString(keyCode));
                    }
                    mHandledByLongPress = true;
                    mActiveRule.onVeryLongPress(eventTime);
                    break;
                case MSG_KEY_DELAYED_PRESS:
                    if (DEBUG) {
                        Log.i(TAG, "Detect press " + KeyEvent.keyCodeToString(keyCode)
                                + ", count " + mKeyPressCounter);
                    }
                    if (mKeyPressCounter == 1) {
                        mActiveRule.onPress(eventTime);
                    } else {
                        mActiveRule.onMultiPress(eventTime, mKeyPressCounter);
                    }
                    mKeyPressCounter = 0;
                    break;
            }
        }
    }
}
+152 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2021 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.
 */

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_POWER;

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

import static com.android.server.policy.SingleKeyGestureDetector.KEY_LONGPRESS;
import static com.android.server.policy.SingleKeyGestureDetector.KEY_VERYLONGPRESS;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import android.app.Instrumentation;
import android.content.Context;
import android.os.SystemClock;
import android.view.KeyEvent;
import android.view.ViewConfiguration;

import org.junit.Before;
import org.junit.Test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Test class for {@link SingleKeyGestureDetector}.
 *
 * Build/Install/Run:
 *  atest WmTests:SingleKeyGestureTests
 */
public class SingleKeyGestureTests {
    private SingleKeyGestureDetector mDetector;

    private int mMaxMultiPressPowerCount = 2;

    private CountDownLatch mShortPressed = new CountDownLatch(1);
    private CountDownLatch mLongPressed = new CountDownLatch(1);
    private CountDownLatch mVeryLongPressed = new CountDownLatch(1);
    private CountDownLatch mMultiPressed = new CountDownLatch(1);

    private final Instrumentation mInstrumentation = getInstrumentation();
    private final Context mContext = mInstrumentation.getTargetContext();
    private long mWaitTimeout;
    private long mLongPressTime;
    private long mVeryLongPressTime;

    @Before
    public void setUp() {
        mDetector = new SingleKeyGestureDetector(mContext);
        initSingleKeyGestureRules();
        mWaitTimeout = ViewConfiguration.getMultiPressTimeout() + 50;
        mLongPressTime = ViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout() + 50;
        mVeryLongPressTime = mContext.getResources().getInteger(
                com.android.internal.R.integer.config_veryLongPressTimeout) + 50;
    }

    private void initSingleKeyGestureRules() {
        mDetector.addRule(new SingleKeyGestureDetector.SingleKeyRule(KEYCODE_POWER,
                KEY_LONGPRESS | KEY_VERYLONGPRESS) {
            @Override
            int getMaxMultiPressCount() {
                return mMaxMultiPressPowerCount;
            }
            @Override
            public void onPress(long downTime) {
                mShortPressed.countDown();
            }

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

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

            @Override
            void onMultiPress(long downTime, int count) {
                mMultiPressed.countDown();
                assertEquals(mMaxMultiPressPowerCount, count);
            }
        });
    }

    private void pressKey(long eventTime, int keyCode, long pressTime) {
        final KeyEvent keyDown = new KeyEvent(eventTime, eventTime, ACTION_DOWN,
                keyCode, 0 /* repeat */, 0 /* metaState */);
        mDetector.interceptKey(keyDown);

        // keep press down.
        try {
            Thread.sleep(pressTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        eventTime += pressTime;
        final KeyEvent keyUp = new KeyEvent(eventTime, eventTime, ACTION_UP,
                keyCode, 0 /* repeat */, 0 /* metaState */);

        mDetector.interceptKey(keyUp);
    }

    @Test
    public void testShortPress() throws InterruptedException {
        final long eventTime = SystemClock.uptimeMillis();
        pressKey(eventTime, KEYCODE_POWER, 0 /* pressTime */);
        assertTrue(mShortPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
    }

    @Test
    public void testLongPress() throws InterruptedException {
        final long eventTime = SystemClock.uptimeMillis();
        pressKey(eventTime, KEYCODE_POWER, mLongPressTime);
        assertTrue(mLongPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
    }

    @Test
    public void testVeryLongPress() throws InterruptedException {
        final long eventTime = SystemClock.uptimeMillis();
        pressKey(eventTime, KEYCODE_POWER, mVeryLongPressTime);
        assertTrue(mVeryLongPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
    }

    @Test
    public void testMultiPress() throws InterruptedException {
        final long eventTime = SystemClock.uptimeMillis();
        pressKey(eventTime, KEYCODE_POWER, 0 /* pressTime */);
        pressKey(eventTime, KEYCODE_POWER, 0 /* pressTime */);
        assertTrue(mMultiPressed.await(mWaitTimeout, TimeUnit.MILLISECONDS));
    }
}