Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 22bbefcc authored by Charles Wang's avatar Charles Wang
Browse files

Allow focused window to capture power button - try 3.

This is a combination of ag/30154729 + ag/30763741, which was originally
reverted due to b/382133936 (SysUI PowerMenuTest failing).

The tests are passing in combination with ag/30850517. ABTD results:
https://android-build.corp.google.com/abtd/run/L66200030008426443/
https://android-build.corp.google.com/abtd/run/L94300030008415110/

With this feature enabled, focused windows with the
OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission and the window
flag INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS set will receive KEYCODE_POWER KeyEvents.

The window can override the double-tap gesture default behavior (launching
camera) by handling the second onKeyDown event of a double tap. If the
app does not handle this event, or if the windw does not have the
permissions to receive KEYCODE_POWER, the default behavior of launching
camera is performed.

A double tap is defined as two consecutive KEYCODE_POWER key down events
within a 300 ms threshold.

Single, long-press and non double-press gestures behaviors will all be
default behavior, regardless of app handling the events or not.

Turning display on and off is unaffected, with the following exception:
In a privileged, focused window that doesn't handle the KEYCODE_POWER
event, there will be a 300 ms delay in turning off the screen.

More details at go/power-button-dd.

Bug: 357144512
Test: atest WmTests:PowerKeyGestureTests
Test: manual testing (single, long-press, double-press, 5x press of
power button on non-privileged windows and privileged windows that
handle and don't handle the KEYCODE_POWER KeyEvent.)
Flag: com.android.hardware.input.override_power_key_behavior_in_focused_window

Change-Id: Ieb09581ccae1b0b4d536e8f8105c7d24f5e8a40d
parent 399ee78c
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -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;
    }
}
+42 −2
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server;

import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap;

import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow;
import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis;

