diff options
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); + } +} |