1 /*
2  * Copyright (C) 2023 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 android.util.Log;
19 import android.util.SparseArray;
20 import android.view.KeyEvent;
21 
22 import java.io.PrintWriter;
23 import java.util.ArrayList;
24 import java.util.List;
25 
26 /**
27  * A class that is responsible for queueing deferred key actions which can be triggered at a later
28  * time.
29  */
30 class DeferredKeyActionExecutor {
31     private static final boolean DEBUG = PhoneWindowManager.DEBUG_INPUT;
32     private static final String TAG = "DeferredKeyAction";
33 
34     private final SparseArray<TimedActionsBuffer> mBuffers = new SparseArray<>();
35 
36     /**
37      * Queue a key action which can be triggered at a later time. Note that this method will also
38      * delete any outdated actions belong to the same key code.
39      *
40      * <p>Warning: the queued actions will only be cleaned up lazily when a new gesture downTime is
41      * recorded. If no new gesture downTime is recorded and the existing gesture is not executable,
42      * the actions will be kept in the buffer indefinitely. This may cause memory leak if the action
43      * itself holds references to temporary objects, or if too many actions are queued for the same
44      * gesture. The risk scales as you track more key codes. Please use this method with caution and
45      * ensure you only queue small amount of actions with limited size.
46      *
47      * <p>If you need to queue a large amount of actions with large size, there are several
48      * potential solutions to relief the memory leak risks:
49      *
50      * <p>1. Add a timeout (e.g. ANR timeout) based clean-up mechanism.
51      *
52      * <p>2. Clean-up queued actions when we know they won't be needed. E.g., add a callback when
53      * the gesture is handled by apps, and clean up queued actions associated with the handled
54      * gesture.
55      *
56      * @param keyCode the key code which triggers the action.
57      * @param downTime the down time of the key gesture. For multi-press actions, this is the down
58      *     time of the last press. For long-press or very long-press actions, this is the initial
59      *     down time.
60      * @param action the action that will be triggered at a later time.
61      */
queueKeyAction(int keyCode, long downTime, Runnable action)62     public void queueKeyAction(int keyCode, long downTime, Runnable action) {
63         getActionsBufferWithLazyCleanUp(keyCode, downTime).addAction(action);
64     }
65 
66     /**
67      * Make actions associated with the given key gesture executable. Actions already queued for the
68      * given gesture will be executed immediately. Any new actions belonging to this gesture will be
69      * executed as soon as they get queued. Note that this method will also delete any outdated
70      * actions belong to the same key code.
71      *
72      * @param keyCode the key code of the gesture.
73      * @param downTime the down time of the gesture.
74      */
setActionsExecutable(int keyCode, long downTime)75     public void setActionsExecutable(int keyCode, long downTime) {
76         getActionsBufferWithLazyCleanUp(keyCode, downTime).setExecutable();
77     }
78 
79     /**
80      * Clears all the queued action for given key code.
81      *
82      * @param keyCode the key code whose queued actions will be cleared.
83      */
cancelQueuedAction(int keyCode)84     public void cancelQueuedAction(int keyCode) {
85         TimedActionsBuffer actionsBuffer = mBuffers.get(keyCode);
86         if (actionsBuffer != null) {
87             actionsBuffer.clear();
88         }
89     }
90 
getActionsBufferWithLazyCleanUp(int keyCode, long downTime)91     private TimedActionsBuffer getActionsBufferWithLazyCleanUp(int keyCode, long downTime) {
92         TimedActionsBuffer buffer = mBuffers.get(keyCode);
93         if (buffer == null || buffer.getDownTime() != downTime) {
94             if (DEBUG && buffer != null) {
95                 Log.d(
96                         TAG,
97                         "getActionsBufferWithLazyCleanUp: cleaning up gesture actions for key "
98                                 + KeyEvent.keyCodeToString(keyCode));
99             }
100             buffer = new TimedActionsBuffer(keyCode, downTime);
101             mBuffers.put(keyCode, buffer);
102         }
103         return buffer;
104     }
105 
dump(String prefix, PrintWriter pw)106     public void dump(String prefix, PrintWriter pw) {
107         pw.println(prefix + "Deferred key action executor:");
108         if (mBuffers.size() == 0) {
109             pw.println(prefix + "  empty");
110             return;
111         }
112         for (int i = 0; i < mBuffers.size(); i++) {
113             mBuffers.valueAt(i).dump(prefix, pw);
114         }
115     }
116 
117     /** A buffer holding a gesture down time and its corresponding actions. */
118     private static class TimedActionsBuffer {
119         private final List<Runnable> mActions = new ArrayList<>();
120         private final int mKeyCode;
121         private final long mDownTime;
122         private boolean mExecutable;
123 
TimedActionsBuffer(int keyCode, long downTime)124         TimedActionsBuffer(int keyCode, long downTime) {
125             mKeyCode = keyCode;
126             mDownTime = downTime;
127         }
128 
getDownTime()129         long getDownTime() {
130             return mDownTime;
131         }
132 
addAction(Runnable action)133         void addAction(Runnable action) {
134             if (mExecutable) {
135                 if (DEBUG) {
136                     Log.i(
137                             TAG,
138                             "addAction: execute action for key "
139                                     + KeyEvent.keyCodeToString(mKeyCode));
140                 }
141                 action.run();
142                 return;
143             }
144             mActions.add(action);
145         }
146 
setExecutable()147         void setExecutable() {
148             mExecutable = true;
149             if (DEBUG && !mActions.isEmpty()) {
150                 Log.i(
151                         TAG,
152                         "setExecutable: execute actions for key "
153                                 + KeyEvent.keyCodeToString(mKeyCode));
154             }
155             for (Runnable action : mActions) {
156                 action.run();
157             }
158             mActions.clear();
159         }
160 
clear()161         void clear() {
162             mActions.clear();
163         }
164 
dump(String prefix, PrintWriter pw)165         void dump(String prefix, PrintWriter pw) {
166             if (mExecutable) {
167                 pw.println(prefix + "  " + KeyEvent.keyCodeToString(mKeyCode) + ": executable");
168             } else {
169                 pw.println(
170                         prefix
171                                 + "  "
172                                 + KeyEvent.keyCodeToString(mKeyCode)
173                                 + ": "
174                                 + mActions.size()
175                                 + " actions queued");
176             }
177         }
178     }
179 }
180