import android.app.ActivityManager;
@@ -232,7 +233,7 @@ public class GestureLauncherService extends SystemService {
    }

    @VisibleForTesting
    GestureLauncherService(Context context, MetricsLogger metricsLogger,
    public GestureLauncherService(Context context, MetricsLogger metricsLogger,
            QuickAccessWalletClient quickAccessWalletClient, UiEventLogger uiEventLogger) {
        super(context);
        mContext = context;
@@ -600,6 +601,45 @@ public class GestureLauncherService extends SystemService {
        return res;
    }

    /**
     * Processes a power key event in GestureLauncherService without performing an action. This
     * method is called on every KEYCODE_POWER ACTION_DOWN event and ensures that, even if
     * KEYCODE_POWER events are passed to and handled by the app, the GestureLauncherService still
     * keeps track of all running KEYCODE_POWER events for its gesture detection and relevant
     * actions.
     */
    public void processPowerKeyDown(KeyEvent event) {
        if (mEmergencyGestureEnabled && mEmergencyGesturePowerButtonCooldownPeriodMs >= 0
                && event.getEventTime() - mLastEmergencyGestureTriggered
                < mEmergencyGesturePowerButtonCooldownPeriodMs) {
            return;
        }
        if (event.isLongPress()) {
            return;
        }

        final long powerTapInterval;

        synchronized (this) {
            powerTapInterval = event.getEventTime() - mLastPowerDown;
            mLastPowerDown = event.getEventTime();
            if (powerTapInterval >= POWER_SHORT_TAP_SEQUENCE_MAX_INTERVAL_MS) {
                // Tap too slow, reset consecutive tap counts.
                mFirstPowerDown = event.getEventTime();
                mPowerButtonConsecutiveTaps = 1;
                mPowerButtonSlowConsecutiveTaps = 1;
            } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) {
                // Tap too slow for shortcuts
                mFirstPowerDown = event.getEventTime();
                mPowerButtonConsecutiveTaps = 1;
                mPowerButtonSlowConsecutiveTaps++;
            } else if (!overridePowerKeyBehaviorInFocusedWindow() || powerTapInterval > 0) {
                // Fast consecutive tap
                mPowerButtonConsecutiveTaps++;
                mPowerButtonSlowConsecutiveTaps++;
            }
        }
    }

    /**
     * Attempts to intercept power key down event by detecting certain gesture patterns
@@ -648,7 +688,7 @@ public class GestureLauncherService extends SystemService {
                mFirstPowerDown  = event.getEventTime();
                mPowerButtonConsecutiveTaps = 1;
                mPowerButtonSlowConsecutiveTaps++;
            } else {
            } else if (powerTapInterval > 0) {
                // Fast consecutive tap
                mPowerButtonConsecutiveTaps++;
                mPowerButtonSlowConsecutiveTaps++;
+227 −4
Original line number Diff line number Diff line
@@ -88,7 +88,9 @@ import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGest
import static com.android.hardware.input.Flags.inputManagerLifecycleSupport;
import static com.android.hardware.input.Flags.keyboardA11yShortcutControl;
import static com.android.hardware.input.Flags.modifierShortcutDump;
import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
import static com.android.server.GestureLauncherService.DOUBLE_POWER_TAP_COUNT_THRESHOLD;
import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser;
import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
@@ -191,6 +193,7 @@ import android.service.dreams.IDreamManager;
import android.service.vr.IPersistentVrStateCallbacks;
import android.speech.RecognizerIntent;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.MathUtils;
import android.util.MutableBoolean;
@@ -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
@@ -491,6 +504,32 @@ public class PhoneWindowManager implements WindowManagerPolicy {

    private WindowWakeUpPolicy mWindowWakeUpPolicy;

    /**
     * The three variables below are used for custom power key gesture detection in
     * PhoneWindowManager. They are used to detect when the power button has been double pressed
     * and, when it does happen, makes the behavior overrideable by the app.
     *
     * We cannot use the {@link PowerKeyRule} for this because multi-press power gesture detection
     * and behaviors are handled by {@link com.android.server.GestureLauncherService}, and the
     * {@link PowerKeyRule} only handles single and long-presses of the power button. As a result,
     * overriding the double tap behavior requires custom gesture detection here that mimics the
     * logic in {@link com.android.server.GestureLauncherService}.
     *
     * Long-term, it would be beneficial to move all power gesture detection to
     * {@link PowerKeyRule} so that this custom logic isn't required.
     */
    // Time of last power down event.
    private long mLastPowerDown;

    // Number of power button events consecutively triggered (within a specific timeout threshold).
    private int mPowerButtonConsecutiveTaps = 0;

    // Whether a double tap of the power button has been detected.
    volatile boolean mDoubleTapPowerDetected;

    // Runnable that is queued on a delay when the first power keyDown event is sent to the app.
    private Runnable mPowerKeyDelayedRunnable = null;

    boolean mSafeMode;

    // Whether to allow dock apps with METADATA_DOCK_HOME to temporarily take over the Home key.
@@ -1079,6 +1118,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        mPowerKeyHandled = mPowerKeyHandled || hungUp
                || handledByPowerManager || isKeyGestureTriggered
                || mKeyCombinationManager.isPowerKeyIntercepted();

        if (overridePowerKeyBehaviorInFocusedWindow()) {
            mPowerKeyHandled |= mDoubleTapPowerDetected;
        }

        if (!mPowerKeyHandled) {
            if (!interactive) {
                wakeUpFromWakeKey(event);
@@ -2651,7 +2695,18 @@ public class PhoneWindowManager implements WindowManagerPolicy {
            if (mShouldEarlyShortPressOnPower) {
                return;
            }
            // TODO(b/380433365): Remove deferring single power press action when refactoring.
            if (overridePowerKeyBehaviorInFocusedWindow()) {
                mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER);
                mDeferredKeyActionExecutor.queueKeyAction(
                        KEYCODE_POWER,
                        downTime,
                        () -> {
                            powerPress(downTime, 1 /*count*/, displayId);
                        });
            } else {
                powerPress(downTime, 1 /*count*/, displayId);
            }
        }

        @Override
@@ -2682,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) {
@@ -3459,6 +3524,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
            }
        }

        if (overridePowerKeyBehaviorInFocusedWindow() && event.getKeyCode() == KEYCODE_POWER
                && event.getAction() == KeyEvent.ACTION_UP
                && mDoubleTapPowerDetected) {
            mDoubleTapPowerDetected = false;
        }

        return needToConsumeKey ? keyConsumed : keyNotConsumed;
    }

