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 = new Handler(); 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 execute()96 abstract void execute(); cancel()97 abstract void cancel(); 98 99 @Override toString()100 public String toString() { 101 return KeyEvent.keyCodeToString(mKeyCode1) + " + " 102 + KeyEvent.keyCodeToString(mKeyCode2); 103 } 104 } 105 KeyCombinationManager()106 public KeyCombinationManager() { 107 } 108 addRule(TwoKeysCombinationRule rule)109 void addRule(TwoKeysCombinationRule rule) { 110 mRules.add(rule); 111 } 112 113 /** 114 * Check if the key event could be intercepted by combination key rule before it is dispatched 115 * to a window. 116 * Return true if any active rule could be triggered by the key event, otherwise false. 117 */ interceptKey(KeyEvent event, boolean interactive)118 boolean interceptKey(KeyEvent event, boolean interactive) { 119 synchronized (mLock) { 120 return interceptKeyLocked(event, interactive); 121 } 122 } 123 interceptKeyLocked(KeyEvent event, boolean interactive)124 private boolean interceptKeyLocked(KeyEvent event, boolean interactive) { 125 final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; 126 final int keyCode = event.getKeyCode(); 127 final int count = mActiveRules.size(); 128 final long eventTime = event.getEventTime(); 129 130 if (interactive && down) { 131 if (mDownTimes.size() > 0) { 132 if (count > 0 133 && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) { 134 // exceed time from first key down. 135 forAllRules(mActiveRules, (rule)-> rule.cancel()); 136 mActiveRules.clear(); 137 return false; 138 } else if (count == 0) { // has some key down but no active rule exist. 139 return false; 140 } 141 } 142 143 if (mDownTimes.get(keyCode) == 0) { 144 mDownTimes.put(keyCode, eventTime); 145 } else { 146 // ignore old key, maybe a repeat key. 147 return false; 148 } 149 150 if (mDownTimes.size() == 1) { 151 mTriggeredRule = null; 152 // check first key and pick active rules. 153 forAllRules(mRules, (rule)-> { 154 if (rule.shouldInterceptKey(keyCode)) { 155 mActiveRules.add(rule); 156 } 157 }); 158 } else { 159 // Ignore if rule already triggered. 160 if (mTriggeredRule != null) { 161 return true; 162 } 163 164 // check if second key can trigger rule, or remove the non-match rule. 165 forAllActiveRules((rule) -> { 166 if (!rule.shouldInterceptKeys(mDownTimes)) { 167 return false; 168 } 169 Log.v(TAG, "Performing combination rule : " + rule); 170 mHandler.post(rule::execute); 171 mTriggeredRule = rule; 172 return true; 173 }); 174 mActiveRules.clear(); 175 if (mTriggeredRule != null) { 176 mActiveRules.add(mTriggeredRule); 177 return true; 178 } 179 } 180 } else { 181 mDownTimes.delete(keyCode); 182 for (int index = count - 1; index >= 0; index--) { 183 final TwoKeysCombinationRule rule = mActiveRules.get(index); 184 if (rule.shouldInterceptKey(keyCode)) { 185 mHandler.post(rule::cancel); 186 mActiveRules.remove(index); 187 } 188 } 189 } 190 return false; 191 } 192 193 /** 194 * Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window. 195 */ getKeyInterceptTimeout(int keyCode)196 long getKeyInterceptTimeout(int keyCode) { 197 synchronized (mLock) { 198 if (forAllActiveRules((rule) -> rule.shouldInterceptKey(keyCode))) { 199 return mDownTimes.get(keyCode) + COMBINE_KEY_DELAY_MILLIS; 200 } 201 return 0; 202 } 203 } 204 205 /** 206 * True if the key event had been handled. 207 */ isKeyConsumed(KeyEvent event)208 boolean isKeyConsumed(KeyEvent event) { 209 synchronized (mLock) { 210 if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) { 211 return false; 212 } 213 return mTriggeredRule != null && mTriggeredRule.shouldInterceptKey(event.getKeyCode()); 214 } 215 } 216 217 /** 218 * True if power key is the candidate. 219 */ isPowerKeyIntercepted()220 boolean isPowerKeyIntercepted() { 221 synchronized (mLock) { 222 if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) { 223 // return false if only if power key pressed. 224 return mDownTimes.size() > 1 || mDownTimes.get(KEYCODE_POWER) == 0; 225 } 226 return false; 227 } 228 } 229 230 /** 231 * Traverse each item of rules. 232 */ forAllRules( ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback)233 private void forAllRules( 234 ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback) { 235 final int count = rules.size(); 236 for (int index = 0; index < count; index++) { 237 final TwoKeysCombinationRule rule = rules.get(index); 238 callback.accept(rule); 239 } 240 } 241 242 /** 243 * Traverse each item of active rules until some rule can be applied, otherwise return false. 244 */ forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback)245 private boolean forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback) { 246 final int count = mActiveRules.size(); 247 for (int index = 0; index < count; index++) { 248 final TwoKeysCombinationRule rule = mActiveRules.get(index); 249 if (callback.apply(rule)) { 250 return true; 251 } 252 } 253 return false; 254 } 255 dump(String prefix, PrintWriter pw)256 void dump(String prefix, PrintWriter pw) { 257 pw.println(prefix + "KeyCombination rules:"); 258 forAllRules(mRules, (rule)-> { 259 pw.println(prefix + " " + rule); 260 }); 261 } 262 } 263