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.Handler; 21 import android.os.SystemClock; 22 import android.util.Log; 23 import android.util.SparseLongArray; 24 import android.view.KeyEvent; 25 26 import com.android.internal.annotations.GuardedBy; 27 import com.android.internal.util.ToBooleanFunction; 28 29 import java.io.PrintWriter; 30 import java.util.ArrayList; 31 import java.util.function.Consumer; 32 33 /** 34 * Handles a mapping of two keys combination. 35 */ 36 public class KeyCombinationManager { 37 private static final String TAG = "KeyCombinationManager"; 38 39 // Store the received down time of keycode. 40 @GuardedBy("mLock") 41 private final SparseLongArray mDownTimes = new SparseLongArray(2); 42 private final ArrayList<TwoKeysCombinationRule> mRules = new ArrayList(); 43 44 // Selected rules according to current key down. 45 private final Object mLock = new Object(); 46 @GuardedBy("mLock") 47 private final ArrayList<TwoKeysCombinationRule> mActiveRules = new ArrayList(); 48 // The rule has been triggered by current keys. 49 @GuardedBy("mLock") 50 private TwoKeysCombinationRule mTriggeredRule; 51 private final Handler mHandler; 52 53 // Keys in a key combination must be pressed within this interval of each other. 54 private static final long COMBINE_KEY_DELAY_MILLIS = 150; 55 56 /** 57 * Rule definition for two keys combination. 58 * E.g : define volume_down + power key. 59 * <pre class="prettyprint"> 60 * TwoKeysCombinationRule rule = 61 * new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) { 62 * boolean preCondition() { // check if it needs to intercept key } 63 * void execute() { // trigger action } 64 * void cancel() { // cancel action } 65 * }; 66 * </pre> 67 */ 68 abstract static class TwoKeysCombinationRule { 69 private int mKeyCode1; 70 private int mKeyCode2; 71 TwoKeysCombinationRule(int keyCode1, int keyCode2)72 TwoKeysCombinationRule(int keyCode1, int keyCode2) { 73 mKeyCode1 = keyCode1; 74 mKeyCode2 = keyCode2; 75 } 76 preCondition()77 boolean preCondition() { 78 return true; 79 } 80 shouldInterceptKey(int keyCode)81 boolean shouldInterceptKey(int keyCode) { 82 return preCondition() && (keyCode == mKeyCode1 || keyCode == mKeyCode2); 83 } 84 shouldInterceptKeys(SparseLongArray downTimes)85 boolean shouldInterceptKeys(SparseLongArray downTimes) { 86 final long now = SystemClock.uptimeMillis(); 87 if (downTimes.get(mKeyCode1) > 0 88 && downTimes.get(mKeyCode2) > 0 89 && now <= downTimes.get(mKeyCode1) + COMBINE_KEY_DELAY_MILLIS 90 && now <= downTimes.get(mKeyCode2) + COMBINE_KEY_DELAY_MILLIS) { 91 return true; 92 } 93 return false; 94 } 95 96 // The excessive delay before it dispatching to client. getKeyInterceptDelayMs()97 long getKeyInterceptDelayMs() { 98 return COMBINE_KEY_DELAY_MILLIS; 99 } 100 execute()101 abstract void execute(); cancel()102 abstract void cancel(); 103 104 @Override toString()105 public String toString() { 106 return KeyEvent.keyCodeToString(mKeyCode1) + " + " 107 + KeyEvent.keyCodeToString(mKeyCode2); 108 } 109 } 110 KeyCombinationManager(Handler handler)111 public KeyCombinationManager(Handler handler) { 112 mHandler = handler; 113 } 114 addRule(TwoKeysCombinationRule rule)115 void addRule(TwoKeysCombinationRule rule) { 116 mRules.add(rule); 117 } 118 119 /** 120 * Check if the key event could be intercepted by combination key rule before it is dispatched 121 * to a window. 122 * Return true if any active rule could be triggered by the key event, otherwise false. 123 */ interceptKey(KeyEvent event, boolean interactive)124 boolean interceptKey(KeyEvent event, boolean interactive) { 125 synchronized (mLock) { 126 return interceptKeyLocked(event, interactive); 127 } 128 } 129 interceptKeyLocked(KeyEvent event, boolean interactive)130 private boolean interceptKeyLocked(KeyEvent event, boolean interactive) { 131 final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; 132 final int keyCode = event.getKeyCode(); 133 final int count = mActiveRules.size(); 134 final long eventTime = event.getEventTime(); 135 136 if (interactive && down) { 137 if (mDownTimes.size() > 0) { 138 if (count > 0 139 && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) { 140 // exceed time from first key down. 141 forAllRules(mActiveRules, (rule)-> rule.cancel()); 142 mActiveRules.clear(); 143 return false; 144 } else if (count == 0) { // has some key down but no active rule exist. 145 return false; 146 } 147 } 148 149 if (mDownTimes.get(keyCode) == 0) { 150 mDownTimes.put(keyCode, eventTime); 151 } else { 152 // ignore old key, maybe a repeat key. 153 return false; 154 } 155 156 if (mDownTimes.size() == 1) { 157 mTriggeredRule = null; 158 // check first key and pick active rules. 159 forAllRules(mRules, (rule)-> { 160 if (rule.shouldInterceptKey(keyCode)) { 161 mActiveRules.add(rule); 162 } 163 }); 164 } else { 165 // Ignore if rule already triggered. 166 if (mTriggeredRule != null) { 167 return true; 168 } 169 170 // check if second key can trigger rule, or remove the non-match rule. 171 forAllActiveRules((rule) -> { 172 if (!rule.shouldInterceptKeys(mDownTimes)) { 173 return false; 174 } 175 Log.v(TAG, "Performing combination rule : " + rule); 176 mHandler.post(rule::execute); 177 mTriggeredRule = rule; 178 return true; 179 }); 180 mActiveRules.clear(); 181 if (mTriggeredRule != null) { 182 mActiveRules.add(mTriggeredRule); 183 return true; 184 } 185 } 186 } else { 187 mDownTimes.delete(keyCode); 188 for (int index = count - 1; index >= 0; index--) { 189 final TwoKeysCombinationRule rule = mActiveRules.get(index); 190 if (rule.shouldInterceptKey(keyCode)) { 191 mHandler.post(rule::cancel); 192 mActiveRules.remove(index); 193 } 194 } 195 } 196 return false; 197 } 198 199 /** 200 * Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window. 201 */ getKeyInterceptTimeout(int keyCode)202 long getKeyInterceptTimeout(int keyCode) { 203 synchronized (mLock) { 204 if (mDownTimes.get(keyCode) == 0) { 205 return 0; 206 } 207 long delayMs = 0; 208 for (final TwoKeysCombinationRule rule : mActiveRules) { 209 if (rule.shouldInterceptKey(keyCode)) { 210 delayMs = Math.max(delayMs, rule.getKeyInterceptDelayMs()); 211 } 212 } 213 // Make sure the delay is less than COMBINE_KEY_DELAY_MILLIS. 214 delayMs = Math.min(delayMs, COMBINE_KEY_DELAY_MILLIS); 215 return mDownTimes.get(keyCode) + delayMs; 216 } 217 } 218 219 /** 220 * True if the key event had been handled. 221 */ isKeyConsumed(KeyEvent event)222 boolean isKeyConsumed(KeyEvent event) { 223 synchronized (mLock) { 224 if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) { 225 return false; 226 } 227 return mTriggeredRule != null && mTriggeredRule.shouldInterceptKey(event.getKeyCode()); 228 } 229 } 230 231 /** 232 * True if power key is the candidate. 233 */ isPowerKeyIntercepted()234 boolean isPowerKeyIntercepted() { 235 synchronized (mLock) { 236 if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) { 237 // return false if only if power key pressed. 238 return mDownTimes.size() > 1 || mDownTimes.get(KEYCODE_POWER) == 0; 239 } 240 return false; 241 } 242 } 243 244 /** 245 * Traverse each item of rules. 246 */ forAllRules( ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback)247 private void forAllRules( 248 ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback) { 249 final int count = rules.size(); 250 for (int index = 0; index < count; index++) { 251 final TwoKeysCombinationRule rule = rules.get(index); 252 callback.accept(rule); 253 } 254 } 255 256 /** 257 * Traverse each item of active rules until some rule can be applied, otherwise return false. 258 */ forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback)259 private boolean forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback) { 260 final int count = mActiveRules.size(); 261 for (int index = 0; index < count; index++) { 262 final TwoKeysCombinationRule rule = mActiveRules.get(index); 263 if (callback.apply(rule)) { 264 return true; 265 } 266 } 267 return false; 268 } 269 dump(String prefix, PrintWriter pw)270 void dump(String prefix, PrintWriter pw) { 271 pw.println(prefix + "KeyCombination rules:"); 272 forAllRules(mRules, (rule)-> { 273 pw.println(prefix + " " + rule); 274 }); 275 } 276 } 277