Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 83e0fa0c authored by Josh Yang's avatar Josh Yang
Browse files

Add DeferredKeyActionExecutor class.

The class can be used to queue a key action and trigger it later. There
are 2 events this class reacts to:

1. Queue a key action and map it to a key gesture.
2. Make actions corresponding to a key gesture executable.

Regardless of the order of these 2 events, any actions corresponding to
an executable key gesture will be executed.

Bug: 308482931
Test: atest WmTests:DeferredKeyActionExecutorTests
Change-Id: I3ce60b068d52d29aab06c2663467376a41638f7b
parent da689d5a
Loading
Loading
Loading
Loading
+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");
            }
        }
    }
}
+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;
        }
    }
}