| /* |
| * Copyright (C) 2020 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.KEYCODE_POWER; |
| |
| import android.os.Handler; |
| import android.os.SystemClock; |
| import android.util.Log; |
| import android.util.SparseLongArray; |
| import android.view.KeyEvent; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.util.ToBooleanFunction; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.function.Consumer; |
| |
| /** |
| * Handles a mapping of two keys combination. |
| */ |
| public class KeyCombinationManager { |
| private static final String TAG = "KeyCombinationManager"; |
| |
| // Store the received down time of keycode. |
| @GuardedBy("mLock") |
| private final SparseLongArray mDownTimes = new SparseLongArray(2); |
| private final ArrayList<TwoKeysCombinationRule> mRules = new ArrayList(); |
| |
| // Selected rules according to current key down. |
| private final Object mLock = new Object(); |
| @GuardedBy("mLock") |
| private final ArrayList<TwoKeysCombinationRule> mActiveRules = new ArrayList(); |
| // The rule has been triggered by current keys. |
| @GuardedBy("mLock") |
| private TwoKeysCombinationRule mTriggeredRule; |
| private final Handler mHandler; |
| |
| // Keys in a key combination must be pressed within this interval of each other. |
| private static final long COMBINE_KEY_DELAY_MILLIS = 150; |
| |
| /** |
| * Rule definition for two keys combination. |
| * E.g : define volume_down + power key. |
| * <pre class="prettyprint"> |
| * TwoKeysCombinationRule rule = |
| * new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) { |
| * boolean preCondition() { // check if it needs to intercept key } |
| * void execute() { // trigger action } |
| * void cancel() { // cancel action } |
| * }; |
| * </pre> |
| */ |
| abstract static class TwoKeysCombinationRule { |
| private int mKeyCode1; |
| private int mKeyCode2; |
| |
| TwoKeysCombinationRule(int keyCode1, int keyCode2) { |
| mKeyCode1 = keyCode1; |
| mKeyCode2 = keyCode2; |
| } |
| |
| boolean preCondition() { |
| return true; |
| } |
| |
| boolean shouldInterceptKey(int keyCode) { |
| return preCondition() && (keyCode == mKeyCode1 || keyCode == mKeyCode2); |
| } |
| |
| boolean shouldInterceptKeys(SparseLongArray downTimes) { |
| final long now = SystemClock.uptimeMillis(); |
| if (downTimes.get(mKeyCode1) > 0 |
| && downTimes.get(mKeyCode2) > 0 |
| && now <= downTimes.get(mKeyCode1) + COMBINE_KEY_DELAY_MILLIS |
| && now <= downTimes.get(mKeyCode2) + COMBINE_KEY_DELAY_MILLIS) { |
| return true; |
| } |
| return false; |
| } |
| |
| // The excessive delay before it dispatching to client. |
| long getKeyInterceptDelayMs() { |
| return COMBINE_KEY_DELAY_MILLIS; |
| } |
| |
| abstract void execute(); |
| abstract void cancel(); |
| |
| @Override |
| public String toString() { |
| return KeyEvent.keyCodeToString(mKeyCode1) + " + " |
| + KeyEvent.keyCodeToString(mKeyCode2); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o instanceof TwoKeysCombinationRule) { |
| TwoKeysCombinationRule that = (TwoKeysCombinationRule) o; |
| return (mKeyCode1 == that.mKeyCode1 && mKeyCode2 == that.mKeyCode2) || ( |
| mKeyCode1 == that.mKeyCode2 && mKeyCode2 == that.mKeyCode1); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = mKeyCode1; |
| result = 31 * result + mKeyCode2; |
| return result; |
| } |
| } |
| |
| KeyCombinationManager(Handler handler) { |
| mHandler = handler; |
| } |
| |
| void addRule(TwoKeysCombinationRule rule) { |
| if (mRules.contains(rule)) { |
| throw new IllegalArgumentException("Rule : " + rule + " already exists."); |
| } |
| mRules.add(rule); |
| } |
| |
| void removeRule(TwoKeysCombinationRule rule) { |
| mRules.remove(rule); |
| } |
| |
| /** |
| * 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. |
| */ |
| boolean interceptKey(KeyEvent event, boolean interactive) { |
| synchronized (mLock) { |
| return interceptKeyLocked(event, interactive); |
| } |
| } |
| |
| private boolean interceptKeyLocked(KeyEvent event, boolean interactive) { |
| final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; |
| final int keyCode = event.getKeyCode(); |
| final int count = mActiveRules.size(); |
| final long eventTime = event.getEventTime(); |
| |
| if (interactive && down) { |
| if (mDownTimes.size() > 0) { |
| if (count > 0 |
| && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) { |
| // exceed time from first key down. |
| forAllRules(mActiveRules, (rule)-> rule.cancel()); |
| mActiveRules.clear(); |
| return false; |
| } else if (count == 0) { // has some key down but no active rule exist. |
| return false; |
| } |
| } |
| |
| if (mDownTimes.get(keyCode) == 0) { |
| mDownTimes.put(keyCode, eventTime); |
| } else { |
| // ignore old key, maybe a repeat key. |
| return false; |
| } |
| |
| if (mDownTimes.size() == 1) { |
| mTriggeredRule = null; |
| // check first key and pick active rules. |
| forAllRules(mRules, (rule)-> { |
| if (rule.shouldInterceptKey(keyCode)) { |
| mActiveRules.add(rule); |
| } |
| }); |
| } else { |
| // Ignore if rule already triggered. |
| if (mTriggeredRule != null) { |
| return true; |
| } |
| |
| // check if second key can trigger rule, or remove the non-match rule. |
| forAllActiveRules((rule) -> { |
| if (!rule.shouldInterceptKeys(mDownTimes)) { |
| return false; |
| } |
| Log.v(TAG, "Performing combination rule : " + rule); |
| mHandler.post(rule::execute); |
| mTriggeredRule = rule; |
| return true; |
| }); |
| mActiveRules.clear(); |
| if (mTriggeredRule != null) { |
| mActiveRules.add(mTriggeredRule); |
| return true; |
| } |
| } |
| } else { |
| mDownTimes.delete(keyCode); |
| for (int index = count - 1; index >= 0; index--) { |
| final TwoKeysCombinationRule rule = mActiveRules.get(index); |
| if (rule.shouldInterceptKey(keyCode)) { |
| mHandler.post(rule::cancel); |
| mActiveRules.remove(index); |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window. |
| */ |
| long getKeyInterceptTimeout(int keyCode) { |
| synchronized (mLock) { |
| if (mDownTimes.get(keyCode) == 0) { |
| return 0; |
| } |
| long delayMs = 0; |
| for (final TwoKeysCombinationRule rule : mActiveRules) { |
| if (rule.shouldInterceptKey(keyCode)) { |
| delayMs = Math.max(delayMs, rule.getKeyInterceptDelayMs()); |
| } |
| } |
| // Make sure the delay is less than COMBINE_KEY_DELAY_MILLIS. |
| delayMs = Math.min(delayMs, COMBINE_KEY_DELAY_MILLIS); |
| return mDownTimes.get(keyCode) + delayMs; |
| } |
| } |
| |
| /** |
| * True if the key event had been handled. |
| */ |
| boolean isKeyConsumed(KeyEvent event) { |
| synchronized (mLock) { |
| if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) { |
| return false; |
| } |
| return mTriggeredRule != null && mTriggeredRule.shouldInterceptKey(event.getKeyCode()); |
| } |
| } |
| |
| /** |
| * True if power key is the candidate. |
| */ |
| boolean isPowerKeyIntercepted() { |
| synchronized (mLock) { |
| if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) { |
| // return false if only if power key pressed. |
| return mDownTimes.size() > 1 || mDownTimes.get(KEYCODE_POWER) == 0; |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * Traverse each item of rules. |
| */ |
| private void forAllRules( |
| ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback) { |
| final int count = rules.size(); |
| for (int index = 0; index < count; index++) { |
| final TwoKeysCombinationRule rule = rules.get(index); |
| callback.accept(rule); |
| } |
| } |
| |
| /** |
| * Traverse each item of active rules until some rule can be applied, otherwise return false. |
| */ |
| private boolean forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback) { |
| final int count = mActiveRules.size(); |
| for (int index = 0; index < count; index++) { |
| final TwoKeysCombinationRule rule = mActiveRules.get(index); |
| if (callback.apply(rule)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void dump(String prefix, PrintWriter pw) { |
| pw.println(prefix + "KeyCombination rules:"); |
| forAllRules(mRules, (rule)-> { |
| pw.println(prefix + " " + rule); |
| }); |
| } |
| } |