| /* |
| * 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.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.io.PrintWriter; |
| 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 = PhoneWindowManager.DEBUG_INPUT; |
| |
| 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 static final int MSG_KEY_UP = 3; |
| |
| private int mKeyPressCounter; |
| private boolean mBeganFromNonInteractive = false; |
| private boolean mBeganFromDefaultDisplayOn = false; |
| |
| 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 boolean mHandledByLongPress = false; |
| private final Handler mHandler; |
| private long mLastDownTime = 0; |
| |
| static final long MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout(); |
| static long sDefaultLongPressTimeout; |
| static long sDefaultVeryLongPressTimeout; |
| |
| /** |
| * 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, int displayId) { // short press behavior. } |
| * void onLongPress(long eventTime) { // long press behavior. } |
| * void onVeryLongPress(long eventTime) { // very long press behavior. } |
| * void onMultiPress(long downTime, int count, int displayId) { |
| * // multi press behavior. |
| * } |
| * }; |
| * </pre> |
| */ |
| abstract static class SingleKeyRule { |
| private final int mKeyCode; |
| |
| SingleKeyRule(int keyCode) { |
| mKeyCode = keyCode; |
| } |
| |
| /** |
| * True if the rule could intercept the key. |
| */ |
| private boolean shouldInterceptKey(int keyCode) { |
| return keyCode == mKeyCode; |
| } |
| |
| /** |
| * True if the rule support long press. |
| */ |
| boolean supportLongPress() { |
| return false; |
| } |
| |
| /** |
| * True if the rule support very long press. |
| */ |
| boolean supportVeryLongPress() { |
| return false; |
| } |
| |
| /** |
| * 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, int displayId); |
| /** |
| * Callback when multi press (>= 2) has been detected. |
| */ |
| void onMultiPress(long downTime, int count, int displayId) {} |
| /** |
| * Returns the timeout in milliseconds for a long press. |
| * |
| * If multipress is also supported, this should always be greater than the multipress |
| * timeout. If very long press is supported, this should always be less than the very long |
| * press timeout. |
| */ |
| long getLongPressTimeoutMs() { |
| return sDefaultLongPressTimeout; |
| } |
| /** |
| * Callback when long press has been detected. |
| */ |
| void onLongPress(long eventTime) {} |
| /** |
| * Returns the timeout in milliseconds for a very long press. |
| * |
| * If long press is supported, this should always be longer than the long press timeout. |
| */ |
| long getVeryLongPressTimeoutMs() { |
| return sDefaultVeryLongPressTimeout; |
| } |
| /** |
| * Callback when very long press has been detected. |
| */ |
| void onVeryLongPress(long eventTime) {} |
| /** |
| * Callback executed upon each key up event that hasn't been processed by long press. |
| * |
| * @param eventTime the timestamp of this event |
| * @param pressCount the number of presses detected leading up to this key up event |
| * @param displayId the display ID of the event |
| */ |
| void onKeyUp(long eventTime, int pressCount, int displayId) {} |
| |
| @Override |
| public String toString() { |
| return "KeyCode=" + KeyEvent.keyCodeToString(mKeyCode) |
| + ", LongPress=" + supportLongPress() |
| + ", VeryLongPress=" + supportVeryLongPress() |
| + ", MaxMultiPressCount=" + getMaxMultiPressCount(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o instanceof SingleKeyRule) { |
| SingleKeyRule that = (SingleKeyRule) o; |
| return mKeyCode == that.mKeyCode; |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return mKeyCode; |
| } |
| } |
| |
| private record MessageObject(SingleKeyRule activeRule, int keyCode, int pressCount, |
| int displayId) { |
| } |
| |
| static SingleKeyGestureDetector get(Context context, Looper looper) { |
| SingleKeyGestureDetector detector = new SingleKeyGestureDetector(looper); |
| sDefaultLongPressTimeout = context.getResources().getInteger( |
| com.android.internal.R.integer.config_globalActionsKeyTimeout); |
| sDefaultVeryLongPressTimeout = context.getResources().getInteger( |
| com.android.internal.R.integer.config_veryLongPressTimeout); |
| return detector; |
| } |
| |
| private SingleKeyGestureDetector(Looper looper) { |
| mHandler = new KeyHandler(looper); |
| } |
| |
| void addRule(SingleKeyRule rule) { |
| if (mRules.contains(rule)) { |
| throw new IllegalArgumentException("Rule : " + rule + " already exists."); |
| } |
| mRules.add(rule); |
| } |
| |
| void removeRule(SingleKeyRule rule) { |
| mRules.remove(rule); |
| } |
| |
| void interceptKey(KeyEvent event, boolean interactive, boolean defaultDisplayOn) { |
| if (event.getAction() == KeyEvent.ACTION_DOWN) { |
| // Store the non interactive state and display on state when first down. |
| if (mDownKeyCode == KeyEvent.KEYCODE_UNKNOWN || mDownKeyCode != event.getKeyCode()) { |
| mBeganFromNonInteractive = !interactive; |
| mBeganFromDefaultDisplayOn = defaultDisplayOn; |
| } |
| 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); |
| MessageObject object = new MessageObject(mActiveRule, keyCode, /* pressCount= */ 1, |
| event.getDisplayId()); |
| final Message msg = mHandler.obtainMessage(MSG_KEY_LONG_PRESS, object); |
| msg.setAsynchronous(true); |
| mHandler.sendMessage(msg); |
| } |
| 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; |
| } |
| } |
| mLastDownTime = 0; |
| } |
| if (mActiveRule == null) { |
| return; |
| } |
| |
| final long keyDownInterval = event.getDownTime() - mLastDownTime; |
| mLastDownTime = event.getDownTime(); |
| if (keyDownInterval >= MULTI_PRESS_TIMEOUT) { |
| mKeyPressCounter = 1; |
| } else { |
| mKeyPressCounter++; |
| } |
| |
| if (mKeyPressCounter == 1) { |
| if (mActiveRule.supportLongPress()) { |
| MessageObject object = new MessageObject(mActiveRule, keyCode, mKeyPressCounter, |
| event.getDisplayId()); |
| final Message msg = mHandler.obtainMessage(MSG_KEY_LONG_PRESS, object); |
| msg.setAsynchronous(true); |
| mHandler.sendMessageDelayed(msg, mActiveRule.getLongPressTimeoutMs()); |
| } |
| |
| if (mActiveRule.supportVeryLongPress()) { |
| MessageObject object = new MessageObject(mActiveRule, keyCode, mKeyPressCounter, |
| event.getDisplayId()); |
| final Message msg = mHandler.obtainMessage(MSG_KEY_VERY_LONG_PRESS, object); |
| msg.setAsynchronous(true); |
| mHandler.sendMessageDelayed(msg, mActiveRule.getVeryLongPressTimeoutMs()); |
| } |
| } 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 (mActiveRule.getMaxMultiPressCount() > 1 |
| && mKeyPressCounter == mActiveRule.getMaxMultiPressCount()) { |
| if (DEBUG) { |
| Log.i(TAG, "Trigger multi press " + mActiveRule.toString() + " for it" |
| + " reached the max count " + mKeyPressCounter); |
| } |
| MessageObject object = new MessageObject(mActiveRule, keyCode, mKeyPressCounter, |
| event.getDisplayId()); |
| final Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, object); |
| msg.setAsynchronous(true); |
| mHandler.sendMessage(msg); |
| } |
| } |
| } |
| |
| private boolean interceptKeyUp(KeyEvent event) { |
| mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN; |
| if (mActiveRule == null) { |
| return false; |
| } |
| |
| if (!mHandledByLongPress) { |
| final long eventTime = event.getEventTime(); |
| if (eventTime < mLastDownTime + mActiveRule.getLongPressTimeoutMs()) { |
| mHandler.removeMessages(MSG_KEY_LONG_PRESS); |
| } else { |
| mHandledByLongPress = mActiveRule.supportLongPress(); |
| } |
| |
| if (eventTime < mLastDownTime + mActiveRule.getVeryLongPressTimeoutMs()) { |
| mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS); |
| } else { |
| // If long press or very long press (~3.5s) had been handled, we should skip the |
| // short press behavior. |
| mHandledByLongPress |= mActiveRule.supportVeryLongPress(); |
| } |
| } |
| |
| if (mHandledByLongPress) { |
| mHandledByLongPress = false; |
| mKeyPressCounter = 0; |
| mActiveRule = null; |
| return true; |
| } |
| |
| if (event.getKeyCode() == mActiveRule.mKeyCode) { |
| // key-up action should always be triggered if not processed by long press. |
| MessageObject object = new MessageObject(mActiveRule, mActiveRule.mKeyCode, |
| mKeyPressCounter, event.getDisplayId()); |
| Message msgKeyUp = mHandler.obtainMessage(MSG_KEY_UP, object); |
| msgKeyUp.setAsynchronous(true); |
| mHandler.sendMessage(msgKeyUp); |
| |
| // Directly trigger short press when max count is 1. |
| if (mActiveRule.getMaxMultiPressCount() == 1) { |
| if (DEBUG) { |
| Log.i(TAG, "press key " + KeyEvent.keyCodeToString(event.getKeyCode())); |
| } |
| object = new MessageObject(mActiveRule, mActiveRule.mKeyCode, |
| /* pressCount= */ 1, event.getDisplayId()); |
| Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, object); |
| msg.setAsynchronous(true); |
| mHandler.sendMessage(msg); |
| mActiveRule = null; |
| return true; |
| } |
| |
| // This could be a multi-press. Wait a little bit longer to confirm. |
| if (mKeyPressCounter < mActiveRule.getMaxMultiPressCount()) { |
| object = new MessageObject(mActiveRule, mActiveRule.mKeyCode, |
| mKeyPressCounter, event.getDisplayId()); |
| Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, object); |
| 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) { |
| return mActiveRule != null && mActiveRule.shouldInterceptKey(keyCode); |
| } |
| |
| boolean beganFromNonInteractive() { |
| return mBeganFromNonInteractive; |
| } |
| |
| boolean beganFromDefaultDisplayOn() { |
| return mBeganFromDefaultDisplayOn; |
| } |
| |
| void dump(String prefix, PrintWriter pw) { |
| pw.println(prefix + "SingleKey rules:"); |
| for (SingleKeyRule rule : mRules) { |
| pw.println(prefix + " " + rule); |
| } |
| } |
| |
| private class KeyHandler extends Handler { |
| KeyHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| final MessageObject object = (MessageObject) msg.obj; |
| final SingleKeyRule rule = object.activeRule; |
| if (rule == null) { |
| Log.wtf(TAG, "No active rule."); |
| return; |
| } |
| |
| final int keyCode = object.keyCode; |
| final int pressCount = object.pressCount; |
| final int displayId = object.displayId; |
| switch(msg.what) { |
| case MSG_KEY_UP: |
| if (DEBUG) { |
| Log.i(TAG, "Detect key up " + KeyEvent.keyCodeToString(keyCode) |
| + " on display " + displayId); |
| } |
| rule.onKeyUp(mLastDownTime, pressCount, displayId); |
| break; |
| case MSG_KEY_LONG_PRESS: |
| if (DEBUG) { |
| Log.i(TAG, "Detect long press " + KeyEvent.keyCodeToString(keyCode)); |
| } |
| rule.onLongPress(mLastDownTime); |
| break; |
| case MSG_KEY_VERY_LONG_PRESS: |
| if (DEBUG) { |
| Log.i(TAG, "Detect very long press " |
| + KeyEvent.keyCodeToString(keyCode)); |
| } |
| rule.onVeryLongPress(mLastDownTime); |
| break; |
| case MSG_KEY_DELAYED_PRESS: |
| if (DEBUG) { |
| Log.i(TAG, "Detect press " + KeyEvent.keyCodeToString(keyCode) |
| + " on display " + displayId + ", count " + pressCount); |
| } |
| if (pressCount == 1) { |
| rule.onPress(mLastDownTime, displayId); |
| } else { |
| rule.onMultiPress(mLastDownTime, pressCount, displayId); |
| } |
| break; |
| } |
| } |
| } |
| } |