Loading services/core/java/com/android/server/policy/KeyCombinationManager.java +10 −6 Original line number Diff line number Diff line Loading @@ -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 int keyCode = event.getKeyCode(); final int count = mActiveRules.size(); Loading @@ -117,9 +119,9 @@ public class KeyCombinationManager { // exceed time from first key down. forAllRules(mActiveRules, (rule)-> rule.cancel()); mActiveRules.clear(); return; return false; } else if (count == 0) { // has some key down but no active rule exist. return; return false; } } Loading @@ -127,7 +129,7 @@ public class KeyCombinationManager { mDownTimes.put(keyCode, eventTime); } else { // ignore old key, maybe a repeat key. return; return false; } if (mDownTimes.size() == 1) { Loading @@ -141,7 +143,7 @@ public class KeyCombinationManager { } else { // Ignore if rule already triggered. if (mTriggeredRule != null) { return; return true; } // check if second key can trigger rule, or remove the non-match rule. Loading @@ -156,6 +158,7 @@ public class KeyCombinationManager { mActiveRules.clear(); if (mTriggeredRule != null) { mActiveRules.add(mTriggeredRule); return true; } } } else { Loading @@ -168,6 +171,7 @@ public class KeyCombinationManager { } } } return false; } /** Loading services/core/java/com/android/server/policy/PhoneWindowManager.java +172 −196 File changed.Preview size limit exceeded, changes collapsed. Show changes services/core/java/com/android/server/policy/SingleKeyGestureDetector.java 0 → 100644 +362 −0 Original line number 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; } } } } services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java 0 → 100644 +152 −0 Original line number 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)); } } Loading
services/core/java/com/android/server/policy/KeyCombinationManager.java +10 −6 Original line number Diff line number Diff line Loading @@ -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 int keyCode = event.getKeyCode(); final int count = mActiveRules.size(); Loading @@ -117,9 +119,9 @@ public class KeyCombinationManager { // exceed time from first key down. forAllRules(mActiveRules, (rule)-> rule.cancel()); mActiveRules.clear(); return; return false; } else if (count == 0) { // has some key down but no active rule exist. return; return false; } } Loading @@ -127,7 +129,7 @@ public class KeyCombinationManager { mDownTimes.put(keyCode, eventTime); } else { // ignore old key, maybe a repeat key. return; return false; } if (mDownTimes.size() == 1) { Loading @@ -141,7 +143,7 @@ public class KeyCombinationManager { } else { // Ignore if rule already triggered. if (mTriggeredRule != null) { return; return true; } // check if second key can trigger rule, or remove the non-match rule. Loading @@ -156,6 +158,7 @@ public class KeyCombinationManager { mActiveRules.clear(); if (mTriggeredRule != null) { mActiveRules.add(mTriggeredRule); return true; } } } else { Loading @@ -168,6 +171,7 @@ public class KeyCombinationManager { } } } return false; } /** Loading
services/core/java/com/android/server/policy/PhoneWindowManager.java +172 −196 File changed.Preview size limit exceeded, changes collapsed. Show changes
services/core/java/com/android/server/policy/SingleKeyGestureDetector.java 0 → 100644 +362 −0 Original line number 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; } } } }
services/tests/wmtests/src/com/android/server/policy/SingleKeyGestureTests.java 0 → 100644 +152 −0 Original line number 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)); } }