• 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.SystemClock;
21 import android.util.Log;
22 import android.util.SparseLongArray;
23 import android.view.KeyEvent;
24 
25 import com.android.internal.util.ToBooleanFunction;
26 
27 import java.io.PrintWriter;
28 import java.util.ArrayList;
29 import java.util.function.Consumer;
30 
31 /**
32  * Handles a mapping of two keys combination.
33  */
34 public class KeyCombinationManager {
35     private static final String TAG = "KeyCombinationManager";
36 
37     // Store the received down time of keycode.
38     private final SparseLongArray mDownTimes = new SparseLongArray(2);
39     private final ArrayList<TwoKeysCombinationRule> mRules = new ArrayList();
40 
41     // Selected rules according to current key down.
42     private final ArrayList<TwoKeysCombinationRule> mActiveRules = new ArrayList();
43     // The rule has been triggered by current keys.
44     private TwoKeysCombinationRule mTriggeredRule;
45 
46     // Keys in a key combination must be pressed within this interval of each other.
47     private static final long COMBINE_KEY_DELAY_MILLIS = 150;
48 
49     /**
50      *  Rule definition for two keys combination.
51      *  E.g : define volume_down + power key.
52      *  <pre class="prettyprint">
53      *  TwoKeysCombinationRule rule =
54      *      new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) {
55      *           boolean preCondition() { // check if it needs to intercept key }
56      *           void execute() { // trigger action }
57      *           void cancel() { // cancel action }
58      *       };
59      *  </pre>
60      */
61     abstract static class TwoKeysCombinationRule {
62         private int mKeyCode1;
63         private int mKeyCode2;
64 
TwoKeysCombinationRule(int keyCode1, int keyCode2)65         TwoKeysCombinationRule(int keyCode1, int keyCode2) {
66             mKeyCode1 = keyCode1;
67             mKeyCode2 = keyCode2;
68         }
69 
preCondition()70         boolean preCondition() {
71             return true;
72         }
73 
shouldInterceptKey(int keyCode)74         boolean shouldInterceptKey(int keyCode) {
75             return preCondition() && (keyCode == mKeyCode1 || keyCode == mKeyCode2);
76         }
77 
shouldInterceptKeys(SparseLongArray downTimes)78         boolean shouldInterceptKeys(SparseLongArray downTimes) {
79             final long now = SystemClock.uptimeMillis();
80             if (downTimes.get(mKeyCode1) > 0
81                     && downTimes.get(mKeyCode2) > 0
82                     && now <= downTimes.get(mKeyCode1) + COMBINE_KEY_DELAY_MILLIS
83                     && now <= downTimes.get(mKeyCode2) + COMBINE_KEY_DELAY_MILLIS) {
84                 return true;
85             }
86             return false;
87         }
88 
execute()89         abstract void execute();
cancel()90         abstract void cancel();
91 
92         @Override
toString()93         public String toString() {
94             return KeyEvent.keyCodeToString(mKeyCode1) + " + "
95                     + KeyEvent.keyCodeToString(mKeyCode2);
96         }
97     }
98 
KeyCombinationManager()99     public KeyCombinationManager() {
100     }
101 
addRule(TwoKeysCombinationRule rule)102     void addRule(TwoKeysCombinationRule rule) {
103         mRules.add(rule);
104     }
105 
106     /**
107      * Check if the key event could be intercepted by combination key rule before it is dispatched
108      * to a window.
109      * Return true if any active rule could be triggered by the key event, otherwise false.
110      */
interceptKey(KeyEvent event, boolean interactive)111     boolean interceptKey(KeyEvent event, boolean interactive) {
112         final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
113         final int keyCode = event.getKeyCode();
114         final int count = mActiveRules.size();
115         final long eventTime = event.getEventTime();
116 
117         if (interactive && down) {
118             if (mDownTimes.size() > 0) {
119                 if (count > 0
120                         && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) {
121                     // exceed time from first key down.
122                     forAllRules(mActiveRules, (rule)-> rule.cancel());
123                     mActiveRules.clear();
124                     return false;
125                 } else if (count == 0) { // has some key down but no active rule exist.
126                     return false;
127                 }
128             }
129 
130             if (mDownTimes.get(keyCode) == 0) {
131                 mDownTimes.put(keyCode, eventTime);
132             } else {
133                 // ignore old key, maybe a repeat key.
134                 return false;
135             }
136 
137             if (mDownTimes.size() == 1) {
138                 mTriggeredRule = null;
139                 // check first key and pick active rules.
140                 forAllRules(mRules, (rule)-> {
141                     if (rule.shouldInterceptKey(keyCode)) {
142                         mActiveRules.add(rule);
143                     }
144                 });
145             } else {
146                 // Ignore if rule already triggered.
147                 if (mTriggeredRule != null) {
148                     return true;
149                 }
150 
151                 // check if second key can trigger rule, or remove the non-match rule.
152                 forAllActiveRules((rule) -> {
153                     if (!rule.shouldInterceptKeys(mDownTimes)) {
154                         return false;
155                     }
156                     Log.v(TAG, "Performing combination rule : " + rule);
157                     rule.execute();
158                     mTriggeredRule = rule;
159                     return true;
160                 });
161                 mActiveRules.clear();
162                 if (mTriggeredRule != null) {
163                     mActiveRules.add(mTriggeredRule);
164                     return true;
165                 }
166             }
167         } else {
168             mDownTimes.delete(keyCode);
169             for (int index = count - 1; index >= 0; index--) {
170                 final TwoKeysCombinationRule rule = mActiveRules.get(index);
171                 if (rule.shouldInterceptKey(keyCode)) {
172                     rule.cancel();
173                     mActiveRules.remove(index);
174                 }
175             }
176         }
177         return false;
178     }
179 
180     /**
181      * Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window.
182      */
getKeyInterceptTimeout(int keyCode)183     long getKeyInterceptTimeout(int keyCode) {
184         if (forAllActiveRules((rule) -> rule.shouldInterceptKey(keyCode))) {
185             return mDownTimes.get(keyCode) + COMBINE_KEY_DELAY_MILLIS;
186         }
187         return 0;
188     }
189 
190     /**
191      * True if the key event had been handled.
192      */
isKeyConsumed(KeyEvent event)193     boolean isKeyConsumed(KeyEvent event) {
194         if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
195             return false;
196         }
197         return mTriggeredRule != null && mTriggeredRule.shouldInterceptKey(event.getKeyCode());
198     }
199 
200     /**
201      * True if power key is the candidate.
202      */
isPowerKeyIntercepted()203     boolean isPowerKeyIntercepted() {
204         if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) {
205             // return false if only if power key pressed.
206             return mDownTimes.size() > 1 || mDownTimes.get(KEYCODE_POWER) == 0;
207         }
208         return false;
209     }
210 
211     /**
212      * Traverse each item of rules.
213      */
forAllRules( ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback)214     private void forAllRules(
215             ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback) {
216         final int count = rules.size();
217         for (int index = 0; index < count; index++) {
218             final TwoKeysCombinationRule rule = rules.get(index);
219             callback.accept(rule);
220         }
221     }
222 
223     /**
224      * Traverse each item of active rules until some rule can be applied, otherwise return false.
225      */
forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback)226     private boolean forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback) {
227         final int count = mActiveRules.size();
228         for (int index = 0; index < count; index++) {
229             final TwoKeysCombinationRule rule = mActiveRules.get(index);
230             if (callback.apply(rule)) {
231                 return true;
232             }
233         }
234         return false;
235     }
236 
dump(String prefix, PrintWriter pw)237     void dump(String prefix, PrintWriter pw) {
238         pw.println(prefix + "KeyCombination rules:");
239         forAllRules(mRules, (rule)-> {
240             pw.println(prefix + "  " + rule);
241         });
242     }
243 }
244