Loading core/java/com/android/internal/policy/KeyInterceptionInfo.java +3 −1 Original line number Diff line number Diff line Loading @@ -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; } } services/core/java/com/android/server/GestureLauncherService.java +42 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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++; Loading services/core/java/com/android/server/policy/PhoneWindowManager.java +227 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -430,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 Loading Loading @@ -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. Loading Loading @@ -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); Loading Loading @@ -2651,7 +2695,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mShouldEarlyShortPressOnPower) { return; } // 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 Loading Loading @@ -2682,8 +2737,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onMultiPress(long downTime, int count, int displayId) { if (overridePowerKeyBehaviorInFocusedWindow()) { mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); mDeferredKeyActionExecutor.queueKeyAction( KEYCODE_POWER, downTime, () -> { powerPress(downTime, count, displayId); }); } else { powerPress(downTime, count, displayId); } } @Override void onKeyUp(long eventTime, int count, int displayId, int deviceId, int metaState) { Loading Loading @@ -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; } Loading Loading @@ -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*/); Loading Loading @@ -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)) { Loading @@ -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()) { Loading Loading @@ -4562,6 +4721,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) { handleUnhandledSystemKey(event); return true; } if (useKeyGestureEventHandler()) { return false; } Loading Loading @@ -5396,8 +5560,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyEvent.actionToString(event.getAction()), mPowerKeyHandled ? 1 : 0, mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER)); 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); Loading Loading @@ -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. Loading @@ -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. Loading Loading @@ -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) { Loading services/core/java/com/android/server/wm/WindowState.java +3 −2 Original line number Diff line number Diff line Loading @@ -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; } Loading services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +40 −0 Original line number Diff line number Diff line Loading @@ -1696,6 +1696,46 @@ 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_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. * Loading Loading
core/java/com/android/internal/policy/KeyInterceptionInfo.java +3 −1 Original line number Diff line number Diff line Loading @@ -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; } }
services/core/java/com/android/server/GestureLauncherService.java +42 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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++; Loading
services/core/java/com/android/server/policy/PhoneWindowManager.java +227 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -430,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 Loading Loading @@ -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. Loading Loading @@ -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); Loading Loading @@ -2651,7 +2695,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mShouldEarlyShortPressOnPower) { return; } // 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 Loading Loading @@ -2682,8 +2737,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onMultiPress(long downTime, int count, int displayId) { if (overridePowerKeyBehaviorInFocusedWindow()) { mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); mDeferredKeyActionExecutor.queueKeyAction( KEYCODE_POWER, downTime, () -> { powerPress(downTime, count, displayId); }); } else { powerPress(downTime, count, displayId); } } @Override void onKeyUp(long eventTime, int count, int displayId, int deviceId, int metaState) { Loading Loading @@ -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; } Loading Loading @@ -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*/); Loading Loading @@ -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)) { Loading @@ -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()) { Loading Loading @@ -4562,6 +4721,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) { handleUnhandledSystemKey(event); return true; } if (useKeyGestureEventHandler()) { return false; } Loading Loading @@ -5396,8 +5560,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyEvent.actionToString(event.getAction()), mPowerKeyHandled ? 1 : 0, mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER)); 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); Loading Loading @@ -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. Loading @@ -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. Loading Loading @@ -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) { Loading
services/core/java/com/android/server/wm/WindowState.java +3 −2 Original line number Diff line number Diff line Loading @@ -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; } Loading
services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +40 −0 Original line number Diff line number Diff line Loading @@ -1696,6 +1696,46 @@ 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_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. * Loading