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