@@ -3974,6 +4045,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
                    sendSystemKeyToStatusBarAsync(event);
                    return true;
                }
            case KeyEvent.KEYCODE_POWER:
                return interceptPowerKeyBeforeDispatching(focusedToken, event);
            case KeyEvent.KEYCODE_SCREENSHOT:
                if (firstDown) {
                    interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
@@ -4029,6 +4102,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
                    sendSystemKeyToStatusBarAsync(event);
                    return true;
                }
            case KeyEvent.KEYCODE_POWER:
                return interceptPowerKeyBeforeDispatching(focusedToken, event);
        }
        if (isValidGlobalKey(keyCode)
                && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
@@ -4039,6 +4114,90 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        return (metaState & KeyEvent.META_META_ON) != 0;
    }

    /**
     * Called by interceptKeyBeforeDispatching to handle interception logic for KEYCODE_POWER
     * KeyEvents.
     *
     * @return true if intercepting the key, false if sending to app.
     */
    private boolean interceptPowerKeyBeforeDispatching(IBinder focusedToken, KeyEvent event) {
        if (!overridePowerKeyBehaviorInFocusedWindow()) {
            //Flag disabled: intercept the power key and do not send to app.
            return true;
        }
        if (event.getKeyCode() != KEYCODE_POWER) {
            Log.wtf(TAG, "interceptPowerKeyBeforeDispatching received a non-power KeyEvent "
                    + "with key code: " + event.getKeyCode());
            return false;
        }

        // Intercept keys (don't send to app) for 3x, 4x, 5x gestures)
        if (mPowerButtonConsecutiveTaps > DOUBLE_POWER_TAP_COUNT_THRESHOLD) {
            setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime());
            return true;
        }

        // UP key; just reuse the original decision.
        if (event.getAction() == KeyEvent.ACTION_UP) {
            final Set<Integer> consumedKeys = mConsumedKeysForDevice.get(event.getDeviceId());
            return consumedKeys != null
                    && consumedKeys.contains(event.getKeyCode());
        }

        KeyInterceptionInfo info =
                mWindowManagerInternal.getKeyInterceptionInfoFromToken(focusedToken);

        if (info == null || !mButtonOverridePermissionChecker.canWindowOverridePowerKey(mContext,
                info.windowOwnerUid, info.inputFeaturesFlags)) {
            // The focused window does not have the permission to override power key behavior.
            if (DEBUG_INPUT) {
                String interceptReason = "";
                if (info == null) {
                    interceptReason = "Window is null";
                } else if (!mButtonOverridePermissionChecker.canAppOverrideSystemKey(mContext,
                        info.windowOwnerUid)) {
                    interceptReason = "Application does not have "
                            + "OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW permission";
                } else {
                    interceptReason = "Window does not have inputFeatureFlag set";
                }

                Log.d(TAG, TextUtils.formatSimple("Intercepting KEYCODE_POWER event. action=%d, "
                                + "eventTime=%d to window=%s. interceptReason=%s. "
                                + "mDoubleTapPowerDetected=%b",
                        event.getAction(), event.getEventTime(), (info != null)
                                ? info.windowTitle : "null", interceptReason,
                        mDoubleTapPowerDetected));
            }
            // Intercept the key (i.e. do not send to app)
            setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, event.getDownTime());
            return true;
        }

        if (DEBUG_INPUT) {
            Log.d(TAG, TextUtils.formatSimple("Sending KEYCODE_POWER to app. action=%d, "
                            + "eventTime=%d to window=%s. mDoubleTapPowerDetected=%b",
                    event.getAction(), event.getEventTime(), info.windowTitle,
                    mDoubleTapPowerDetected));
        }

        if (!mDoubleTapPowerDetected) {
            //Single press: post a delayed runnable for the single press power action that will be
            // called if it's not cancelled by a double press.
            final var downTime = event.getDownTime();
            mPowerKeyDelayedRunnable = () ->
                    setDeferredKeyActionsExecutableAsync(KEYCODE_POWER, downTime);
            mHandler.postDelayed(mPowerKeyDelayedRunnable, POWER_MULTI_PRESS_TIMEOUT_MILLIS);
        } else if (mPowerKeyDelayedRunnable != null) {
            //Double press detected: cancel the single press runnable.
            mHandler.removeCallbacks(mPowerKeyDelayedRunnable);
            mPowerKeyDelayedRunnable = null;
        }

        // Focused window has permission. Send to app.
        return false;
    }

    @SuppressLint("MissingPermission")
    private void initKeyGestures() {
        if (!useKeyGestureEventHandler()) {
@@ -4562,6 +4721,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
            return true;
        }

        if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) {
            handleUnhandledSystemKey(event);
            return true;
        }

        if (useKeyGestureEventHandler()) {
            return false;
        }
