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 @Override equals(Object o)111 public boolean equals(Object o) { 112 if (this == o) { 113 return true; 114 } 115 if (o instanceof TwoKeysCombinationRule) { 116 TwoKeysCombinationRule that = (TwoKeysCombinationRule) o; 117 return (mKeyCode1 == that.mKeyCode1 && mKeyCode2 == that.mKeyCode2) || ( 118 mKeyCode1 == that.mKeyCode2 && mKeyCode2 == that.mKeyCode1); 119 } 120 return false; 121 } 122 123 @Override hashCode()124 public int hashCode() { 125 int result = mKeyCode1; 126 result = 31 * result + mKeyCode2; 127 return result; 128 } 129 } 130 KeyCombinationManager(Handler handler)131 KeyCombinationManager(Handler handler) { 132 mHandler = handler; 133 } 134 addRule(TwoKeysCombinationRule rule)135 void addRule(TwoKeysCombinationRule rule) { 136 if (mRules.contains(rule)) { 137 throw new IllegalArgumentException("Rule : " + rule + " already exists."); 138 } 139 mRules.add(rule); 140 } 141 removeRule(TwoKeysCombinationRule rule)142 void removeRule(TwoKeysCombinationRule rule) { 143 mRules.remove(rule); 144 } 145 146 /** 147 * Check if the key event could be intercepted by combination key rule before it is dispatched 148 * to a window. 149 * Return true if any active rule could be triggered by the key event, otherwise false. 150 */ interceptKey(KeyEvent event, boolean interactive)151 boolean interceptKey(KeyEvent event, boolean interactive) { 152 synchronized (mLock) { 153 return interceptKeyLocked(event, interactive); 154 } 155 } 156 interceptKeyLocked(KeyEvent event, boolean interactive)157 private boolean interceptKeyLocked(KeyEvent event, boolean interactive) { 158 final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; 159 final int keyCode = event.getKeyCode(); 160 final int count = mActiveRules.size(); 161 final long eventTime = event.getEventTime(); 162 163 if (interactive && down) { 164 if (mDownTimes.size() > 0) { 165 if (count > 0 166 && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) { 167 // exceed time from first key down. 168 forAllRules(mActiveRules, (rule)-> rule.cancel()); 169 mActiveRules.clear(); 170 return false; 171 } else if (count == 0) { // has some key down but no active rule exist. 172 return false; 173 } 174 } 175 176 if (mDownTimes.get(keyCode) == 0) { 177 mDownTimes.put(keyCode, eventTime); 178 } else { 179 // ignore old key, maybe a repeat key. 180 return false; 181 } 182 183 if (mDownTimes.size() == 1) { 184 mTriggeredRule = null; 185 // check first key and pick active rules. 186 forAllRules(mRules, (rule)-> { 187 if (rule.shouldInterceptKey(keyCode)) { 188 mActiveRules.add(rule); 189 } 190 }); 191 } else { 192 // Ignore if rule already triggered. 193 if (mTriggeredRule != null) { 194 return true; 195 } 196 197 // check if second key can trigger rule, or remove the non-match rule. 198 forAllActiveRules((rule) -> { 199 if (!rule.shouldInterceptKeys(mDownTimes)) { 200 return false; 201 } 202 Log.v(TAG, "Performing combination rule : " + rule); 203 mHandler.post(rule::execute); 204 mTriggeredRule = rule; 205 return true; 206 }); 207 mActiveRules.clear(); 208 if (mTriggeredRule != null) { 209 mActiveRules.add(mTriggeredRule); 210 return true; 211 } 212 } 213 } else { 214 mDownTimes.delete(keyCode); 215 for (int index = count - 1; index >= 0; index--) { 216 final TwoKeysCombinationRule rule = mActiveRules.get(index); 217 if (rule.shouldInterceptKey(keyCode)) { 218 mHandler.post(rule::cancel); 219 mActiveRules.remove(index); 220 } 221 } 222 } 223 return false; 224 } 225 226 /** 227 * Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window. 228 */ getKeyInterceptTimeout(int keyCode)229 long getKeyInterceptTimeout(int keyCode) { 230 synchronized (mLock) { 231 if (mDownTimes.get(keyCode) == 0) { 232 return 0; 233 } 234 long delayMs = 0; 235 for (final TwoKeysCombinationRule rule : mActiveRules) { 236 if (rule.shouldInterceptKey(keyCode)) { 237 delayMs = Math.max(delayMs, rule.getKeyInterceptDelayMs()); 238 } 239 } 240 // Make sure the delay is less than COMBINE_KEY_DELAY_MILLIS. 241 delayMs = Math.min(delayMs, COMBINE_KEY_DELAY_MILLIS); 242 return mDownTimes.get(keyCode) + delayMs; 243 } 244 } 245 246 /** 247 * True if the key event had been handled. 248 */ isKeyConsumed(KeyEvent event)249 boolean isKeyConsumed(KeyEvent event) { 250 synchronized (mLock) { 251 if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) { 252 return false; 253 } 254 return mTriggeredRule != null && mTriggeredRule.shouldInterceptKey(event.getKeyCode()); 255 } 256 } 257 258 /** 259 * True if power key is the candidate. 260 */ isPowerKeyIntercepted()261 boolean isPowerKeyIntercepted() { 262 synchronized (mLock) { 263 if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) { 264 // return false if only if power key pressed. 265 return mDownTimes.size() > 1 || mDownTimes.get(KEYCODE_POWER) == 0; 266 } 267 return false; 268 } 269 } 270 271 /** 272 * Traverse each item of rules. 273 */ forAllRules( ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback)274 private void forAllRules( 275 ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback) { 276 final int count = rules.size(); 277 for (int index = 0; index < count; index++) { 278 final TwoKeysCombinationRule rule = rules.get(index); 279 callback.accept(rule); 280 } 281 } 282 283 /** 284 * Traverse each item of active rules until some rule can be applied, otherwise return false. 285 */ forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback)286 private boolean forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback) { 287 final int count = mActiveRules.size(); 288 for (int index = 0; index < count; index++) { 289 final TwoKeysCombinationRule rule = mActiveRules.get(index); 290 if (callback.apply(rule)) { 291 return true; 292 } 293 } 294 return false; 295 } 296 dump(String prefix, PrintWriter pw)297 void dump(String prefix, PrintWriter pw) { 298 pw.println(prefix + "KeyCombination rules:"); 299 forAllRules(mRules, (rule)-> { 300 pw.println(prefix + " " + rule); 301 }); 302 } 303 } 304