• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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