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