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

Commit 6ea899bb authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Intruduce KeyCombinationManager to PhoneWindowManager"

parents c43d935c f130c799
Loading
Loading
Loading
Loading
+229 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 android.view.KeyEvent.KEYCODE_POWER;

import android.os.SystemClock;
import android.util.SparseLongArray;
import android.view.KeyEvent;

import com.android.internal.util.ToBooleanFunction;

import java.util.ArrayList;
import java.util.function.Consumer;

/**
 * Handles a mapping of two keys combination.
 */
public class KeyCombinationManager {
    private static final String TAG = "KeyCombinationManager";

    // Store the received down time of keycode.
    private final SparseLongArray mDownTimes = new SparseLongArray(2);
    private final ArrayList<TwoKeysCombinationRule> mRules = new ArrayList();

    // Selected rules according to current key down.
    private final ArrayList<TwoKeysCombinationRule> mActiveRules = new ArrayList();
    // The rule has been triggered by current keys.
    private TwoKeysCombinationRule mTriggeredRule;

    // Keys in a key combination must be pressed within this interval of each other.
    private static final long COMBINE_KEY_DELAY_MILLIS = 150;

    /**
     *  Rule definition for two keys combination.
     *  E.g : define volume_down + power key.
     *  <pre class="prettyprint">
     *  TwoKeysCombinationRule rule =
     *      new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) {
     *           boolean preCondition() { // check if it needs to intercept key }
     *           void execute() { // trigger action }
     *           void cancel() { // cancel action }
     *       };
     *  </pre>
     */
    abstract static class TwoKeysCombinationRule {
        private int mKeyCode1;
        private int mKeyCode2;

        TwoKeysCombinationRule(int keyCode1, int keyCode2) {
            mKeyCode1 = keyCode1;
            mKeyCode2 = keyCode2;
        }

        boolean preCondition() {
            return true;
        }

        boolean shouldInterceptKey(int keyCode) {
            return preCondition() && (keyCode == mKeyCode1 || keyCode == mKeyCode2);
        }

        boolean shouldInterceptKeys(SparseLongArray downTimes) {
            final long now = SystemClock.uptimeMillis();
            if (downTimes.get(mKeyCode1) > 0
                    && downTimes.get(mKeyCode2) > 0
                    && now <= downTimes.get(mKeyCode1) + COMBINE_KEY_DELAY_MILLIS
                    && now <= downTimes.get(mKeyCode2) + COMBINE_KEY_DELAY_MILLIS) {
                return true;
            }
            return false;
        }

        abstract void execute();
        abstract void cancel();

        @Override
        public String toString() {
            return "KeyCode1 = " + KeyEvent.keyCodeToString(mKeyCode1)
                    + ", KeyCode2 = " +  KeyEvent.keyCodeToString(mKeyCode2);
        }
    }

    public KeyCombinationManager() {
    }

    void addRule(TwoKeysCombinationRule rule) {
        mRules.add(rule);
    }

    /**
     * Check if the key event could be triggered by combine key rule before dispatching to a window.
     */
    void interceptKey(KeyEvent event, boolean interactive) {
        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
        final int keyCode = event.getKeyCode();
        final int count = mActiveRules.size();
        final long eventTime = event.getEventTime();

        if (interactive && down) {
            if (mDownTimes.size() > 0) {
                if (count > 0
                        && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) {
                    // exceed time from first key down.
                    forAllRules(mActiveRules, (rule)-> rule.cancel());
                    mActiveRules.clear();
                    return;
                } else if (count == 0) { // has some key down but no active rule exist.
                    return;
                }
            }

            if (mDownTimes.get(keyCode) == 0) {
                mDownTimes.put(keyCode, eventTime);
            } else {
                // ignore old key, maybe a repeat key.
                return;
            }

            if (mDownTimes.size() == 1) {
                mTriggeredRule = null;
                // check first key and pick active rules.
                forAllRules(mRules, (rule)-> {
                    if (rule.shouldInterceptKey(keyCode)) {
                        mActiveRules.add(rule);
                    }
                });
            } else {
                // Ignore if rule already triggered.
                if (mTriggeredRule != null) {
                    return;
                }

                // check if second key can trigger rule, or remove the non-match rule.
                forAllActiveRules((rule) -> {
                    if (!rule.shouldInterceptKeys(mDownTimes)) {
                        return false;
                    }
                    rule.execute();
                    mTriggeredRule = rule;
                    return true;
                });
                mActiveRules.clear();
                if (mTriggeredRule != null) {
                    mActiveRules.add(mTriggeredRule);
                }
            }
        } else {
            mDownTimes.delete(keyCode);
            for (int index = count - 1; index >= 0; index--) {
                final TwoKeysCombinationRule rule = mActiveRules.get(index);
                if (rule.shouldInterceptKey(keyCode)) {
                    rule.cancel();
                    mActiveRules.remove(index);
                }
            }
        }
    }

