From d6efa30ef9e9cf93d94dca8204019b347275f0a0 Mon Sep 17 00:00:00 2001 From: Charles Wang Date: Wed, 4 Dec 2024 16:54:46 +0000 Subject: Revert "Revert "Allow focused privileged windows to capture the ..." Revert submission 30698369-revert-30412668-HWHJRSWUQP Reason for revert: Created a fix for the failing test Reverted changes: /q/submissionid:30698369-revert-30412668-HWHJRSWUQP Bug: b/382133936 Test: android.platform.test.scenario.sysui.power.PowerMenuTest#testPower_verifySystemPowerMenuAppears Change-Id: I705b6371677ddbb71769a7c6cd21ddb0819ea301 --- .../internal/policy/KeyInterceptionInfo.java | 4 +- .../com/android/server/GestureLauncherService.java | 53 ++++- .../android/server/policy/PhoneWindowManager.java | 232 ++++++++++++++++++++- .../java/com/android/server/wm/WindowState.java | 5 +- .../android/server/GestureLauncherServiceTest.java | 69 ++++++ .../server/policy/PowerKeyGestureTests.java | 154 +++++++++++++- .../android/server/policy/ShortcutKeyTestBase.java | 22 +- .../server/policy/TestPhoneWindowManager.java | 105 +++++++++- .../server/input/InputManagerServiceTests.kt | 3 +- 9 files changed, 624 insertions(+), 23 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 ccc44a41759b..19bc8e31791e 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -16,6 +16,7 @@ package com.android.server; +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis; import android.app.ActivityManager; @@ -103,7 +104,7 @@ public class GestureLauncherService extends SystemService { /** * Number of taps required to launch camera shortcut. */ - private static final int CAMERA_POWER_TAP_COUNT_THRESHOLD = 2; + public static final int CAMERA_POWER_TAP_COUNT_THRESHOLD = 2; /** The listener that receives the gesture event. */ private final GestureEventListener mGestureListener = new GestureEventListener(); @@ -208,7 +209,9 @@ public class GestureLauncherService extends SystemService { } @VisibleForTesting - GestureLauncherService(Context context, MetricsLogger metricsLogger, + public GestureLauncherService( + Context context, + MetricsLogger metricsLogger, UiEventLogger uiEventLogger) { super(context); mContext = context; @@ -500,6 +503,46 @@ public class GestureLauncherService extends SystemService { || isEmergencyGestureEnabled(resources); } + /** + * 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 >= CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS) { + // Tap too slow for shortcuts + mFirstPowerDown = event.getEventTime(); + mPowerButtonConsecutiveTaps = 1; + mPowerButtonSlowConsecutiveTaps++; + } else if (powerTapInterval > 0) { + // Fast consecutive tap + mPowerButtonConsecutiveTaps++; + mPowerButtonSlowConsecutiveTaps++; + } + } + } + /** * Attempts to intercept power key down event by detecting certain gesture patterns * @@ -507,8 +550,8 @@ public class GestureLauncherService extends SystemService { * @param outLaunched true if some action is taken as part of the key intercept (eg, app launch) * @return true if the key down event is intercepted */ - public boolean interceptPowerKeyDown(KeyEvent event, boolean interactive, - MutableBoolean outLaunched) { + public boolean interceptPowerKeyDown( + KeyEvent event, boolean interactive, MutableBoolean outLaunched) { if (mEmergencyGestureEnabled && mEmergencyGesturePowerButtonCooldownPeriodMs >= 0 && event.getEventTime() - mLastEmergencyGestureTriggered < mEmergencyGesturePowerButtonCooldownPeriodMs) { @@ -546,7 +589,7 @@ public class GestureLauncherService extends SystemService { mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; mPowerButtonSlowConsecutiveTaps++; - } else { + } else if (!overridePowerKeyBehaviorInFocusedWindow() || 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 f1a481155458..3fb371cd5d03 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -88,10 +88,12 @@ 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.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; +import static com.android.server.GestureLauncherService.CAMERA_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; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_UNCOVERED; @@ -431,6 +433,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { private static final String ACTION_VOICE_ASSIST_RETAIL = "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 @@ -492,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. @@ -1097,6 +1135,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { mPowerKeyHandled = mPowerKeyHandled || hungUp || handledByPowerManager || isKeyGestureTriggered || mKeyCombinationManager.isPowerKeyIntercepted(); + + if (overridePowerKeyBehaviorInFocusedWindow()) { + mPowerKeyHandled |= mDoubleTapPowerDetected; + } + if (!mPowerKeyHandled) { if (!interactive) { wakeUpFromWakeKey(event); @@ -2669,7 +2712,19 @@ 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 @@ -2700,7 +2755,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 @@ -3477,6 +3542,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + if (overridePowerKeyBehaviorInFocusedWindow() && event.getKeyCode() == KEYCODE_POWER + && event.getAction() == KeyEvent.ACTION_UP + && mDoubleTapPowerDetected) { + mDoubleTapPowerDetected = false; + } + return needToConsumeKey ? keyConsumed : keyNotConsumed; } @@ -3992,6 +4063,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*/); @@ -4047,6 +4120,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)) { @@ -4057,6 +4132,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 > CAMERA_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 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, String.format("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, String.format("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()) { @@ -4633,6 +4792,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } + if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) { + handleUnhandledSystemKey(event); + return true; + } + if (useKeyGestureEventHandler()) { return false; } @@ -5467,8 +5631,13 @@ 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); @@ -5730,6 +5899,32 @@ public class PhoneWindowManager implements WindowManagerPolicy { } if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) { + if (overridePowerKeyBehaviorInFocusedWindow()) { + 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. @@ -5741,6 +5936,27 @@ public class PhoneWindowManager implements WindowManagerPolicy { mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn); } + private boolean detectDoubleTapPower(KeyEvent event) { + if (event.getKeyCode() != KEYCODE_POWER || event.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + if (event.isLongPress()) { + 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 == CAMERA_POWER_TAP_COUNT_THRESHOLD; + } + // The camera gesture will be detected by GestureLauncherService. private boolean handleCameraGesture(KeyEvent event, boolean interactive) { // camera gesture. @@ -7597,6 +7813,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 cebe790bb1b9..447d443282bd 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 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 8024915692aa..2349def93b0c 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -1408,6 +1408,47 @@ public class GestureLauncherServiceTest { assertEquals(1, tapCounts.get(1).intValue()); } + /** + * 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_cameraDoesNotLaunch_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 = CAMERA_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. * @@ -1510,4 +1551,32 @@ public class GestureLauncherServiceTest { userSetupCompleteValue, UserHandle.USER_CURRENT); } + + + private void enableEmergencyGesture() { + withEmergencyGestureEnabledConfigValue(true); + withEmergencyGestureEnabledSettingValue(true); + mGestureLauncherService.updateEmergencyGestureEnabled(); + withUserSetupCompleteValue(true); + } + + private void enableCameraGesture() { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(0); + mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + withUserSetupCompleteValue(true); + } + + private void sendPowerKeyDownToGestureLauncherServiceAndAssertValues( + long eventTime, boolean expectedIntercept, boolean expectedOutLaunchedValue) { + KeyEvent keyEvent = + new KeyEvent( + IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); + boolean interactive = true; + MutableBoolean outLaunched = new MutableBoolean(true); + boolean intercepted = + mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, outLaunched); + assertEquals(intercepted, expectedIntercept); + assertEquals(outLaunched.value, expectedOutLaunchedValue); + } } 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..be516e9553c0 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java @@ -18,15 +18,22 @@ 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.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.view.Display; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; /** @@ -39,8 +46,12 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { @Before public void setUp() { setUpPhoneWindowManager(); + mPhoneWindowManager.overrideStatusBarManagerInternal(); } + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + /** * Power single press to turn screen on/off. */ @@ -50,6 +61,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 +103,7 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { mPhoneWindowManager.overrideCanStartDreaming(false); sendKey(KEYCODE_POWER); sendKey(KEYCODE_POWER); - mPhoneWindowManager.assertCameraLaunch(); + mPhoneWindowManager.assertDoublePowerLaunch(); mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); } @@ -101,7 +114,7 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { public void testPowerDoublePress() { sendKey(KEYCODE_POWER); sendKey(KEYCODE_POWER); - mPhoneWindowManager.assertCameraLaunch(); + mPhoneWindowManager.assertDoublePowerLaunch(); } /** @@ -114,6 +127,8 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER, true); mPhoneWindowManager.assertSearchManagerLaunchAssist(); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + // Show global actions. mPhoneWindowManager.overrideLongPressOnPower(LONG_PRESS_POWER_GLOBAL_ACTIONS); sendKey(KEYCODE_POWER, true); @@ -141,4 +156,139 @@ 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); + + for (int i = 0; i < 5; ++i) { + sendKey(KEYCODE_POWER); + mPhoneWindowManager.moveTimeForward(100); + } + + 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..591950176444 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,11 @@ 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 +120,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 @@ -278,6 +285,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 +300,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 9db76d47fed7..18ecfa19772d 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -22,6 +22,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.STATE_ON; import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE; +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong; @@ -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,8 @@ class TestPhoneWindowManager { private PhoneWindowManager mPhoneWindowManager; private Context mContext; + private GestureLauncherService mGestureLauncherService; + @Mock private WindowManagerInternal mWindowManagerInternal; @Mock private ActivityManagerInternal mActivityManagerInternal; @@ -163,7 +176,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 +207,8 @@ class TestPhoneWindowManager { private int mKeyEventPolicyFlags = FLAG_INTERACTIVE; + private int mProcessPowerKeyDownCount = 0; + private class TestTalkbackShortcutController extends TalkbackShortcutController { TestTalkbackShortcutController(Context context) { super(context); @@ -260,6 +277,8 @@ class TestPhoneWindowManager { MockitoAnnotations.initMocks(this); mHandler = new Handler(mTestLooper.getLooper()); mContext = mockingDetails(context).isSpy() ? context : spy(context); + mGestureLauncherService = spy(new GestureLauncherService(mContext, mMetricsLogger, + mUiEventLogger)); setUp(supportSettingsUpdate); mTestLooper.dispatchAll(); } @@ -272,6 +291,7 @@ class TestPhoneWindowManager { mMockitoSession = mockitoSession() .mockStatic(LocalServices.class, spyStubOnly) .mockStatic(KeyCharacterMap.class) + .mockStatic(GestureLauncherService.class) .strictness(Strictness.LENIENT) .startMocking(); @@ -294,6 +314,16 @@ class TestPhoneWindowManager { () -> LocalServices.getService(eq(PowerManagerInternal.class))); doReturn(mDisplayManagerInternal).when( () -> LocalServices.getService(eq(DisplayManagerInternal.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(mGestureLauncherService).when( () -> LocalServices.getService(eq(GestureLauncherService.class))); doReturn(mUserManagerInternal).when( @@ -375,7 +405,8 @@ 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, + /* inputFeatureFlags = */ 0); doReturn(interceptionInfo) .when(mWindowManagerInternal).getKeyInterceptionInfoFromToken(any()); @@ -393,6 +424,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); } @@ -638,6 +671,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; } @@ -713,13 +752,59 @@ class TestPhoneWindowManager { verify(mPowerManager, never()).goToSleep(anyLong(), anyInt(), anyInt()); } - void assertCameraLaunch() { + void assertDoublePowerLaunch() { + ArgumentCaptor 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 capturedValues = valueCaptor.getAllValues().stream() + .map(mutableBoolean -> mutableBoolean.value) + .toList(); + + assertTrue(capturedValues.contains(true)); } + void assertNoDoublePowerLaunch() { + ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(MutableBoolean.class); + + mTestLooper.dispatchAll(); + verify(mGestureLauncherService, atLeast(0)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + + List capturedValues = valueCaptor.getAllValues().stream() + .map(mutableBoolean -> mutableBoolean.value) + .toList(); + + assertTrue(capturedValues.stream().noneMatch(value -> value)); + } + + void assertEmergencyLaunch() { + ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(MutableBoolean.class); + + mTestLooper.dispatchAll(); + verify(mGestureLauncherService, atLeast(1)) + .interceptPowerKeyDown(any(), anyBoolean(), valueCaptor.capture()); + + if (overridePowerKeyBehaviorInFocusedWindow()) { + assertEquals(mProcessPowerKeyDownCount, 5); + } + + List capturedValues = valueCaptor.getAllValues().stream() + .map(mutableBoolean -> mutableBoolean.value) + .toList(); + + assertTrue(capturedValues.getLast()); + } + + void assertSearchManagerLaunchAssist() { mTestLooper.dispatchAll(); verify(mSearchManager).launchAssist(any()); @@ -929,4 +1014,12 @@ class TestPhoneWindowManager { verify(mInputManagerInternal) .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType)); } + + private void captureProcessPowerKeyDownCount() { + doAnswer((Answer) 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 43844f6514e8..038c6d7754b5 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -541,7 +541,8 @@ class InputManagerServiceTests { 0 }, "title", - /* uid = */0 + /* uid = */0, + /* inputFeatureFlags = */ 0 ) whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info) } -- cgit v1.2.3-59-g8ed1b