@@ -5396,8 +5560,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
                        KeyEvent.actionToString(event.getAction()),
                        mPowerKeyHandled ? 1 : 0,
                        mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER));
                if (overridePowerKeyBehaviorInFocusedWindow()) {
                    result |= ACTION_PASS_TO_USER;
                } else {
                    // Any activity on the power button stops the accessibility shortcut
                    result &= ~ACTION_PASS_TO_USER;
                }
                isWakeKey = false; // wake-up will be handled separately
                if (down) {
                    interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered);
@@ -5659,6 +5827,35 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        }

        if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) {
            if (overridePowerKeyBehaviorInFocusedWindow()) {
                if (event.getRepeatCount() > 0 && !mHasFeatureWatch) {
                    return;
                }
                if (mGestureLauncherService != null) {
                    mGestureLauncherService.processPowerKeyDown(event);
                }

                if (detectDoubleTapPower(event)) {
                    mDoubleTapPowerDetected = true;

                    // Copy of the event for handler in case the original event gets recycled.
                    KeyEvent eventCopy = KeyEvent.obtain(event);
                    mDeferredKeyActionExecutor.queueKeyAction(
                            KeyEvent.KEYCODE_POWER,
                            eventCopy.getEventTime(),
                            () -> {
                                if (!handleCameraGesture(eventCopy, interactive)) {
                                    mSingleKeyGestureDetector.interceptKey(
                                            eventCopy, interactive, defaultDisplayOn);
                                } else {
                                    mSingleKeyGestureDetector.reset();
                                }
                                eventCopy.recycle();
                            });
                    return;
                }
            }

            mPowerKeyHandled = handleCameraGesture(event, interactive);
            if (mPowerKeyHandled) {
                // handled by camera gesture.
@@ -5670,6 +5867,26 @@ public class PhoneWindowManager implements WindowManagerPolicy {
        mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn);
    }

    private boolean detectDoubleTapPower(KeyEvent event) {
        //Watches use the SingleKeyGestureDetector for detecting multi-press gestures.
        if (mHasFeatureWatch || event.getKeyCode() != KEYCODE_POWER
                || event.getAction() != KeyEvent.ACTION_DOWN  || event.getRepeatCount() != 0) {
            return false;
        }

        final long powerTapInterval = event.getEventTime() - mLastPowerDown;
        mLastPowerDown = event.getEventTime();
        if (powerTapInterval >= POWER_MULTI_PRESS_TIMEOUT_MILLIS) {
            // Tap too slow for double press
            mPowerButtonConsecutiveTaps = 1;
        } else {
            mPowerButtonConsecutiveTaps++;
        }

        return powerTapInterval < POWER_MULTI_PRESS_TIMEOUT_MILLIS
                && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD;
    }

    // The camera gesture will be detected by GestureLauncherService.
    private boolean handleCameraGesture(KeyEvent event, boolean interactive) {
        // camera gesture.
@@ -7526,6 +7743,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
                    null)
                    == PERMISSION_GRANTED;
        }

        boolean canWindowOverridePowerKey(Context context, int uid, int inputFeaturesFlags) {
            return canAppOverrideSystemKey(context, uid)
                    && (inputFeaturesFlags & WindowManager.LayoutParams
                    .INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0;
        }
    }

    private int getTargetDisplayIdForKeyEvent(KeyEvent event) {
+3 −2
Original line number Diff line number Diff line
@@ -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;
    }
+40 −0
Original line number Diff line number Diff line
@@ -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