blob: 1ef2bf9151e0aa3800892b407d5e242b944f106b [file] [log] [blame]
/*
* 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.io.PrintWriter;
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 = 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 final long mLongPressTimeout;
private final long mVeryLongPressTimeout;
private volatile int mKeyPressCounter;
private boolean mBeganFromNonInteractive = 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 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)
+ ", LongPress=" + supportLongPress()
+ ", VeryLongPress=" + supportVeryLongPress()
+ ", MaxMultiPressCount=" + 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, boolean interactive) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
// Store the non interactive state when first down.
if (mDownKeyCode == KeyEvent.KEYCODE_UNKNOWN || mDownKeyCode != event.getKeyCode()) {
mBeganFromNonInteractive = !interactive;
}
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()));
}
Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, mActiveRule.mKeyCode,
1, downTime);
msg.setAsynchronous(true);
mHandler.sendMessage(msg);
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) {
return mActiveRule != null && mActiveRule.shouldInterceptKey(keyCode);
}
boolean beganFromNonInteractive() {
return mBeganFromNonInteractive;
}
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() {
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);
}
reset();
break;
}
}
}
}