1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.server.policy; 17 18 import static android.view.KeyEvent.KEYCODE_POWER; 19 20 import android.os.SystemClock; 21 import android.util.Log; 22 import android.util.SparseLongArray; 23 import android.view.KeyEvent; 24 25 import com.android.internal.util.ToBooleanFunction; 26 27 import java.io.PrintWriter; 28 import java.util.ArrayList; 29 import java.util.function.Consumer; 30 31 /** 32 * Handles a mapping of two keys combination. 33 */ 34 public class KeyCombinationManager { 35 private static final String TAG = "KeyCombinationManager"; 36 37 // Store the received down time of keycode. 38 private final SparseLongArray mDownTimes = new SparseLongArray(2); 39 private final ArrayList<TwoKeysCombinationRule> mRules = new ArrayList(); 40 41 // Selected rules according to current key down. 42 private final ArrayList<TwoKeysCombinationRule> mActiveRules = new ArrayList(); 43 // The rule has been triggered by current keys. 44 private TwoKeysCombinationRule mTriggeredRule; 45 46 // Keys in a key combination must be pressed within this interval of each other. 47 private static final long COMBINE_KEY_DELAY_MILLIS = 150; 48 49 /** 50 * Rule definition for two keys combination. 51 * E.g : define volume_down + power key. 52 * <pre class="prettyprint"> 53 * TwoKeysCombinationRule rule = 54 * new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) { 55 * boolean preCondition() { // check if it needs to intercept key } 56 * void execute() { // trigger action } 57 * void cancel() { // cancel action } 58 * }; 59 * </pre> 60 */ 61 abstract static class TwoKeysCombinationRule { 62 private int mKeyCode1; 63 private int mKeyCode2; 64 TwoKeysCombinationRule(int keyCode1, int keyCode2)65 TwoKeysCombinationRule(int keyCode1, int keyCode2) { 66 mKeyCode1 = keyCode1; 67 mKeyCode2 = keyCode2; 68 } 69 preCondition()70 boolean preCondition() { 71 return true; 72 } 73 shouldInterceptKey(int keyCode)74 boolean shouldInterceptKey(int keyCode) { 75 return preCondition() && (keyCode == mKeyCode1 || keyCode == mKeyCode2); 76 } 77 shouldInterceptKeys(SparseLongArray downTimes)78 boolean shouldInterceptKeys(SparseLongArray downTimes) { 79 final long now = SystemClock.uptimeMillis(); 80 if (downTimes.get(mKeyCode1) > 0 81 && downTimes.get(mKeyCode2) > 0 82 && now <= downTimes.get(mKeyCode1) + COMBINE_KEY_DELAY_MILLIS 83 && now <= downTimes.get(mKeyCode2) + COMBINE_KEY_DELAY_MILLIS) { 84 return true; 85 } 86 return false; 87 } 88 execute()89 abstract void execute(); cancel()90 abstract void cancel(); 91 92 @Override toString()93 public String toString() { 94 return KeyEvent.keyCodeToString(mKeyCode1) + " + " 95 + KeyEvent.keyCodeToString(mKeyCode2); 96 } 97 } 98 KeyCombinationManager()99 public KeyCombinationManager() { 100 } 101 addRule(TwoKeysCombinationRule rule)102 void addRule(TwoKeysCombinationRule rule) { 103 mRules.add(rule); 104 } 105 106 /** 107 * Check if the key event could be intercepted by combination key rule before it is dispatched 108 * to a window. 109 * Return true if any active rule could be triggered by the key event, otherwise false. 110 */ interceptKey(KeyEvent event, boolean interactive)111 boolean interceptKey(KeyEvent event, boolean interactive) { 112 final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; 113 final int keyCode = event.getKeyCode(); 114 final int count = mActiveRules.size(); 115 final long eventTime = event.getEventTime(); 116 117 if (interactive && down) { 118 if (mDownTimes.size() > 0) { 119 if (count > 0 120 && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) { 121 // exceed time from first key down. 122 forAllRules(mActiveRules, (rule)-> rule.cancel()); 123 mActiveRules.clear(); 124 return false; 125 } else if (count == 0) { // has some key down but no active rule exist. 126 return false; 127 } 128 } 129 130 if (mDownTimes.get(keyCode) == 0) { 131 mDownTimes.put(keyCode, eventTime); 132 } else { 133 // ignore old key, maybe a repeat key. 134 return false; 135 } 136 137 if (mDownTimes.size() == 1) { 138 mTriggeredRule = null; 139 // check first key and pick active rules. 140 forAllRules(mRules, (rule)-> { 141 if (rule.shouldInterceptKey(keyCode)) { 142 mActiveRules.add(rule); 143 } 144 }); 145 } else { 146 // Ignore if rule already triggered. 147 if (mTriggeredRule != null) { 148 return true; 149 } 150 151 // check if second key can trigger rule, or remove the non-match rule. 152 forAllActiveRules((rule) -> { 153 if (!rule.shouldInterceptKeys(mDownTimes)) { 154 return false; 155 } 156 Log.v(TAG, "Performing combination rule : " + rule); 157 rule.execute(); 158 mTriggeredRule = rule; 159 return true; 160 }); 161 mActiveRules.clear(); 162 if (mTriggeredRule != null) { 163 mActiveRules.add(mTriggeredRule); 164 return true; 165 } 166 } 167 } else { 168 mDownTimes.delete(keyCode); 169 for (int index = count - 1; index >= 0; index--) { 170 final TwoKeysCombinationRule rule = mActiveRules.get(index); 171 if (rule.shouldInterceptKey(keyCode)) { 172 rule.cancel(); 173 mActiveRules.remove(index); 174 } 175 } 176 } 177 return false; 178 } 179 180 /** 181 * Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window. 182 */ getKeyInterceptTimeout(int keyCode)183 long getKeyInterceptTimeout(int keyCode) { 184 if (forAllActiveRules((rule) -> rule.shouldInterceptKey(keyCode))) { 185 return mDownTimes.get(keyCode) + COMBINE_KEY_DELAY_MILLIS; 186 } 187 return 0; 188 } 189 190 /** 191 * True if the key event had been handled. 192 */ isKeyConsumed(KeyEvent event)193 boolean isKeyConsumed(KeyEvent event) { 194 if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) { 195 return false; 196 } 197 return mTriggeredRule != null && mTriggeredRule.shouldInterceptKey(event.getKeyCode()); 198 } 199 200 /** 201 * True if power key is the candidate. 202 */ isPowerKeyIntercepted()203 boolean isPowerKeyIntercepted() { 204 if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) { 205 // return false if only if power key pressed. 206 return mDownTimes.size() > 1 || mDownTimes.get(KEYCODE_POWER) == 0; 207 } 208 return false; 209 } 210 211 /** 212 * Traverse each item of rules. 213 */ forAllRules( ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback)214 private void forAllRules( 215 ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback) { 216 final int count = rules.size(); 217 for (int index = 0; index < count; index++) { 218 final TwoKeysCombinationRule rule = rules.get(index); 219 callback.accept(rule); 220 } 221 } 222 223 /** 224 * Traverse each item of active rules until some rule can be applied, otherwise return false. 225 */ forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback)226 private boolean forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback) { 227 final int count = mActiveRules.size(); 228 for (int index = 0; index < count; index++) { 229 final TwoKeysCombinationRule rule = mActiveRules.get(index); 230 if (callback.apply(rule)) { 231 return true; 232 } 233 } 234 return false; 235 } 236 dump(String prefix, PrintWriter pw)237 void dump(String prefix, PrintWriter pw) { 238 pw.println(prefix + "KeyCombination rules:"); 239 forAllRules(mRules, (rule)-> { 240 pw.println(prefix + " " + rule); 241 }); 242 } 243 } 244