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

Commit f130c799 authored by arthurhung's avatar arthurhung
Browse files

Intruduce KeyCombinationManager to PhoneWindowManager

Move two keys combination from PhoneWindowManager to
KeyCombinationManager.

- Extract power/volume_down, power/volume_up volume_down/volume_up
  combinations from interceptKeyBeforeQueueing to KeyCombinationManager.
- Extract back/dpad_down, back/dpad_center combinations for tv device.

Bug: 169058831
Bug: 127687575
Test: atest KeyEventTest
Test: atest KeyCombinationTests
Test: manual, (volume_down+power) (volume_down+volume_up)
      (volume_up+power) (back+dpad_down) (back+dpad_center)
Change-Id: Ice2ffa7d6b5418cb50849180bf9b782408e7cb1c
parent 79c9b419
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);
    }
}