diff options
| author | 2024-12-20 14:16:04 -0800 | |
|---|---|---|
| committer | 2024-12-20 14:16:04 -0800 | |
| commit | 8b6c5d3e79e956229eecfb31d69b50bf3ccb5abc (patch) | |
| tree | cab0776c2e096dbf57af6be844e8111b70dc53bc | |
| parent | d2273fad954bada855804ce9a49305410889dbf5 (diff) | |
| parent | 22bbefccbc2245b1419f5feb45cb4c5afad16747 (diff) | |
Merge "Allow focused window to capture power button - try 3." into main
9 files changed, 589 insertions, 21 deletions
diff --git a/core/java/com/android/internal/policy/KeyInterceptionInfo.java b/core/java/com/android/internal/policy/KeyInterceptionInfo.java index b20f6d225b69..fed8fe3b4cc0 100644 --- a/core/java/com/android/internal/policy/KeyInterceptionInfo.java +++ b/core/java/com/android/internal/policy/KeyInterceptionInfo.java @@ -27,11 +27,13 @@ public class KeyInterceptionInfo { // Debug friendly name to help identify the window public final String windowTitle; public final int windowOwnerUid; + public final int inputFeaturesFlags; - public KeyInterceptionInfo(int type, int flags, String title, int uid) { + public KeyInterceptionInfo(int type, int flags, String title, int uid, int inputFeaturesFlags) { layoutParamsType = type; layoutParamsPrivateFlags = flags; windowTitle = title; windowOwnerUid = uid; + this.inputFeaturesFlags = inputFeaturesFlags; } } diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index bedc1308e8eb..c1c03ff9ec11 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -18,6 +18,7 @@ package com.android.server; import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap; +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis; import android.app.ActivityManager; @@ -232,7 +233,7 @@ public class GestureLauncherService extends SystemService { } @VisibleForTesting - GestureLauncherService(Context context, MetricsLogger metricsLogger, + public GestureLauncherService(Context context, MetricsLogger metricsLogger, QuickAccessWalletClient quickAccessWalletClient, UiEventLogger uiEventLogger) { super(context); mContext = context; @@ -600,6 +601,45 @@ public class GestureLauncherService extends SystemService { return res; } + /** + * Processes a power key event in GestureLauncherService without performing an action. This + * method is called on every KEYCODE_POWER ACTION_DOWN event and ensures that, even if + * KEYCODE_POWER events are passed to and handled by the app, the GestureLauncherService still + * keeps track of all running KEYCODE_POWER events for its gesture detection and relevant + * actions. + */ + public void processPowerKeyDown(KeyEvent event) { + if (mEmergencyGestureEnabled && mEmergencyGesturePowerButtonCooldownPeriodMs >= 0 + && event.getEventTime() - mLastEmergencyGestureTriggered + < mEmergencyGesturePowerButtonCooldownPeriodMs) { + return; + } + if (event.isLongPress()) { + return; + } + + final long powerTapInterval; + + synchronized (this) { + powerTapInterval = event.getEventTime() - mLastPowerDown; + mLastPowerDown = event.getEventTime(); + if (powerTapInterval >= POWER_SHORT_TAP_SEQUENCE_MAX_INTERVAL_MS) { + // Tap too slow, reset consecutive tap counts. + mFirstPowerDown = event.getEventTime(); + mPowerButtonConsecutiveTaps = 1; + mPowerButtonSlowConsecutiveTaps = 1; + } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) { + // Tap too slow for shortcuts + mFirstPowerDown = event.getEventTime(); + mPowerButtonConsecutiveTaps = 1; + mPowerButtonSlowConsecutiveTaps++; + } else if (!overridePowerKeyBehaviorInFocusedWindow() || powerTapInterval > 0) { + // Fast consecutive tap + mPowerButtonConsecutiveTaps++; + mPowerButtonSlowConsecutiveTaps++; + } + } + } /** * Attempts to intercept power key down event by detecting certain gesture patterns @@ -648,7 +688,7 @@ public class GestureLauncherService extends SystemService { mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; mPowerButtonSlowConsecutiveTaps++; - } else { + } else if (powerTapInterval > 0) { // Fast consecutive tap mPowerButtonConsecutiveTaps++; mPowerButtonSlowConsecutiveTaps++; diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 17b712de3be6..5ab59657d4ce 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -88,7 +88,9 @@ import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGest import static com.android.hardware.input.Flags.inputManagerLifecycleSupport; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.hardware.input.Flags.modifierShortcutDump; +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; +import static com.android.server.GestureLauncherService.DOUBLE_POWER_TAP_COUNT_THRESHOLD; import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; @@ -191,6 +193,7 @@ import android.service.dreams.IDreamManager; import android.service.vr.IPersistentVrStateCallbacks; import android.speech.RecognizerIntent; import android.telecom.TelecomManager; +import android.text.TextUtils; import android.util.Log; import android.util.MathUtils; import android.util.MutableBoolean; @@ -431,6 +434,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { "android.intent.action.VOICE_ASSIST_RETAIL"; /** + * Maximum amount of time in milliseconds between consecutive power onKeyDown events to be + * considered a multi-press, only used for the power button. + * Note: To maintain backwards compatibility for the power button, we are measuring the times + * between consecutive down events instead of the first tap's up event and the second tap's + * down event. + */ + @VisibleForTesting public static final int POWER_MULTI_PRESS_TIMEOUT_MILLIS = + ViewConfiguration.getMultiPressTimeout(); + + /** * Lock protecting internal state. Must not call out into window * manager with lock held. (This lock will be acquired in places * where the window manager is calling in with its own lock held.) @@ -491,6 +504,32 @@ public class PhoneWindowManager implements WindowManagerPolicy { private WindowWakeUpPolicy mWindowWakeUpPolicy; + /** + * The three variables below are used for custom power key gesture detection in + * PhoneWindowManager. They are used to detect when the power button has been double pressed + * and, when it does happen, makes the behavior overrideable by the app. + * + * We cannot use the {@link PowerKeyRule} for this because multi-press power gesture detection + * and behaviors are handled by {@link com.android.server.GestureLauncherService}, and the + * {@link PowerKeyRule} only handles single and long-presses of the power button. As a result, + * overriding the double tap behavior requires custom gesture detection here that mimics the + * logic in {@link com.android.server.GestureLauncherService}. + * + * Long-term, it would be beneficial to move all power gesture detection to + * {@link PowerKeyRule} so that this custom logic isn't required. + */ + // Time of last power down event. + private long mLastPowerDown; + + // Number of power button events consecutively triggered (within a specific timeout threshold). + private int mPowerButtonConsecutiveTaps = 0; + + // Whether a double tap of the power button has been detected. + volatile boolean mDoubleTapPowerDetected; + + // Runnable that is queued on a delay when the first power keyDown event is sent to the app. + private Runnable mPowerKeyDelayedRunnable = null; + boolean mSafeMode; // Whether to allow dock apps with METADATA_DOCK_HOME to temporarily take over the Home key. @@ -1079,6 +1118,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { mPowerKeyHandled = mPowerKeyHandled || hungUp || handledByPowerManager || isKeyGestureTriggered || mKeyCombinationManager.isPowerKeyIntercepted(); + + if (overridePowerKeyBehaviorInFocusedWindow()) { + mPowerKeyHandled |= mDoubleTapPowerDetected; + } + if (!mPowerKeyHandled) { if (!interactive) { wakeUpFromWakeKey(event); @@ -2651,7 +2695,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mShouldEarlyShortPressOnPower) { return; } - powerPress(downTime, 1 /*count*/, displayId); + // TODO(b/380433365): Remove deferring single power press action when refactoring. + if (overridePowerKeyBehaviorInFocusedWindow()) { + mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); + mDeferredKeyActionExecutor.queueKeyAction( + KEYCODE_POWER, + downTime, + () -> { + powerPress(downTime, 1 /*count*/, displayId); + }); + } else { + powerPress(downTime, 1 /*count*/, displayId); + } } @Override @@ -2682,7 +2737,17 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onMultiPress(long downTime, int count, int displayId) { - powerPress(downTime, count, displayId); + if (overridePowerKeyBehaviorInFocusedWindow()) { + mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); + mDeferredKeyActionExecutor.queueKeyAction( + KEYCODE_POWER, + downTime, + () -> { + powerPress(downTime, count, displayId); + }); + } else { + powerPress(downTime, count, displayId); + } } @Override @@ -3459,6 +3524,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + if (overridePowerKeyBehaviorInFocusedWindow() && event.getKeyCode() == KEYCODE_POWER + && event.getAction() == KeyEvent.ACTION_UP + && mDoubleTapPowerDetected) { + mDoubleTapPowerDetected = false; + } + return needToConsumeKey ? keyConsumed : keyNotConsumed; } @@ -3974,6 +4045,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } + case KeyEvent.KEYCODE_POWER: + return interceptPowerKeyBeforeDispatching(focusedToken, event); case KeyEvent.KEYCODE_SCREENSHOT: if (firstDown) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); @@ -4029,6 +4102,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } + case KeyEvent.KEYCODE_POWER: + return interceptPowerKeyBeforeDispatching(focusedToken, event); } if (isValidGlobalKey(keyCode) && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) { @@ -4039,6 +4114,90 @@ public class PhoneWindowManager implements WindowManagerPolicy { return (metaState & KeyEvent.META_META_ON) != 0; } + /** + * Called by interceptKeyBeforeDispatching to handle interception logic for KEYCODE_POWER + * KeyEvents. + * + * @return true if intercepting the key, false if sending to app. + */ + private boolean interceptPowerKeyBeforeDispatching(IBinder focusedToken, KeyEvent event) { + if (!overridePowerKeyBehaviorInFocusedWindow()) { + //Flag disabled: intercept the power key and do not send to app. + return true; + } + if (event.getKeyCode() != KEYCODE_POWER) { + Log.wtf(TAG, "interceptPowerKeyBeforeDispatching received a non-power KeyEvent " + + "with key code: " + event.getKeyCode()); + return false; + } + + // Intercept keys (don't send to app) for 3x, 4x, 5x gestures) + if (mPowerButtonConsecutiveTaps > DOUBLE_POWER_TAP_COUNT_THRESHOLD) { + setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime()); + return true; + } + + // UP key; just reuse the original decision. + if (event.getAction() == KeyEvent.ACTION_UP) { + final Set<Integer> consumedKeys = mConsumedKeysForDevice.get(event.getDeviceId()); + return consumedKeys != null + && consumedKeys.contains(event.getKeyCode()); + } + + KeyInterceptionInfo info = + mWindowManagerInternal.getKeyInterceptionInfoFromToken(focusedToken); + + if (info == null || !mButtonOverridePermissionChecker.canWindowOverridePowerKey(mContext, + info.windowOwnerUid, info.inputFeaturesFlags)) { + // The focused window does not have the permission to override power key behavior. + if (DEBUG_INPUT) { + String interceptReason = ""; + if (info == null) { + interceptReason = "Window is null"; + } else if (!mButtonOverridePermissionChecker.canAppOverrideSystemKey(mContext, + info.windowOwnerUid)) { + interceptReason = "Application does not have " + + "OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission"; + } else { + interceptReason = "Window does not have inputFeatureFlag set"; + } + + Log.d(TAG, TextUtils.formatSimple("Intercepting KEYCODE_POWER event. action=%d, " + + "eventTime=%d to window=%s. interceptReason=%s. " + + "mDoubleTapPowerDetected=%b", + event.getAction(), event.getEventTime(), (info != null) + ? info.windowTitle : "null", interceptReason, + mDoubleTapPowerDetected)); + } + // Intercept the key (i.e. do not send to app) + setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime()); + return true; + } + + if (DEBUG_INPUT) { + Log.d(TAG, TextUtils.formatSimple("Sending KEYCODE_POWER to app. action=%d, " + + "eventTime=%d to window=%s. mDoubleTapPowerDetected=%b", + event.getAction(), event.getEventTime(), info.windowTitle, + mDoubleTapPowerDetected)); + } + + if (!mDoubleTapPowerDetected) { + //Single press: post a delayed runnable for the single press power action that will be + // called if it's not cancelled by a double press. + final var downTime = event.getDownTime(); + mPowerKeyDelayedRunnable = () -> + setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, downTime); + mHandler.postDelayed(mPowerKeyDelayedRunnable, POWER_MULTI_PRESS_TIMEOUT_MILLIS); + } else if (mPowerKeyDelayedRunnable != null) { + //Double press detected: cancel the single press runnable. + mHandler.removeCallbacks(mPowerKeyDelayedRunnable); + mPowerKeyDelayedRunnable = null; + } + + // Focused window has permission. Send to app. + return false; + } + @SuppressLint("MissingPermission") private void initKeyGestures() { if (!useKeyGestureEventHandler()) { @@ -4562,6 +4721,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } + if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) { + handleUnhandledSystemKey(event); + return true; + } + if (useKeyGestureEventHandler()) { return false; } @@ -5396,8 +5560,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyEvent.actionToString(event.getAction()), mPowerKeyHandled ? 1 : 0, mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER)); - // Any activity on the power button stops the accessibility shortcut - result &= ~ACTION_PASS_TO_USER; + if (overridePowerKeyBehaviorInFocusedWindow()) { + result |= ACTION_PASS_TO_USER; + } else { + // Any activity on the power button stops the accessibility shortcut + result &= ~ACTION_PASS_TO_USER; + } isWakeKey = false; // wake-up will be handled separately if (down) { interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered); @@ -5659,6 +5827,35 @@ public class PhoneWindowManager implements WindowManagerPolicy { } if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) { + if (overridePowerKeyBehaviorInFocusedWindow()) { + if (event.getRepeatCount() > 0 && !mHasFeatureWatch) { + return; + } + if (mGestureLauncherService != null) { + mGestureLauncherService.processPowerKeyDown(event); + } + + if (detectDoubleTapPower(event)) { + mDoubleTapPowerDetected = true; + + // Copy of the event for handler in case the original event gets recycled. + KeyEvent eventCopy = KeyEvent.obtain(event); + mDeferredKeyActionExecutor.queueKeyAction( + KeyEvent.KEYCODE_POWER, + eventCopy.getEventTime(), + () -> { + if (!handleCameraGesture(eventCopy, interactive)) { + mSingleKeyGestureDetector.interceptKey( + eventCopy, interactive, defaultDisplayOn); + } else { + mSingleKeyGestureDetector.reset(); + } + eventCopy.recycle(); + }); + return; + } + } + mPowerKeyHandled = handleCameraGesture(event, interactive); if (mPowerKeyHandled) { // handled by camera gesture. @@ -5670,6 +5867,26 @@ public class PhoneWindowManager implements WindowManagerPolicy { mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn); } + private boolean detectDoubleTapPower(KeyEvent event) { + //Watches use the SingleKeyGestureDetector for detecting multi-press gestures. + if (mHasFeatureWatch || event.getKeyCode() != KEYCODE_POWER + || event.getAction() != KeyEvent.ACTION_DOWN || event.getRepeatCount() != 0) { + return false; + } + + final long powerTapInterval = event.getEventTime() - mLastPowerDown; + mLastPowerDown = event.getEventTime(); + if (powerTapInterval >= POWER_MULTI_PRESS_TIMEOUT_MILLIS) { + // Tap too slow for double press + mPowerButtonConsecutiveTaps = 1; + } else { + mPowerButtonConsecutiveTaps++; + } + + return powerTapInterval < POWER_MULTI_PRESS_TIMEOUT_MILLIS + && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD; + } + // The camera gesture will be detected by GestureLauncherService. private boolean handleCameraGesture(KeyEvent event, boolean interactive) { // camera gesture. @@ -7526,6 +7743,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { null) == PERMISSION_GRANTED; } + + boolean canWindowOverridePowerKey(Context context, int uid, int inputFeaturesFlags) { + return canAppOverrideSystemKey(context, uid) + && (inputFeaturesFlags & WindowManager.LayoutParams + .INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0; + } } private int getTargetDisplayIdForKeyEvent(KeyEvent event) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 68b4b6f0ae91..b43e334d6e1a 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5750,9 +5750,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP || mKeyInterceptionInfo.layoutParamsPrivateFlags != getAttrs().privateFlags || mKeyInterceptionInfo.layoutParamsType != getAttrs().type || mKeyInterceptionInfo.windowTitle != getWindowTag() - || mKeyInterceptionInfo.windowOwnerUid != getOwningUid()) { + || mKeyInterceptionInfo.windowOwnerUid != getOwningUid() + || mKeyInterceptionInfo.inputFeaturesFlags != getAttrs().inputFeatures) { mKeyInterceptionInfo = new KeyInterceptionInfo(getAttrs().type, getAttrs().privateFlags, - getWindowTag().toString(), getOwningUid()); + getWindowTag().toString(), getOwningUid(), getAttrs().inputFeatures); } return mKeyInterceptionInfo; } diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java index 71a2651a0f14..45761b696877 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -1697,6 +1697,46 @@ public class GestureLauncherServiceTest { } /** + * If processPowerKeyDown is called instead of interceptPowerKeyDown (meaning the double tap + * gesture isn't performed), the emergency gesture is still launched. + */ + @Test + public void testProcessPowerKeyDown_fiveInboundPresses_emergencyGestureLaunches() { + enableCameraGesture(); + enableEmergencyGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + //Second event; call processPowerKeyDown without calling interceptPowerKeyDown + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + KeyEvent keyEvent = + new KeyEvent( + IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); + mGestureLauncherService.processPowerKeyDown(keyEvent); + + verify(mMetricsLogger, never()) + .action(eq(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE), anyInt()); + verify(mUiEventLogger, never()).log(any()); + + // Presses 3 and 4 should not trigger any gesture + for (int i = 0; i < 2; i++) { + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false); + } + + // Fifth button press should still trigger the emergency flow + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + verify(mUiEventLogger, times(1)) + .log(GestureLauncherService.GestureLauncherEvent.GESTURE_EMERGENCY_TAP_POWER); + verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected(); + } + + /** * Helper method to trigger emergency gesture by pressing button for 5 times. * * @return last event time. diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java index 05a1482b9be6..33ccec3f785a 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java @@ -18,11 +18,16 @@ package com.android.server.policy; import static android.view.KeyEvent.KEYCODE_POWER; import static android.view.KeyEvent.KEYCODE_VOLUME_UP; +import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS; +import static com.android.server.policy.PhoneWindowManager.POWER_MULTI_PRESS_TIMEOUT_MILLIS; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_SLEEP; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_GO_TO_SLEEP; +import static org.junit.Assert.assertEquals; + +import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.view.Display; @@ -39,6 +44,7 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { @Before public void setUp() { setUpPhoneWindowManager(); + mPhoneWindowManager.overrideStatusBarManagerInternal(); } /** @@ -50,6 +56,8 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER); mPhoneWindowManager.assertPowerSleep(); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + // turn screen on when begin from non-interactive. mPhoneWindowManager.overrideDisplayState(Display.STATE_OFF); sendKey(KEYCODE_POWER); @@ -90,7 +98,7 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { mPhoneWindowManager.overrideCanStartDreaming(false); sendKey(KEYCODE_POWER); sendKey(KEYCODE_POWER); - mPhoneWindowManager.assertCameraLaunch(); + mPhoneWindowManager.assertDoublePowerLaunch(); mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); } @@ -101,7 +109,7 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { public void testPowerDoublePress() { sendKey(KEYCODE_POWER); sendKey(KEYCODE_POWER); - mPhoneWindowManager.assertCameraLaunch(); + mPhoneWindowManager.assertDoublePowerLaunch(); } /** @@ -111,12 +119,14 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { public void testPowerLongPress() { // Show assistant. mPhoneWindowManager.overrideLongPressOnPower(LONG_PRESS_POWER_ASSISTANT); - sendKey(KEYCODE_POWER, true); + sendKey(KEYCODE_POWER, SingleKeyGestureDetector.sDefaultLongPressTimeout); mPhoneWindowManager.assertSearchManagerLaunchAssist(); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + // Show global actions. mPhoneWindowManager.overrideLongPressOnPower(LONG_PRESS_POWER_GLOBAL_ACTIONS); - sendKey(KEYCODE_POWER, true); + sendKey(KEYCODE_POWER, SingleKeyGestureDetector.sDefaultLongPressTimeout); mPhoneWindowManager.assertShowGlobalActionsCalled(); } @@ -141,4 +151,143 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER); mPhoneWindowManager.assertNoPowerSleep(); } + + + /** + * Double press of power when the window handles the power key events. The + * system double power gesture launch should not be performed. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerDoublePress_windowHasOverridePermissionAndKeysHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); + + mPhoneWindowManager.assertNoDoublePowerLaunch(); + } + + /** + * Double press of power when the window doesn't handle the power key events. + * The system default gesture launch should be performed and the app should receive both events. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerDoublePress_windowHasOverridePermissionAndKeysUnHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> false); + + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); + mPhoneWindowManager.assertDoublePowerLaunch(); + assertEquals(getDownKeysDispatched(), 2); + assertEquals(getUpKeysDispatched(), 2); + } + + /** + * Triple press of power when the window handles the power key double press gesture. + * The system default gesture launch should not be performed, and the app only receives the + * first two presses. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerTriplePress_windowHasOverridePermissionAndKeysHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); + mPhoneWindowManager.assertNoDoublePowerLaunch(); + assertEquals(getDownKeysDispatched(), 2); + assertEquals(getUpKeysDispatched(), 2); + } + + /** + * Tests a single press, followed by a double press when the window can handle the power key. + * The app should receive all 3 events. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerTriplePressWithDelay_windowHasOverridePermissionAndKeysHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + + sendKey(KEYCODE_POWER); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertNoDoublePowerLaunch(); + assertEquals(getDownKeysDispatched(), 3); + assertEquals(getUpKeysDispatched(), 3); + } + + /** + * Tests single press when window doesn't handle the power key. Phone should go to sleep. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerSinglePress_windowHasOverridePermissionAndKeyUnhandledByApp() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> false); + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); + + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertPowerSleep(); + } + + /** + * Tests single press when the window handles the power key. Phone should go to sleep after a + * delay of {POWER_MULTI_PRESS_TIMEOUT_MILLIS} + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerSinglePress_windowHasOverridePermissionAndKeyHandledByApp() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); + + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + + mPhoneWindowManager.assertPowerSleep(); + } + + + /** + * Tests 5x press when the window handles the power key. Emergency gesture should still be + * launched. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerFiveTimesPress_windowHasOverridePermissionAndKeyHandledByApp() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); + + int minEmergencyGestureDurationMillis = mContext.getResources().getInteger( + com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis); + int durationMillis = minEmergencyGestureDurationMillis / 4; + for (int i = 0; i < 5; ++i) { + sendKey(KEYCODE_POWER); + mPhoneWindowManager.moveTimeForward(durationMillis); + } + + mPhoneWindowManager.assertEmergencyLaunch(); + assertEquals(getDownKeysDispatched(), 2); + assertEquals(getUpKeysDispatched(), 2); + } } diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java index 9e47a008592c..8ede9efd32a0 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java @@ -85,7 +85,9 @@ class ShortcutKeyTestBase { private Resources mResources; private PackageManager mPackageManager; TestPhoneWindowManager mPhoneWindowManager; - DispatchedKeyHandler mDispatchedKeyHandler = event -> false; + DispatchedKeyHandler mDispatchedKeyHandler; + private int mDownKeysDispatched; + private int mUpKeysDispatched; Context mContext; /** Modifier key to meta state */ @@ -116,6 +118,9 @@ class ShortcutKeyTestBase { XmlResourceParser testBookmarks = mResources.getXml( com.android.frameworks.wmtests.R.xml.bookmarks); doReturn(testBookmarks).when(mResources).getXml(com.android.internal.R.xml.bookmarks); + mDispatchedKeyHandler = event -> false; + mDownKeysDispatched = 0; + mUpKeysDispatched = 0; try { // Keep packageName / className in sync with @@ -229,6 +234,10 @@ class ShortcutKeyTestBase { sendKeyCombination(new int[]{keyCode}, 0 /*durationMillis*/, longPress, DEFAULT_DISPLAY); } + void sendKey(int keyCode, long durationMillis) { + sendKeyCombination(new int[]{keyCode}, durationMillis, false, DEFAULT_DISPLAY); + } + boolean sendKeyGestureEventStart(int gestureType) { return mPhoneWindowManager.sendKeyGestureEvent( new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction( @@ -278,6 +287,14 @@ class ShortcutKeyTestBase { doReturn(expectedBehavior).when(mResources).getInteger(eq(resId)); } + int getDownKeysDispatched() { + return mDownKeysDispatched; + } + + int getUpKeysDispatched() { + return mUpKeysDispatched; + } + private void interceptKey(KeyEvent keyEvent) { int actions = mPhoneWindowManager.interceptKeyBeforeQueueing(keyEvent); if ((actions & ACTION_PASS_TO_USER) != 0) { @@ -285,6 +302,11 @@ class ShortcutKeyTestBase { if (!mDispatchedKeyHandler.onKeyDispatched(keyEvent)) { mPhoneWindowManager.interceptUnhandledKey(keyEvent); } + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + ++mDownKeysDispatched; + } else { + ++mUpKeysDispatched; + } } } mPhoneWindowManager.dispatchAllPendingEvents(); diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 285d94d2fafe..6c48ba26a475 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -37,6 +37,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GO_TO_VOICE_ASSIST; @@ -45,10 +46,14 @@ import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_SHUT import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM; import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.after; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.description; import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.timeout; @@ -85,7 +90,9 @@ import android.os.VibratorInfo; import android.os.test.TestLooper; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; +import android.service.quickaccesswallet.QuickAccessWalletClient; import android.telecom.TelecomManager; +import android.util.MutableBoolean; import android.view.Display; import android.view.InputEvent; import android.view.KeyCharacterMap; @@ -95,9 +102,12 @@ import android.view.autofill.AutofillManagerInternal; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.accessibility.AccessibilityShortcutController; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.KeyInterceptionInfo; import com.android.server.GestureLauncherService; import com.android.server.LocalServices; +import com.android.server.SystemService; import com.android.server.input.InputManagerInternal; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.pm.UserManagerInternal; @@ -120,6 +130,7 @@ import org.mockito.MockSettings; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.util.List; import java.util.function.Supplier; @@ -132,6 +143,7 @@ class TestPhoneWindowManager { private PhoneWindowManager mPhoneWindowManager; private Context mContext; + private GestureLauncherService mGestureLauncherService; @Mock private WindowManagerInternal mWindowManagerInternal; @Mock private ActivityManagerInternal mActivityManagerInternal; @@ -163,7 +175,9 @@ class TestPhoneWindowManager { @Mock private DisplayRotation mDisplayRotation; @Mock private DisplayPolicy mDisplayPolicy; @Mock private WindowManagerPolicy.ScreenOnListener mScreenOnListener; - @Mock private GestureLauncherService mGestureLauncherService; + @Mock private QuickAccessWalletClient mQuickAccessWalletClient; + @Mock private MetricsLogger mMetricsLogger; + @Mock private UiEventLogger mUiEventLogger; @Mock private GlobalActions mGlobalActions; @Mock private AccessibilityShortcutController mAccessibilityShortcutController; @@ -192,6 +206,8 @@ class TestPhoneWindowManager { private int mKeyEventPolicyFlags = FLAG_INTERACTIVE; + private int mProcessPowerKeyDownCount = 0; + private class TestTalkbackShortcutController extends TalkbackShortcutController { TestTalkbackShortcutController(Context context) { super(context); @@ -260,6 +276,8 @@ class TestPhoneWindowManager { MockitoAnnotations.initMocks(this); mHandler = new Handler(mTestLooper.getLooper()); mContext = mockingDetails(context).isSpy() ? context : spy(context); + mGestureLauncherService = spy(new GestureLauncherService(mContext, mMetricsLogger, + mQuickAccessWalletClient, mUiEventLogger)); setUp(supportSettingsUpdate); mTestLooper.dispatchAll(); } @@ -272,6 +290,7 @@ class TestPhoneWindowManager { mMockitoSession = mockitoSession() .mockStatic(LocalServices.class, spyStubOnly) .mockStatic(KeyCharacterMap.class) + .mockStatic(GestureLauncherService.class) .strictness(Strictness.LENIENT) .startMocking(); @@ -296,6 +315,16 @@ class TestPhoneWindowManager { () -> LocalServices.getService(eq(DisplayManagerInternal.class))); doReturn(mGestureLauncherService).when( () -> LocalServices.getService(eq(GestureLauncherService.class))); + doReturn(true).when( + () -> GestureLauncherService.isCameraDoubleTapPowerSettingEnabled(any(), anyInt()) + ); + doReturn(true).when( + () -> GestureLauncherService.isEmergencyGestureSettingEnabled(any(), anyInt()) + ); + doReturn(true).when( + () -> GestureLauncherService.isGestureLauncherEnabled(any()) + ); + mGestureLauncherService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START); doReturn(mUserManagerInternal).when( () -> LocalServices.getService(eq(UserManagerInternal.class))); doReturn(null).when(() -> LocalServices.getService(eq(VrManagerInternal.class))); @@ -375,7 +404,7 @@ class TestPhoneWindowManager { doNothing().when(mContext).startActivityAsUser(any(), any()); doNothing().when(mContext).startActivityAsUser(any(), any(), any()); - KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0); + KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0, 0); doReturn(interceptionInfo) .when(mWindowManagerInternal).getKeyInterceptionInfoFromToken(any()); @@ -393,6 +422,8 @@ class TestPhoneWindowManager { eq(TEST_BROWSER_ROLE_PACKAGE_NAME)); doReturn(mSmsIntent).when(mPackageManager).getLaunchIntentForPackage( eq(TEST_SMS_ROLE_PACKAGE_NAME)); + mProcessPowerKeyDownCount = 0; + captureProcessPowerKeyDownCount(); Mockito.reset(mContext); } @@ -650,6 +681,12 @@ class TestPhoneWindowManager { .when(mButtonOverridePermissionChecker).canAppOverrideSystemKey(any(), anyInt()); } + void overrideCanWindowOverridePowerKey(boolean granted) { + doReturn(granted) + .when(mButtonOverridePermissionChecker).canWindowOverridePowerKey(any(), anyInt(), + anyInt()); + } + void overrideKeyEventPolicyFlags(int flags) { mKeyEventPolicyFlags = flags; } @@ -725,11 +762,56 @@ class TestPhoneWindowManager { verify(mPowerManager, never()).goToSleep(anyLong(), anyInt(), anyInt()); } - void assertCameraLaunch() { + void assertDoublePowerLaunch() { + ArgumentCaptor<MutableBoolean> valueCaptor = ArgumentCaptor.forClass(MutableBoolean.class); + mTestLooper.dispatchAll(); - // GestureLauncherService should receive interceptPowerKeyDown twice. - verify(mGestureLauncherService, times(2)) - .interceptPowerKeyDown(any(), anyBoolean(), any()); + verify(mGestureLauncherService, atLeast(2)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + verify(mGestureLauncherService, atMost(4)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + + if (overridePowerKeyBehaviorInFocusedWindow()) { + assertTrue(mProcessPowerKeyDownCount >= 2 && mProcessPowerKeyDownCount <= 4); + } + + List<Boolean> capturedValues = valueCaptor.getAllValues().stream() + .map(mutableBoolean -> mutableBoolean.value) + .toList(); + + assertTrue(capturedValues.contains(true)); + } + + void assertNoDoublePowerLaunch() { + ArgumentCaptor<MutableBoolean> valueCaptor = ArgumentCaptor.forClass(MutableBoolean.class); + + mTestLooper.dispatchAll(); + verify(mGestureLauncherService, atLeast(0)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + + List<Boolean> capturedValues = valueCaptor.getAllValues().stream() + .map(mutableBoolean -> mutableBoolean.value) + .toList(); + + assertTrue(capturedValues.stream().noneMatch(value -> value)); + } + + void assertEmergencyLaunch() { + ArgumentCaptor<MutableBoolean> valueCaptor = ArgumentCaptor.forClass(MutableBoolean.class); + + mTestLooper.dispatchAll(); + verify(mGestureLauncherService, atLeast(1)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + + if (overridePowerKeyBehaviorInFocusedWindow()) { + assertEquals(mProcessPowerKeyDownCount, 5); + } + + List<Boolean> capturedValues = valueCaptor.getAllValues().stream() + .map(mutableBoolean -> mutableBoolean.value) + .toList(); + + assertTrue(capturedValues.getLast()); } void assertSearchManagerLaunchAssist() { @@ -952,4 +1034,12 @@ class TestPhoneWindowManager { verify(mContext, never()).startActivityAsUser(any(), any(), any()); verify(mContext, never()).startActivityAsUser(any(), any()); } + + private void captureProcessPowerKeyDownCount() { + doAnswer((Answer<Void>) invocation -> { + invocation.callRealMethod(); + mProcessPowerKeyDownCount++; + return null; + }).when(mGestureLauncherService).processPowerKeyDown(any()); + } } diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index aea75d8e7c85..d75a8f433af8 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -613,7 +613,8 @@ class InputManagerServiceTests { 0 }, "title", - /* uid = */0 + /* uid = */0, + /* inputFeatureFlags = */ 0 ) whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info) } |