summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/policy/KeyCombinationManager.java229
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java405
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java208
3 files changed, 583 insertions, 259 deletions
diff --git a/services/core/java/com/android/server/policy/KeyCombinationManager.java b/services/core/java/com/android/server/policy/KeyCombinationManager.java
new file mode 100644
index 000000000000..84ac12497e71
--- /dev/null
+++ b/services/core/java/com/android/server/policy/KeyCombinationManager.java
@@ -0,0 +1,229 @@
+/*
+ * 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;
+ }
+}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 8beec35ebc64..75868e3216cc 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -35,7 +35,13 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_OFF;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.Display.STATE_OFF;
+import static android.view.KeyEvent.KEYCODE_BACK;
+import static android.view.KeyEvent.KEYCODE_DPAD_CENTER;
+import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
+import static android.view.KeyEvent.KEYCODE_POWER;
import static android.view.KeyEvent.KEYCODE_UNKNOWN;
+import static android.view.KeyEvent.KEYCODE_VOLUME_DOWN;
+import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
import static android.view.WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW;
@@ -202,6 +208,7 @@ import com.android.server.GestureLauncherService;
import com.android.server.LocalServices;
import com.android.server.SystemServiceManager;
import com.android.server.inputmethod.InputMethodManagerInternal;
+import com.android.server.policy.KeyCombinationManager.TwoKeysCombinationRule;
import com.android.server.policy.keyguard.KeyguardServiceDelegate;
import com.android.server.policy.keyguard.KeyguardServiceDelegate.DrawnListener;
import com.android.server.policy.keyguard.KeyguardStateMonitor.StateCallback;
@@ -405,7 +412,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private boolean mEnableCarDockHomeCapture = true;
boolean mBootMessageNeedsHiding;
- KeyguardServiceDelegate mKeyguardDelegate;
+ private KeyguardServiceDelegate mKeyguardDelegate;
private boolean mKeyguardBound;
final Runnable mWindowManagerDrawCallback = new Runnable() {
@Override
@@ -422,8 +429,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
};
- GlobalActions mGlobalActions;
- Handler mHandler;
+ private GlobalActions mGlobalActions;
+ private Handler mHandler;
// FIXME This state is shared between the input reader and handler thread.
// Technically it's broken and buggy but it has been like this for many years
@@ -547,34 +554,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private boolean mGoToSleepOnButtonPressTheaterMode;
// Screenshot trigger states
- // Time to volume and power must be pressed within this interval of each other.
- private static final long SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS = 150;
// Increase the chord delay when taking a screenshot from the keyguard
private static final float KEYGUARD_SCREENSHOT_CHORD_DELAY_MULTIPLIER = 2.5f;
- private boolean mScreenshotChordEnabled;
- private boolean mScreenshotChordVolumeDownKeyTriggered;
- private long mScreenshotChordVolumeDownKeyTime;
- private boolean mScreenshotChordVolumeDownKeyConsumed;
- private boolean mA11yShortcutChordVolumeUpKeyTriggered;
- private long mA11yShortcutChordVolumeUpKeyTime;
- private boolean mA11yShortcutChordVolumeUpKeyConsumed;
-
- private boolean mScreenshotChordPowerKeyTriggered;
- private long mScreenshotChordPowerKeyTime;
// Ringer toggle should reuse timing and triggering from screenshot power and a11y vol up
- private int mRingerToggleChord = VOLUME_HUSH_OFF;
+ int mRingerToggleChord = VOLUME_HUSH_OFF;
private static final long BUGREPORT_TV_GESTURE_TIMEOUT_MILLIS = 1000;
- private boolean mBugreportTvKey1Pressed;
- private boolean mBugreportTvKey2Pressed;
- private boolean mBugreportTvScheduled;
-
- private boolean mAccessibilityTvKey1Pressed;
- private boolean mAccessibilityTvKey2Pressed;
- private boolean mAccessibilityTvScheduled;
-
/* The number of steps between min and max brightness */
private static final int BRIGHTNESS_STEPS = 10;
@@ -603,6 +590,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private int mPowerButtonSuppressionDelayMillis = POWER_BUTTON_SUPPRESSION_DELAY_DEFAULT_MILLIS;
+ private KeyCombinationManager mKeyCombinationManager;
+
private static final int MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK = 3;
private static final int MSG_DISPATCH_MEDIA_KEY_REPEAT_WITH_WAKE_LOCK = 4;
private static final int MSG_KEYGUARD_DRAWN_COMPLETE = 5;
@@ -900,15 +889,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mWindowManagerFuncs.onPowerKeyDown(interactive);
- // Latch power key state to detect screenshot chord.
- if (interactive && !mScreenshotChordPowerKeyTriggered
- && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
- mScreenshotChordPowerKeyTriggered = true;
- mScreenshotChordPowerKeyTime = event.getDownTime();
- interceptScreenshotChord();
- interceptRingerToggleChord();
- }
-
// Stop ringing or end call if configured to do so when power is pressed.
TelecomManager telecomManager = getTelecommService();
boolean hungUp = false;
@@ -946,9 +926,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// If the power key has still not yet been handled, then detect short
// press, long press, or multi press and decide what to do.
- mPowerKeyHandled = hungUp || mScreenshotChordVolumeDownKeyTriggered
- || mA11yShortcutChordVolumeUpKeyTriggered || gesturedServiceIntercepted
- || handledByPowerManager;
+ mPowerKeyHandled = hungUp || gesturedServiceIntercepted
+ || handledByPowerManager || mKeyCombinationManager.isPowerKeyIntercepted();
if (!mPowerKeyHandled) {
if (interactive) {
// When interactive, we're already awake.
@@ -1004,8 +983,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private void interceptPowerKeyUp(KeyEvent event, boolean interactive, boolean canceled) {
final boolean handled = canceled || mPowerKeyHandled;
- mScreenshotChordPowerKeyTriggered = false;
- cancelPendingScreenshotChordAction();
cancelPendingPowerKeyAction();
if (!handled) {
@@ -1315,52 +1292,22 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
private void interceptScreenshotChord() {
- if (mScreenshotChordEnabled
- && mScreenshotChordVolumeDownKeyTriggered && mScreenshotChordPowerKeyTriggered
- && !mA11yShortcutChordVolumeUpKeyTriggered) {
- final long now = SystemClock.uptimeMillis();
- if (now <= mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS
- && now <= mScreenshotChordPowerKeyTime
- + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {
- mScreenshotChordVolumeDownKeyConsumed = true;
- cancelPendingPowerKeyAction();
- mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
- mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_CHORD);
- mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());
- }
- }
+ mHandler.removeCallbacks(mScreenshotRunnable);
+ mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
+ mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_CHORD);
+ mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());
}
private void interceptAccessibilityShortcutChord() {
- if (mAccessibilityShortcutController.isAccessibilityShortcutAvailable(isKeyguardLocked())
- && mScreenshotChordVolumeDownKeyTriggered && mA11yShortcutChordVolumeUpKeyTriggered
- && !mScreenshotChordPowerKeyTriggered) {
- final long now = SystemClock.uptimeMillis();
- if (now <= mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS
- && now <= mA11yShortcutChordVolumeUpKeyTime
- + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {
- mScreenshotChordVolumeDownKeyConsumed = true;
- mA11yShortcutChordVolumeUpKeyConsumed = true;
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
- getAccessibilityShortcutTimeout());
- }
- }
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
+ getAccessibilityShortcutTimeout());
}
private void interceptRingerToggleChord() {
- if (mRingerToggleChord != Settings.Secure.VOLUME_HUSH_OFF
- && mScreenshotChordPowerKeyTriggered && mA11yShortcutChordVolumeUpKeyTriggered) {
- final long now = SystemClock.uptimeMillis();
- if (now <= mA11yShortcutChordVolumeUpKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS
- && now <= mScreenshotChordPowerKeyTime
- + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {
- mA11yShortcutChordVolumeUpKeyConsumed = true;
- cancelPendingPowerKeyAction();
-
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RINGER_TOGGLE_CHORD),
- getRingerToggleChordDelay());
- }
- }
+ mHandler.removeMessages(MSG_RINGER_TOGGLE_CHORD);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RINGER_TOGGLE_CHORD),
+ getRingerToggleChordDelay());
}
private long getAccessibilityShortcutTimeout() {
@@ -1942,9 +1889,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mSafeModeEnabledVibePattern = getLongIntArray(mContext.getResources(),
com.android.internal.R.array.config_safeModeEnabledVibePattern);
- mScreenshotChordEnabled = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_enableScreenshotChord);
-
mGlobalKeyManager = new GlobalKeyManager(mContext);
// Controls rotation and the like.
@@ -1980,6 +1924,92 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mWindowManagerFuncs.onKeyguardShowingAndNotOccludedChanged();
}
});
+ initKeyCombinationRules();
+ }
+
+ private void initKeyCombinationRules() {
+ mKeyCombinationManager = new KeyCombinationManager();
+ final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_enableScreenshotChord);
+
+ if (screenshotChordEnabled) {
+ mKeyCombinationManager.addRule(
+ new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) {
+ @Override
+ void execute() {
+ cancelPendingPowerKeyAction();
+ interceptScreenshotChord();
+ }
+ @Override
+ void cancel() {
+ cancelPendingScreenshotChordAction();
+ }
+ });
+ }
+
+ mKeyCombinationManager.addRule(
+ new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP) {
+ @Override
+ boolean preCondition() {
+ return mAccessibilityShortcutController
+ .isAccessibilityShortcutAvailable(isKeyguardLocked());
+ }
+ @Override
+ void execute() {
+ interceptAccessibilityShortcutChord();
+ }
+ @Override
+ void cancel() {
+ cancelPendingAccessibilityShortcutAction();
+ }
+ });
+
+ mKeyCombinationManager.addRule(
+ new TwoKeysCombinationRule(KEYCODE_VOLUME_UP, KEYCODE_POWER) {
+ @Override
+ boolean preCondition() {
+ return mRingerToggleChord != VOLUME_HUSH_OFF;
+ }
+ @Override
+ void execute() {
+ cancelPendingPowerKeyAction();
+ interceptRingerToggleChord();
+ }
+ @Override
+ void cancel() {
+ cancelPendingRingerToggleChordAction();
+ }
+ });
+
+ if (mHasFeatureLeanback) {
+ mKeyCombinationManager.addRule(
+ new TwoKeysCombinationRule(KEYCODE_BACK, KEYCODE_DPAD_DOWN) {
+ @Override
+ void execute() {
+ cancelPendingBackKeyAction();
+ interceptAccessibilityGestureTv();
+ }
+
+ @Override
+ void cancel() {
+ cancelAccessibilityGestureTv();
+ }
+ });
+
+ mKeyCombinationManager.addRule(
+ new TwoKeysCombinationRule(KEYCODE_DPAD_CENTER, KEYCODE_BACK) {
+ @Override
+ void execute() {
+ cancelPendingBackKeyAction();
+ interceptBugreportGestureTv();
+ }
+
+ @Override
+ void cancel() {
+ cancelBugreportGestureTv();
+ }
+ });
+ }
}
/**
@@ -2552,70 +2582,15 @@ public class PhoneWindowManager implements WindowManagerPolicy {
+ repeatCount + " keyguardOn=" + keyguardOn + " canceled=" + canceled);
}
- // If we think we might have a volume down & power key chord on the way
- // but we're not sure, then tell the dispatcher to wait a little while and
- // try again later before dispatching.
- if (mScreenshotChordEnabled && (flags & KeyEvent.FLAG_FALLBACK) == 0) {
- if (mScreenshotChordVolumeDownKeyTriggered && !mScreenshotChordPowerKeyTriggered) {
- final long now = SystemClock.uptimeMillis();
- final long timeoutTime = mScreenshotChordVolumeDownKeyTime
- + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS;
- if (now < timeoutTime) {
- return timeoutTime - now;
- }
- }
- if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
- && mScreenshotChordVolumeDownKeyConsumed) {
- if (!down) {
- mScreenshotChordVolumeDownKeyConsumed = false;
- }
- return -1;
- }
- }
-
- // If an accessibility shortcut might be partially complete, hold off dispatching until we
- // know if it is complete or not
- if (mAccessibilityShortcutController.isAccessibilityShortcutAvailable(false)
- && (flags & KeyEvent.FLAG_FALLBACK) == 0) {
- if (mScreenshotChordVolumeDownKeyTriggered ^ mA11yShortcutChordVolumeUpKeyTriggered) {
- final long now = SystemClock.uptimeMillis();
- final long timeoutTime = (mScreenshotChordVolumeDownKeyTriggered
- ? mScreenshotChordVolumeDownKeyTime : mA11yShortcutChordVolumeUpKeyTime)
- + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS;
- if (now < timeoutTime) {
- return timeoutTime - now;
- }
- }
- if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && mScreenshotChordVolumeDownKeyConsumed) {
- if (!down) {
- mScreenshotChordVolumeDownKeyConsumed = false;
- }
- return -1;
- }
- if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mA11yShortcutChordVolumeUpKeyConsumed) {
- if (!down) {
- mA11yShortcutChordVolumeUpKeyConsumed = false;
- }
- return -1;
- }
+ if (mKeyCombinationManager.isKeyConsumed(event)) {
+ return -1;
}
- // If a ringer toggle chord could be on the way but we're not sure, then tell the dispatcher
- // to wait a little while and try again later before dispatching.
- if (mRingerToggleChord != VOLUME_HUSH_OFF && (flags & KeyEvent.FLAG_FALLBACK) == 0) {
- if (mA11yShortcutChordVolumeUpKeyTriggered && !mScreenshotChordPowerKeyTriggered) {
- final long now = SystemClock.uptimeMillis();
- final long timeoutTime = mA11yShortcutChordVolumeUpKeyTime
- + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS;
- if (now < timeoutTime) {
- return timeoutTime - now;
- }
- }
- if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && mA11yShortcutChordVolumeUpKeyConsumed) {
- if (!down) {
- mA11yShortcutChordVolumeUpKeyConsumed = false;
- }
- return -1;
+ if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
+ final long now = SystemClock.uptimeMillis();
+ final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(keyCode);
+ if (now < interceptTimeout) {
+ return interceptTimeout - now;
}
}
@@ -2774,8 +2749,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
} else if (keyCode == KeyEvent.KEYCODE_TAB && event.isMetaPressed()) {
// Pass through keyboard navigation keys.
return 0;
- } else if (mHasFeatureLeanback && interceptBugreportGestureTv(keyCode, down)) {
- return -1;
} else if (keyCode == KeyEvent.KEYCODE_ALL_APPS) {
if (!down) {
mHandler.removeMessages(MSG_HANDLE_ALL_APPS);
@@ -2978,53 +2951,30 @@ public class PhoneWindowManager implements WindowManagerPolicy {
/**
* TV only: recognizes a remote control gesture for capturing a bug report.
*/
- private boolean interceptBugreportGestureTv(int keyCode, boolean down) {
+ private void interceptBugreportGestureTv() {
+ mHandler.removeMessages(MSG_BUGREPORT_TV);
// The bugreport capture chord is a long press on DPAD CENTER and BACK simultaneously.
- if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
- mBugreportTvKey1Pressed = down;
- } else if (keyCode == KeyEvent.KEYCODE_BACK) {
- mBugreportTvKey2Pressed = down;
- }
-
- if (mBugreportTvKey1Pressed && mBugreportTvKey2Pressed) {
- if (!mBugreportTvScheduled) {
- mBugreportTvScheduled = true;
- Message msg = Message.obtain(mHandler, MSG_BUGREPORT_TV);
- msg.setAsynchronous(true);
- mHandler.sendMessageDelayed(msg, BUGREPORT_TV_GESTURE_TIMEOUT_MILLIS);
- }
- } else if (mBugreportTvScheduled) {
- mHandler.removeMessages(MSG_BUGREPORT_TV);
- mBugreportTvScheduled = false;
- }
+ Message msg = Message.obtain(mHandler, MSG_BUGREPORT_TV);
+ msg.setAsynchronous(true);
+ mHandler.sendMessageDelayed(msg, BUGREPORT_TV_GESTURE_TIMEOUT_MILLIS);
+ }
- return mBugreportTvScheduled;
+ private void cancelBugreportGestureTv() {
+ mHandler.removeMessages(MSG_BUGREPORT_TV);
}
/**
* TV only: recognizes a remote control gesture as Accessibility shortcut.
* Shortcut: Long press (BACK + DPAD_DOWN)
*/
- private boolean interceptAccessibilityGestureTv(int keyCode, boolean down) {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- mAccessibilityTvKey1Pressed = down;
- } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
- mAccessibilityTvKey2Pressed = down;
- }
-
- if (mAccessibilityTvKey1Pressed && mAccessibilityTvKey2Pressed) {
- if (!mAccessibilityTvScheduled) {
- mAccessibilityTvScheduled = true;
- Message msg = Message.obtain(mHandler, MSG_ACCESSIBILITY_TV);
- msg.setAsynchronous(true);
- mHandler.sendMessageDelayed(msg, getAccessibilityShortcutTimeout());
- }
- } else if (mAccessibilityTvScheduled) {
- mHandler.removeMessages(MSG_ACCESSIBILITY_TV);
- mAccessibilityTvScheduled = false;
- }
-
- return mAccessibilityTvScheduled;
+ private void interceptAccessibilityGestureTv() {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_TV);
+ Message msg = Message.obtain(mHandler, MSG_ACCESSIBILITY_TV);
+ msg.setAsynchronous(true);
+ mHandler.sendMessageDelayed(msg, getAccessibilityShortcutTimeout());
+ }
+ private void cancelAccessibilityGestureTv() {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_TV);
}
private void requestBugreportForTv() {
@@ -3547,16 +3497,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
final int displayId = event.getDisplayId();
final boolean isInjected = (policyFlags & WindowManagerPolicy.FLAG_INJECTED) != 0;
- // If screen is off then we treat the case where the keyguard is open but hidden
- // the same as if it were open and in front.
- // This will prevent any keys other than the power button from waking the screen
- // when the keyguard is hidden by another activity.
- final boolean keyguardActive = (mKeyguardDelegate == null ? false :
- (interactive ?
- isKeyguardShowingAndNotOccluded() :
- mKeyguardDelegate.isShowing()));
-
if (DEBUG_INPUT) {
+ // If screen is off then we treat the case where the keyguard is open but hidden
+ // the same as if it were open and in front.
+ // This will prevent any keys other than the power button from waking the screen
+ // when the keyguard is hidden by another activity.
+ final boolean keyguardActive = (mKeyguardDelegate != null
+ && (interactive ? isKeyguardShowingAndNotOccluded() :
+ mKeyguardDelegate.isShowing()));
Log.d(TAG, "interceptKeyTq keycode=" + keyCode
+ " interactive=" + interactive + " keyguardActive=" + keyguardActive
+ " policyFlags=" + Integer.toHexString(policyFlags));
@@ -3581,7 +3529,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// Reset the pending key
mPendingWakeKey = PENDING_KEY_NULL;
}
- } else if (!interactive && shouldDispatchInputWhenNonInteractive(displayId, keyCode)) {
+ } else if (shouldDispatchInputWhenNonInteractive(displayId, keyCode)) {
// If we're currently dozing with the screen on and the keyguard showing, pass the key
// to the application but preserve its wake key status to make sure we still move
// from dozing to fully interactive if we would normally go from off to fully
@@ -3613,6 +3561,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return result;
}
+ if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
+ mKeyCombinationManager.interceptKey(event, interactive);
+ }
+
// Enable haptics if down and virtual key without multiple repetitions. If this is a hard
// virtual key such as a navigation bar button, only vibrate if flag is enabled.
final boolean isNavBarVirtKey = ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0);
@@ -3640,46 +3592,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_MUTE: {
- if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
- if (down) {
- // Any activity on the vol down button stops the ringer toggle shortcut
- cancelPendingRingerToggleChordAction();
-
- if (interactive && !mScreenshotChordVolumeDownKeyTriggered
- && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
- mScreenshotChordVolumeDownKeyTriggered = true;
- mScreenshotChordVolumeDownKeyTime = event.getDownTime();
- mScreenshotChordVolumeDownKeyConsumed = false;
- cancelPendingPowerKeyAction();
- interceptScreenshotChord();
- interceptAccessibilityShortcutChord();
- }
- } else {
- mScreenshotChordVolumeDownKeyTriggered = false;
- cancelPendingScreenshotChordAction();
- cancelPendingAccessibilityShortcutAction();
- }
- } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
- if (down) {
- if (interactive && !mA11yShortcutChordVolumeUpKeyTriggered
- && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
- mA11yShortcutChordVolumeUpKeyTriggered = true;
- mA11yShortcutChordVolumeUpKeyTime = event.getDownTime();
- mA11yShortcutChordVolumeUpKeyConsumed = false;
- cancelPendingPowerKeyAction();
- cancelPendingScreenshotChordAction();
- cancelPendingRingerToggleChordAction();
-
- interceptAccessibilityShortcutChord();
- interceptRingerToggleChord();
- }
- } else {
- mA11yShortcutChordVolumeUpKeyTriggered = false;
- cancelPendingScreenshotChordAction();
- cancelPendingAccessibilityShortcutAction();
- cancelPendingRingerToggleChordAction();
- }
- }
if (down) {
sendSystemKeyToStatusBarAsync(event.getKeyCode());
@@ -3784,7 +3696,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
KeyEvent.actionToString(event.getAction()),
mPowerKeyHandled ? 1 : 0, mPowerKeyPressCounter);
// Any activity on the power button stops the accessibility shortcut
- cancelPendingAccessibilityShortcutAction();
result &= ~ACTION_PASS_TO_USER;
isWakeKey = false; // wake-up will be handled separately
if (down) {
@@ -3922,22 +3833,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- // Intercept the Accessibility keychord for TV (DPAD_DOWN + Back) before the keyevent is
- // processed through interceptKeyEventBeforeDispatch since Talkback may consume this event
- // before it has a chance to reach that method.
- if (mHasFeatureLeanback) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_BACK: {
- boolean handled = interceptAccessibilityGestureTv(keyCode, down);
- if (handled) {
- result &= ~ACTION_PASS_TO_USER;
- }
- break;
- }
- }
- }
-
// Intercept the Accessibility keychord (CTRL + ALT + Z) for keyboard users.
if (mAccessibilityShortcutController.isAccessibilityShortcutAvailable(isKeyguardLocked())) {
switch (keyCode) {
@@ -5388,14 +5283,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
pw.print(!mAllowLockscreenWhenOnDisplays.isEmpty());
pw.print(" mLockScreenTimeout="); pw.print(mLockScreenTimeout);
pw.print(" mLockScreenTimerActive="); pw.println(mLockScreenTimerActive);
- if (mHasFeatureLeanback) {
- pw.print(prefix);
- pw.print("mAccessibilityTvKey1Pressed="); pw.println(mAccessibilityTvKey1Pressed);
- pw.print(prefix);
- pw.print("mAccessibilityTvKey2Pressed="); pw.println(mAccessibilityTvKey2Pressed);
- pw.print(prefix);
- pw.print("mAccessibilityTvScheduled="); pw.println(mAccessibilityTvScheduled);
- }
mGlobalKeyManager.dump(prefix, pw);
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java
new file mode 100644
index 000000000000..75479de26b1d
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java
@@ -0,0 +1,208 @@
+/*
+ * 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);
+ }
+}