    /**
     * Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window.
     */
    long getKeyInterceptTimeout(int keyCode) {
        if (forAllActiveRules((rule) -> rule.shouldInterceptKey(keyCode))) {
            return mDownTimes.get(keyCode) + COMBINE_KEY_DELAY_MILLIS;
        }
        return 0;
    }

    /**
     * True if the key event had been handled.
     */
    boolean isKeyConsumed(KeyEvent event) {
        if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
            return false;
        }
        return mTriggeredRule != null && mTriggeredRule.shouldInterceptKey(event.getKeyCode());
    }

    /**
     * True if power key is the candidate.
     */
    boolean isPowerKeyIntercepted() {
        if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) {
            // return false if only if power key pressed.
            return mDownTimes.size() > 1 || mDownTimes.get(KEYCODE_POWER) == 0;
        }
        return false;
    }

    /**
     * Traverse each item of rules.
     */
    private void forAllRules(
            ArrayList<TwoKeysCombinationRule> rules, Consumer<TwoKeysCombinationRule> callback) {
        final int count = rules.size();
        for (int index = 0; index < count; index++) {
            final TwoKeysCombinationRule rule = rules.get(index);
            callback.accept(rule);
        }
    }

    /**
     * Traverse each item of active rules until some rule can be applied, otherwise return false.
     */
    private boolean forAllActiveRules(ToBooleanFunction<TwoKeysCombinationRule> callback) {
        final int count = mActiveRules.size();
        for (int index = 0; index < count; index++) {
            final TwoKeysCombinationRule rule = mActiveRules.get(index);
            if (callback.apply(rule)) {
                return true;
            }
        }
        return false;
    }
}
+146 −259

File changed.

Preview size limit exceeded, changes collapsed.

+208 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.KEYCODE_BACK;
import static android.view.KeyEvent.KEYCODE_POWER;
import static android.view.KeyEvent.KEYCODE_VOLUME_DOWN;
import static android.view.KeyEvent.KEYCODE_VOLUME_UP;

import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.KeyEvent;

import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Test;

/**
 * Test class for {@link KeyCombinationManager}.
 *
 * Build/Install/Run:
 *  atest KeyCombinationTests
 */

@SmallTest
public class KeyCombinationTests {
    private KeyCombinationManager mKeyCombinationManager;

    private boolean mAction1Triggered = false;
    private boolean mAction2Triggered = false;
    private boolean mAction3Triggered = false;

    private boolean mPreCondition = true;
    private static final long SCHEDULE_TIME = 300;

    @Before
    public void setUp() {
        mKeyCombinationManager = new KeyCombinationManager();
        initKeyCombinationRules();
    }

    private void initKeyCombinationRules() {
        // Rule 1 : power + volume_down trigger action immediately.
        mKeyCombinationManager.addRule(
                new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
                        KEYCODE_POWER) {
                    @Override
                    void execute() {
                        mAction1Triggered = true;
                    }

                    @Override
                    void cancel() {
                    }
                });

