Loading services/core/java/com/android/server/policy/DeferredKeyActionExecutor.java 0 → 100644 +163 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.policy; import android.util.Log; import android.util.SparseArray; import android.view.KeyEvent; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; /** * A class that is responsible for queueing deferred key actions which can be triggered at a later * time. */ class DeferredKeyActionExecutor { private static final boolean DEBUG = PhoneWindowManager.DEBUG_INPUT; private static final String TAG = "DeferredKeyAction"; private final SparseArray<TimedActionsBuffer> mBuffers = new SparseArray<>(); /** * Queue a key action which can be triggered at a later time. Note that this method will also * delete any outdated actions belong to the same key code. * * <p>Warning: the queued actions will only be cleaned up lazily when a new gesture downTime is * recorded. If no new gesture downTime is recorded and the existing gesture is not executable, * the actions will be kept in the buffer indefinitely. This may cause memory leak if the action * itself holds references to temporary objects, or if too many actions are queued for the same * gesture. The risk scales as you track more key codes. Please use this method with caution and * ensure you only queue small amount of actions with limited size. * * <p>If you need to queue a large amount of actions with large size, there are several * potential solutions to relief the memory leak risks: * * <p>1. Add a timeout (e.g. ANR timeout) based clean-up mechanism. * * <p>2. Clean-up queued actions when we know they won't be needed. E.g., add a callback when * the gesture is handled by apps, and clean up queued actions associated with the handled * gesture. * * @param keyCode the key code which triggers the action. * @param downTime the down time of the key gesture. For multi-press actions, this is the down * time of the last press. For long-press or very long-press actions, this is the initial * down time. * @param action the action that will be triggered at a later time. */ public void queueKeyAction(int keyCode, long downTime, Runnable action) { getActionsBufferWithLazyCleanUp(keyCode, downTime).addAction(action); } /** * Make actions associated with the given key gesture executable. Actions already queued for the * given gesture will be executed immediately. Any new actions belonging to this gesture will be * executed as soon as they get queued. Note that this method will also delete any outdated * actions belong to the same key code. * * @param keyCode the key code of the gesture. * @param downTime the down time of the gesture. */ public void setActionsExecutable(int keyCode, long downTime) { getActionsBufferWithLazyCleanUp(keyCode, downTime).setExecutable(); } private TimedActionsBuffer getActionsBufferWithLazyCleanUp(int keyCode, long downTime) { TimedActionsBuffer buffer = mBuffers.get(keyCode); if (buffer == null || buffer.getDownTime() != downTime) { if (DEBUG && buffer != null) { Log.d( TAG, "getActionsBufferWithLazyCleanUp: cleaning up gesture actions for key " + KeyEvent.keyCodeToString(keyCode)); } buffer = new TimedActionsBuffer(keyCode, downTime); mBuffers.put(keyCode, buffer); } return buffer; } public void dump(String prefix, PrintWriter pw) { pw.println(prefix + "Deferred key action executor:"); if (mBuffers.size() == 0) { pw.println(prefix + " empty"); return; } for (int i = 0; i < mBuffers.size(); i++) { mBuffers.valueAt(i).dump(prefix, pw); } } /** A buffer holding a gesture down time and its corresponding actions. */ private static class TimedActionsBuffer { private final List<Runnable> mActions = new ArrayList<>(); private final int mKeyCode; private final long mDownTime; private boolean mExecutable; TimedActionsBuffer(int keyCode, long downTime) { mKeyCode = keyCode; mDownTime = downTime; } long getDownTime() { return mDownTime; } void addAction(Runnable action) { if (mExecutable) { if (DEBUG) { Log.i( TAG, "addAction: execute action for key " + KeyEvent.keyCodeToString(mKeyCode)); } action.run(); return; } mActions.add(action); } void setExecutable() { mExecutable = true; if (DEBUG && !mActions.isEmpty()) { Log.i( TAG, "setExecutable: execute actions for key " + KeyEvent.keyCodeToString(mKeyCode)); } for (Runnable action : mActions) { action.run(); } mActions.clear(); } void dump(String prefix, PrintWriter pw) { if (mExecutable) { pw.println(prefix + " " + KeyEvent.keyCodeToString(mKeyCode) + ": executable"); } else { pw.println( prefix + " " + KeyEvent.keyCodeToString(mKeyCode) + ": " + mActions.size() + " actions queued"); } } } } services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java 0 → 100644 +106 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.policy; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.view.KeyEvent; import org.junit.Before; import org.junit.Test; /** * Test class for {@link DeferredKeyActionExecutor}. * * <p>Build/Install/Run: atest WmTests:DeferredKeyActionExecutorTests */ public final class DeferredKeyActionExecutorTests { private DeferredKeyActionExecutor mKeyActionExecutor; @Before public void setUp() { mKeyActionExecutor = new DeferredKeyActionExecutor(); } @Test public void queueKeyAction_actionNotExecuted() { TestAction action = new TestAction(); mKeyActionExecutor.queueKeyAction(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action); assertFalse(action.executed); } @Test public void setActionsExecutable_afterActionQueued_actionExecuted() { TestAction action = new TestAction(); mKeyActionExecutor.queueKeyAction(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action); mKeyActionExecutor.setActionsExecutable(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1); assertTrue(action.executed); } @Test public void queueKeyAction_alreadyExecutable_actionExecuted() { TestAction action = new TestAction(); mKeyActionExecutor.setActionsExecutable(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1); mKeyActionExecutor.queueKeyAction(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action); assertTrue(action.executed); } @Test public void setActionsExecutable_afterActionQueued_downTimeMismatch_actionNotExecuted() { TestAction action1 = new TestAction(); mKeyActionExecutor.queueKeyAction( KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action1); mKeyActionExecutor.setActionsExecutable(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 2); assertFalse(action1.executed); TestAction action2 = new TestAction(); mKeyActionExecutor.queueKeyAction( KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 2, action2); assertFalse(action1.executed); assertTrue(action2.executed); } @Test public void queueKeyAction_afterSetExecutable_downTimeMismatch_actionNotExecuted() { TestAction action = new TestAction(); mKeyActionExecutor.setActionsExecutable(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1); mKeyActionExecutor.queueKeyAction(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 2, action); assertFalse(action.executed); } static class TestAction implements Runnable { public boolean executed; @Override public void run() { executed = true; } } } Loading
services/core/java/com/android/server/policy/DeferredKeyActionExecutor.java 0 → 100644 +163 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.policy; import android.util.Log; import android.util.SparseArray; import android.view.KeyEvent; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; /** * A class that is responsible for queueing deferred key actions which can be triggered at a later * time. */ class DeferredKeyActionExecutor { private static final boolean DEBUG = PhoneWindowManager.DEBUG_INPUT; private static final String TAG = "DeferredKeyAction"; private final SparseArray<TimedActionsBuffer> mBuffers = new SparseArray<>(); /** * Queue a key action which can be triggered at a later time. Note that this method will also * delete any outdated actions belong to the same key code. * * <p>Warning: the queued actions will only be cleaned up lazily when a new gesture downTime is * recorded. If no new gesture downTime is recorded and the existing gesture is not executable, * the actions will be kept in the buffer indefinitely. This may cause memory leak if the action * itself holds references to temporary objects, or if too many actions are queued for the same * gesture. The risk scales as you track more key codes. Please use this method with caution and * ensure you only queue small amount of actions with limited size. * * <p>If you need to queue a large amount of actions with large size, there are several * potential solutions to relief the memory leak risks: * * <p>1. Add a timeout (e.g. ANR timeout) based clean-up mechanism. * * <p>2. Clean-up queued actions when we know they won't be needed. E.g., add a callback when * the gesture is handled by apps, and clean up queued actions associated with the handled * gesture. * * @param keyCode the key code which triggers the action. * @param downTime the down time of the key gesture. For multi-press actions, this is the down * time of the last press. For long-press or very long-press actions, this is the initial * down time. * @param action the action that will be triggered at a later time. */ public void queueKeyAction(int keyCode, long downTime, Runnable action) { getActionsBufferWithLazyCleanUp(keyCode, downTime).addAction(action); } /** * Make actions associated with the given key gesture executable. Actions already queued for the * given gesture will be executed immediately. Any new actions belonging to this gesture will be * executed as soon as they get queued. Note that this method will also delete any outdated * actions belong to the same key code. * * @param keyCode the key code of the gesture. * @param downTime the down time of the gesture. */ public void setActionsExecutable(int keyCode, long downTime) { getActionsBufferWithLazyCleanUp(keyCode, downTime).setExecutable(); } private TimedActionsBuffer getActionsBufferWithLazyCleanUp(int keyCode, long downTime) { TimedActionsBuffer buffer = mBuffers.get(keyCode); if (buffer == null || buffer.getDownTime() != downTime) { if (DEBUG && buffer != null) { Log.d( TAG, "getActionsBufferWithLazyCleanUp: cleaning up gesture actions for key " + KeyEvent.keyCodeToString(keyCode)); } buffer = new TimedActionsBuffer(keyCode, downTime); mBuffers.put(keyCode, buffer); } return buffer; } public void dump(String prefix, PrintWriter pw) { pw.println(prefix + "Deferred key action executor:"); if (mBuffers.size() == 0) { pw.println(prefix + " empty"); return; } for (int i = 0; i < mBuffers.size(); i++) { mBuffers.valueAt(i).dump(prefix, pw); } } /** A buffer holding a gesture down time and its corresponding actions. */ private static class TimedActionsBuffer { private final List<Runnable> mActions = new ArrayList<>(); private final int mKeyCode; private final long mDownTime; private boolean mExecutable; TimedActionsBuffer(int keyCode, long downTime) { mKeyCode = keyCode; mDownTime = downTime; } long getDownTime() { return mDownTime; } void addAction(Runnable action) { if (mExecutable) { if (DEBUG) { Log.i( TAG, "addAction: execute action for key " + KeyEvent.keyCodeToString(mKeyCode)); } action.run(); return; } mActions.add(action); } void setExecutable() { mExecutable = true; if (DEBUG && !mActions.isEmpty()) { Log.i( TAG, "setExecutable: execute actions for key " + KeyEvent.keyCodeToString(mKeyCode)); } for (Runnable action : mActions) { action.run(); } mActions.clear(); } void dump(String prefix, PrintWriter pw) { if (mExecutable) { pw.println(prefix + " " + KeyEvent.keyCodeToString(mKeyCode) + ": executable"); } else { pw.println( prefix + " " + KeyEvent.keyCodeToString(mKeyCode) + ": " + mActions.size() + " actions queued"); } } } }
services/tests/wmtests/src/com/android/server/policy/DeferredKeyActionExecutorTests.java 0 → 100644 +106 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.policy; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.view.KeyEvent; import org.junit.Before; import org.junit.Test; /** * Test class for {@link DeferredKeyActionExecutor}. * * <p>Build/Install/Run: atest WmTests:DeferredKeyActionExecutorTests */ public final class DeferredKeyActionExecutorTests { private DeferredKeyActionExecutor mKeyActionExecutor; @Before public void setUp() { mKeyActionExecutor = new DeferredKeyActionExecutor(); } @Test public void queueKeyAction_actionNotExecuted() { TestAction action = new TestAction(); mKeyActionExecutor.queueKeyAction(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action); assertFalse(action.executed); } @Test public void setActionsExecutable_afterActionQueued_actionExecuted() { TestAction action = new TestAction(); mKeyActionExecutor.queueKeyAction(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action); mKeyActionExecutor.setActionsExecutable(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1); assertTrue(action.executed); } @Test public void queueKeyAction_alreadyExecutable_actionExecuted() { TestAction action = new TestAction(); mKeyActionExecutor.setActionsExecutable(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1); mKeyActionExecutor.queueKeyAction(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action); assertTrue(action.executed); } @Test public void setActionsExecutable_afterActionQueued_downTimeMismatch_actionNotExecuted() { TestAction action1 = new TestAction(); mKeyActionExecutor.queueKeyAction( KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1, action1); mKeyActionExecutor.setActionsExecutable(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 2); assertFalse(action1.executed); TestAction action2 = new TestAction(); mKeyActionExecutor.queueKeyAction( KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 2, action2); assertFalse(action1.executed); assertTrue(action2.executed); } @Test public void queueKeyAction_afterSetExecutable_downTimeMismatch_actionNotExecuted() { TestAction action = new TestAction(); mKeyActionExecutor.setActionsExecutable(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 1); mKeyActionExecutor.queueKeyAction(KeyEvent.KEYCODE_STEM_PRIMARY, /* downTime= */ 2, action); assertFalse(action.executed); } static class TestAction implements Runnable { public boolean executed; @Override public void run() { executed = true; } } }