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
diff --git a/core/java/com/android/internal/policy/KeyInterceptionInfo.java b/core/java/com/android/internal/policy/KeyInterceptionInfo.java
index b20f6d2..fed8fe3 100644
--- a/core/java/com/android/internal/policy/KeyInterceptionInfo.java
+++ b/core/java/com/android/internal/policy/KeyInterceptionInfo.java
@@ -27,11 +27,13 @@
// 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 ccc44a4..19bc8e3 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 @@
/**
* 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 @@
}
@VisibleForTesting
- GestureLauncherService(Context context, MetricsLogger metricsLogger,
+ public GestureLauncherService(
+ Context context,
+ MetricsLogger metricsLogger,
UiEventLogger uiEventLogger) {
super(context);
mContext = context;
@@ -501,14 +504,54 @@
}
/**
+ * 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
*
* @param interactive true if the event's policy contains {@code FLAG_INTERACTIVE}
* @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 @@
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 f1a4811..3fb371c 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.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;
@@ -432,6 +434,16 @@
"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.)
@@ -492,6 +504,32 @@
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 @@
mPowerKeyHandled = mPowerKeyHandled || hungUp
|| handledByPowerManager || isKeyGestureTriggered
|| mKeyCombinationManager.isPowerKeyIntercepted();
+
+ if (overridePowerKeyBehaviorInFocusedWindow()) {
+ mPowerKeyHandled |= mDoubleTapPowerDetected;
+ }
+
if (!mPowerKeyHandled) {
if (!interactive) {
wakeUpFromWakeKey(event);
@@ -2669,7 +2712,19 @@
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 @@
@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 @@
}
}
+ if (overridePowerKeyBehaviorInFocusedWindow() && event.getKeyCode() == KEYCODE_POWER
+ && event.getAction() == KeyEvent.ACTION_UP
+ && mDoubleTapPowerDetected) {
+ mDoubleTapPowerDetected = false;
+ }
+
return needToConsumeKey ? keyConsumed : keyNotConsumed;
}
@@ -3992,6 +4063,8 @@
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 @@
sendSystemKeyToStatusBarAsync(event);
return true;
}
+ case KeyEvent.KEYCODE_POWER:
+ return interceptPowerKeyBeforeDispatching(focusedToken, event);
}
if (isValidGlobalKey(keyCode)
&& mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
@@ -4057,6 +4132,90 @@
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<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, 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 @@
return true;
}
+ if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) {
+ handleUnhandledSystemKey(event);
+ return true;
+ }
+
if (useKeyGestureEventHandler()) {
return false;
}
@@ -5467,8 +5631,13 @@
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 @@
}
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 @@
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 @@
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 cebe790..447d443 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 @@
|| 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 8024915..2349def 100644
--- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java
@@ -1409,6 +1409,47 @@
}
/**
+ * 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.
*
* @return last event time.
@@ -1510,4 +1551,32 @@
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 05a1482..be516e9 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 @@
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 @@
@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 @@
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 @@
mPhoneWindowManager.overrideCanStartDreaming(false);
sendKey(KEYCODE_POWER);
sendKey(KEYCODE_POWER);
- mPhoneWindowManager.assertCameraLaunch();
+ mPhoneWindowManager.assertDoublePowerLaunch();
mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished();
}
@@ -101,7 +114,7 @@
public void testPowerDoublePress() {
sendKey(KEYCODE_POWER);
sendKey(KEYCODE_POWER);
- mPhoneWindowManager.assertCameraLaunch();
+ mPhoneWindowManager.assertDoublePowerLaunch();
}
/**
@@ -114,6 +127,8 @@
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 @@
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 9e47a00..5919501 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 @@
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 @@
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 @@
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 @@
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 9db76d4..18ecfa1 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.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_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.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 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.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 @@
private PhoneWindowManager mPhoneWindowManager;
private Context mContext;
+ private GestureLauncherService mGestureLauncherService;
+
@Mock private WindowManagerInternal mWindowManagerInternal;
@Mock private ActivityManagerInternal mActivityManagerInternal;
@@ -163,7 +176,9 @@
@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 @@
private int mKeyEventPolicyFlags = FLAG_INTERACTIVE;
+ private int mProcessPowerKeyDownCount = 0;
+
private class TestTalkbackShortcutController extends TalkbackShortcutController {
TestTalkbackShortcutController(Context context) {
super(context);
@@ -260,6 +277,8 @@
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 @@
mMockitoSession = mockitoSession()
.mockStatic(LocalServices.class, spyStubOnly)
.mockStatic(KeyCharacterMap.class)
+ .mockStatic(GestureLauncherService.class)
.strictness(Strictness.LENIENT)
.startMocking();
@@ -294,6 +314,16 @@
() -> 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 @@
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 @@
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 @@
.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 @@
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() {
mTestLooper.dispatchAll();
verify(mSearchManager).launchAssist(any());
@@ -929,4 +1014,12 @@
verify(mInputManagerInternal)
.handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType));
}
+
+ 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 43844f6..038c6d7 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 @@
0
},
"title",
- /* uid = */0
+ /* uid = */0,
+ /* inputFeatureFlags = */ 0
)
whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info)
}