        // Rule 2 : volume_up + volume_down with condition.
        mKeyCombinationManager.addRule(
                new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
                        KEYCODE_VOLUME_UP) {
                    @Override
                    boolean preCondition() {
                        return mPreCondition;
                    }

                    @Override
                    void execute() {
                        mAction2Triggered = true;
                    }

                    @Override
                    void cancel() {
                    }
                });

        // Rule 3 : power + volume_up schedule and trigger action after timeout.
        mKeyCombinationManager.addRule(
                new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_UP, KEYCODE_POWER) {
                    final Runnable mAction = new Runnable() {
                        @Override
                        public void run() {
                            mAction3Triggered = true;
                        }
                    };
                    final Handler mHandler = new Handler(Looper.getMainLooper());

                    @Override
                    void execute() {
                        mHandler.postDelayed(mAction, SCHEDULE_TIME);
                    }

                    @Override
                    void cancel() {
                        mHandler.removeCallbacks(mAction);
                    }
                });
    }

    private void pressKeys(long firstKeyTime, int firstKeyCode, long secondKeyTime,
            int secondKeyCode) {
        pressKeys(firstKeyTime, firstKeyCode, secondKeyTime, secondKeyCode, 0);
    }

    private void pressKeys(long firstKeyTime, int firstKeyCode, long secondKeyTime,
            int secondKeyCode, long pressTime) {
        final KeyEvent firstKeyDown = new KeyEvent(firstKeyTime, firstKeyTime, ACTION_DOWN,
                firstKeyCode, 0 /* repeat */, 0 /* metaState */);
        final KeyEvent secondKeyDown = new KeyEvent(secondKeyTime, secondKeyTime, ACTION_DOWN,
                secondKeyCode, 0 /* repeat */, 0 /* metaState */);

        mKeyCombinationManager.interceptKey(firstKeyDown, true);
        mKeyCombinationManager.interceptKey(secondKeyDown, true);

        // keep press down.
        try {
            Thread.sleep(pressTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        final KeyEvent firstKeyUp = new KeyEvent(firstKeyTime, firstKeyTime, ACTION_UP,
                firstKeyCode, 0 /* repeat */, 0 /* metaState */);
        final KeyEvent secondKeyUp = new KeyEvent(secondKeyTime, secondKeyTime, ACTION_UP,
                secondKeyCode, 0 /* repeat */, 0 /* metaState */);

        mKeyCombinationManager.interceptKey(firstKeyUp, true);
        mKeyCombinationManager.interceptKey(secondKeyUp, true);
    }

    @Test
    public void testTriggerRule() {
        final long eventTime = SystemClock.uptimeMillis();
        pressKeys(eventTime, KEYCODE_POWER, eventTime, KEYCODE_VOLUME_DOWN);
        assertTrue(mAction1Triggered);

        pressKeys(eventTime, KEYCODE_VOLUME_UP, eventTime, KEYCODE_VOLUME_DOWN);
        assertTrue(mAction2Triggered);

        pressKeys(eventTime, KEYCODE_POWER, eventTime, KEYCODE_VOLUME_UP, SCHEDULE_TIME + 50);
        assertTrue(mAction3Triggered);
    }

    /**
     *  Nothing should happen if there is no definition.
     */
    @Test
    public void testNotTrigger_NoRule() {
        final long eventTime = SystemClock.uptimeMillis();
        pressKeys(eventTime, KEYCODE_BACK, eventTime, KEYCODE_VOLUME_DOWN);
        assertFalse(mAction1Triggered);
        assertFalse(mAction2Triggered);
        assertFalse(mAction3Triggered);
    }

    /**
     *  Nothing should happen if the interval of press time is too long.
     */
    @Test
    public void testNotTrigger_Interval() {
        final long eventTime = SystemClock.uptimeMillis();
        final long earlyEventTime = eventTime - 200; // COMBINE_KEY_DELAY_MILLIS = 150;
        pressKeys(earlyEventTime, KEYCODE_POWER, eventTime, KEYCODE_VOLUME_DOWN);
        assertFalse(mAction1Triggered);
    }

    /**
     *  Nothing should happen if the condition is false.
     */
    @Test
    public void testNotTrigger_Condition() {
        final long eventTime = SystemClock.uptimeMillis();
        // we won't trigger action 2 because the condition is false.
        mPreCondition = false;
        pressKeys(eventTime, KEYCODE_VOLUME_UP, eventTime, KEYCODE_VOLUME_DOWN);
        assertFalse(mAction2Triggered);
    }

    /**
     *  Nothing should happen if the keys released too early.
     */
    @Test
    public void testNotTrigger_EarlyRelease() {
        final long eventTime = SystemClock.uptimeMillis();
        pressKeys(eventTime, KEYCODE_POWER, eventTime, KEYCODE_VOLUME_UP);
        assertFalse(mAction3Triggered);
    }
}