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

Commit af526b43 authored by Josh Yang's avatar Josh Yang Committed by Android (Google) Code Review
Browse files

Merge "Add DeferredKeyActionExecutor class." into main

parents 51a75a73 83e0fa0c
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;
        }
    }
}