diff options
9 files changed, 1259 insertions, 135 deletions
diff --git a/core/java/com/android/internal/policy/KeyInterceptionInfo.java b/core/java/com/android/internal/policy/KeyInterceptionInfo.java index b20f6d225b69..fed8fe3b4cc0 100644 --- a/core/java/com/android/internal/policy/KeyInterceptionInfo.java +++ b/core/java/com/android/internal/policy/KeyInterceptionInfo.java @@ -27,11 +27,13 @@ public class KeyInterceptionInfo { // Debug friendly name to help identify the window public final String windowTitle; public final int windowOwnerUid; + public final int inputFeaturesFlags; - public KeyInterceptionInfo(int type, int flags, String title, int uid) { + public KeyInterceptionInfo(int type, int flags, String title, int uid, int inputFeaturesFlags) { layoutParamsType = type; layoutParamsPrivateFlags = flags; windowTitle = title; windowOwnerUid = uid; + this.inputFeaturesFlags = inputFeaturesFlags; } } diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index ccc44a41759b..d84a892e4f54 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -16,9 +16,14 @@ 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; +import android.app.ActivityOptions; +import android.app.PendingIntent; import android.app.StatusBarManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -33,6 +38,7 @@ import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.hardware.TriggerEvent; import android.hardware.TriggerEventListener; +import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; import android.os.PowerManager.WakeLock; @@ -41,10 +47,12 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; +import android.service.quickaccesswallet.QuickAccessWalletClient; import android.util.MutableBoolean; import android.util.Slog; import android.view.KeyEvent; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; @@ -70,7 +78,8 @@ public class GestureLauncherService extends SystemService { * Time in milliseconds in which the power button must be pressed twice so it will be considered * as a camera launch. */ - @VisibleForTesting static final long CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS = 300; + @VisibleForTesting + static final long POWER_DOUBLE_TAP_MAX_TIME_MS = 300; /** @@ -100,10 +109,23 @@ public class GestureLauncherService extends SystemService { @VisibleForTesting static final int EMERGENCY_GESTURE_POWER_BUTTON_COOLDOWN_PERIOD_MS_MAX = 5000; - /** - * Number of taps required to launch camera shortcut. - */ - private static final int CAMERA_POWER_TAP_COUNT_THRESHOLD = 2; + /** Indicates camera should be launched on power double tap. */ + @VisibleForTesting static final int LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER = 0; + + /** Indicates wallet should be launched on power double tap. */ + @VisibleForTesting static final int LAUNCH_WALLET_ON_DOUBLE_TAP_POWER = 1; + + /** Number of taps required to launch the double tap shortcut (either camera or wallet). */ + public static final int DOUBLE_POWER_TAP_COUNT_THRESHOLD = 2; + + /** Bundle to send with PendingIntent to grant background activity start privileges. */ + private static final Bundle GRANT_BACKGROUND_START_PRIVILEGES = + ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) + .toBundle(); + + private final QuickAccessWalletClient mQuickAccessWalletClient; /** The listener that receives the gesture event. */ private final GestureEventListener mGestureListener = new GestureEventListener(); @@ -158,6 +180,9 @@ public class GestureLauncherService extends SystemService { */ private boolean mCameraDoubleTapPowerEnabled; + /** Whether wallet double tap power button gesture is currently enabled. */ + private boolean mWalletDoubleTapPowerEnabled; + /** * Whether emergency gesture is currently enabled */ @@ -204,14 +229,22 @@ public class GestureLauncherService extends SystemService { } } public GestureLauncherService(Context context) { - this(context, new MetricsLogger(), new UiEventLoggerImpl()); + this( + context, + new MetricsLogger(), + QuickAccessWalletClient.create(context), + new UiEventLoggerImpl()); } @VisibleForTesting - GestureLauncherService(Context context, MetricsLogger metricsLogger, + public GestureLauncherService( + Context context, + MetricsLogger metricsLogger, + QuickAccessWalletClient quickAccessWalletClient, UiEventLogger uiEventLogger) { super(context); mContext = context; + mQuickAccessWalletClient = quickAccessWalletClient; mMetricsLogger = metricsLogger; mUiEventLogger = uiEventLogger; } @@ -237,6 +270,9 @@ public class GestureLauncherService extends SystemService { "GestureLauncherService"); updateCameraRegistered(); updateCameraDoubleTapPowerEnabled(); + if (launchWalletOptionOnPowerDoubleTap()) { + updateWalletDoubleTapPowerEnabled(); + } updateEmergencyGestureEnabled(); updateEmergencyGesturePowerButtonCooldownPeriodMs(); @@ -292,6 +328,14 @@ public class GestureLauncherService extends SystemService { } @VisibleForTesting + void updateWalletDoubleTapPowerEnabled() { + boolean enabled = isWalletDoubleTapPowerSettingEnabled(mContext, mUserId); + synchronized (this) { + mWalletDoubleTapPowerEnabled = enabled; + } + } + + @VisibleForTesting void updateEmergencyGestureEnabled() { boolean enabled = isEmergencyGestureSettingEnabled(mContext, mUserId); synchronized (this) { @@ -418,10 +462,33 @@ public class GestureLauncherService extends SystemService { Settings.Secure.CAMERA_GESTURE_DISABLED, 0, userId) == 0); } + /** Checks if camera should be launched on double press of the power button. */ public static boolean isCameraDoubleTapPowerSettingEnabled(Context context, int userId) { - return isCameraDoubleTapPowerEnabled(context.getResources()) - && (Settings.Secure.getIntForUser(context.getContentResolver(), - Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0); + boolean res; + + if (launchWalletOptionOnPowerDoubleTap()) { + res = isDoubleTapPowerGestureSettingEnabled(context, userId) + && getDoubleTapPowerGestureAction(context, userId) + == LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER; + } else { + // These are legacy settings that will be deprecated once the option to launch both + // wallet and camera has been created. + res = isCameraDoubleTapPowerEnabled(context.getResources()) + && (Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0, userId) == 0); + } + return res; + } + + /** Checks if wallet should be launched on double tap of the power button. */ + public static boolean isWalletDoubleTapPowerSettingEnabled(Context context, int userId) { + if (!launchWalletOptionOnPowerDoubleTap()) { + return false; + } + + return isDoubleTapPowerGestureSettingEnabled(context, userId) + && getDoubleTapPowerGestureAction(context, userId) + == LAUNCH_WALLET_ON_DOUBLE_TAP_POWER; } public static boolean isCameraLiftTriggerSettingEnabled(Context context, int userId) { @@ -441,6 +508,28 @@ public class GestureLauncherService extends SystemService { isDefaultEmergencyGestureEnabled(context.getResources()) ? 1 : 0, userId) != 0; } + private static int getDoubleTapPowerGestureAction(Context context, int userId) { + return Settings.Secure.getIntForUser( + context.getContentResolver(), + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE, + LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER, + userId); + } + + /** Whether the shortcut to launch app on power double press is enabled. */ + private static boolean isDoubleTapPowerGestureSettingEnabled(Context context, int userId) { + return Settings.Secure.getIntForUser( + context.getContentResolver(), + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED, + isDoubleTapConfigEnabled(context.getResources()) ? 1 : 0, + userId) + == 1; + } + + private static boolean isDoubleTapConfigEnabled(Resources resources) { + return resources.getBoolean(R.bool.config_doubleTapPowerGestureEnabled); + } + /** * Gets power button cooldown period in milliseconds after emergency gesture is triggered. The * value is capped at a maximum @@ -494,10 +583,56 @@ public class GestureLauncherService extends SystemService { * Whether GestureLauncherService should be enabled according to system properties. */ public static boolean isGestureLauncherEnabled(Resources resources) { - return isCameraLaunchEnabled(resources) - || isCameraDoubleTapPowerEnabled(resources) - || isCameraLiftTriggerEnabled(resources) - || isEmergencyGestureEnabled(resources); + boolean res = + isCameraLaunchEnabled(resources) + || isCameraLiftTriggerEnabled(resources) + || isEmergencyGestureEnabled(resources); + if (launchWalletOptionOnPowerDoubleTap()) { + res |= isDoubleTapConfigEnabled(resources); + } else { + res |= isCameraDoubleTapPowerEnabled(resources); + } + 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 (powerTapInterval > 0) { + // Fast consecutive tap + mPowerButtonConsecutiveTaps++; + mPowerButtonSlowConsecutiveTaps++; + } + } } /** @@ -507,8 +642,8 @@ public class GestureLauncherService extends SystemService { * @param outLaunched true if some action is taken as part of the key intercept (eg, app launch) * @return true if the key down event is intercepted */ - public boolean interceptPowerKeyDown(KeyEvent event, boolean interactive, - MutableBoolean outLaunched) { + public boolean interceptPowerKeyDown( + KeyEvent event, boolean interactive, MutableBoolean outLaunched) { if (mEmergencyGestureEnabled && mEmergencyGesturePowerButtonCooldownPeriodMs >= 0 && event.getEventTime() - mLastEmergencyGestureTriggered < mEmergencyGesturePowerButtonCooldownPeriodMs) { @@ -530,6 +665,7 @@ public class GestureLauncherService extends SystemService { return false; } boolean launchCamera = false; + boolean launchWallet = false; boolean launchEmergencyGesture = false; boolean intercept = false; long powerTapInterval; @@ -541,12 +677,12 @@ public class GestureLauncherService extends SystemService { mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; mPowerButtonSlowConsecutiveTaps = 1; - } else if (powerTapInterval >= CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS) { + } else if (powerTapInterval >= POWER_DOUBLE_TAP_MAX_TIME_MS) { // Tap too slow for shortcuts mFirstPowerDown = event.getEventTime(); mPowerButtonConsecutiveTaps = 1; mPowerButtonSlowConsecutiveTaps++; - } else { + } else if (!overridePowerKeyBehaviorInFocusedWindow() || powerTapInterval > 0) { // Fast consecutive tap mPowerButtonConsecutiveTaps++; mPowerButtonSlowConsecutiveTaps++; @@ -586,10 +722,16 @@ public class GestureLauncherService extends SystemService { } } if (mCameraDoubleTapPowerEnabled - && powerTapInterval < CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - && mPowerButtonConsecutiveTaps == CAMERA_POWER_TAP_COUNT_THRESHOLD) { + && powerTapInterval < POWER_DOUBLE_TAP_MAX_TIME_MS + && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD) { launchCamera = true; intercept = interactive; + } else if (launchWalletOptionOnPowerDoubleTap() + && mWalletDoubleTapPowerEnabled + && powerTapInterval < POWER_DOUBLE_TAP_MAX_TIME_MS + && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD) { + launchWallet = true; + intercept = interactive; } } if (mPowerButtonConsecutiveTaps > 1 || mPowerButtonSlowConsecutiveTaps > 1) { @@ -608,6 +750,10 @@ public class GestureLauncherService extends SystemService { (int) powerTapInterval); mUiEventLogger.log(GestureLauncherEvent.GESTURE_CAMERA_DOUBLE_TAP_POWER); } + } else if (launchWallet) { + Slog.i(TAG, "Power button double tap gesture detected, launching wallet. Interval=" + + powerTapInterval + "ms"); + launchWallet = sendGestureTargetActivityPendingIntent(); } else if (launchEmergencyGesture) { Slog.i(TAG, "Emergency gesture detected, launching."); launchEmergencyGesture = handleEmergencyGesture(); @@ -623,11 +769,74 @@ public class GestureLauncherService extends SystemService { mPowerButtonSlowConsecutiveTaps); mMetricsLogger.histogram("power_double_tap_interval", (int) powerTapInterval); - outLaunched.value = launchCamera || launchEmergencyGesture; + outLaunched.value = launchCamera || launchEmergencyGesture || launchWallet; // Intercept power key event if the press is part of a gesture (camera, eGesture) and the // user has completed setup. return intercept && isUserSetupComplete(); } + + /** + * Fetches and sends gestureTargetActivityPendingIntent from QuickAccessWallet, which is a + * specific activity that QuickAccessWalletService has defined to be launch on detection of the + * power button gesture. + */ + private boolean sendGestureTargetActivityPendingIntent() { + boolean userSetupComplete = isUserSetupComplete(); + if (mQuickAccessWalletClient == null + || !mQuickAccessWalletClient.isWalletServiceAvailable()) { + Slog.w(TAG, "QuickAccessWalletService is not available, ignoring wallet gesture."); + return false; + } + + if (!userSetupComplete) { + if (DBG) { + Slog.d(TAG, "userSetupComplete = false, ignoring wallet gesture."); + } + return false; + } + if (DBG) { + Slog.d(TAG, "userSetupComplete = true, performing wallet gesture."); + } + + mQuickAccessWalletClient.getGestureTargetActivityPendingIntent( + getContext().getMainExecutor(), + gesturePendingIntent -> { + if (gesturePendingIntent == null) { + Slog.d(TAG, "getGestureTargetActivityPendingIntent is null."); + sendFallbackPendingIntent(); + return; + } + sendPendingIntentWithBackgroundStartPrivileges(gesturePendingIntent); + }); + return true; + } + + /** + * If gestureTargetActivityPendingIntent is null, this method is invoked to start the activity + * that QuickAccessWalletService has defined to host the Wallet view, which is typically the + * home screen of the Wallet application. + */ + private void sendFallbackPendingIntent() { + mQuickAccessWalletClient.getWalletPendingIntent( + getContext().getMainExecutor(), + walletPendingIntent -> { + if (walletPendingIntent == null) { + Slog.w(TAG, "getWalletPendingIntent returns null. Not launching " + + "anything for wallet."); + return; + } + sendPendingIntentWithBackgroundStartPrivileges(walletPendingIntent); + }); + } + + private void sendPendingIntentWithBackgroundStartPrivileges(PendingIntent pendingIntent) { + try { + pendingIntent.send(GRANT_BACKGROUND_START_PRIVILEGES); + } catch (PendingIntent.CanceledException e) { + Slog.e(TAG, "PendingIntent was canceled", e); + } + } + /** * @return true if camera was launched, false otherwise. */ @@ -700,31 +909,39 @@ public class GestureLauncherService extends SystemService { Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0; } - private final BroadcastReceiver mUserReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) { - mUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); - mContext.getContentResolver().unregisterContentObserver(mSettingObserver); - registerContentObservers(); - updateCameraRegistered(); - updateCameraDoubleTapPowerEnabled(); - updateEmergencyGestureEnabled(); - updateEmergencyGesturePowerButtonCooldownPeriodMs(); - } - } - }; - - private final ContentObserver mSettingObserver = new ContentObserver(new Handler()) { - public void onChange(boolean selfChange, android.net.Uri uri, int userId) { - if (userId == mUserId) { - updateCameraRegistered(); - updateCameraDoubleTapPowerEnabled(); - updateEmergencyGestureEnabled(); - updateEmergencyGesturePowerButtonCooldownPeriodMs(); - } - } - }; + private final BroadcastReceiver mUserReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_USER_SWITCHED.equals(intent.getAction())) { + mUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); + mContext.getContentResolver().unregisterContentObserver(mSettingObserver); + registerContentObservers(); + updateCameraRegistered(); + updateCameraDoubleTapPowerEnabled(); + if (launchWalletOptionOnPowerDoubleTap()) { + updateWalletDoubleTapPowerEnabled(); + } + updateEmergencyGestureEnabled(); + updateEmergencyGesturePowerButtonCooldownPeriodMs(); + } + } + }; + + private final ContentObserver mSettingObserver = + new ContentObserver(new Handler()) { + public void onChange(boolean selfChange, android.net.Uri uri, int userId) { + if (userId == mUserId) { + updateCameraRegistered(); + updateCameraDoubleTapPowerEnabled(); + if (launchWalletOptionOnPowerDoubleTap()) { + updateWalletDoubleTapPowerEnabled(); + } + updateEmergencyGestureEnabled(); + updateEmergencyGesturePowerButtonCooldownPeriodMs(); + } + } + }; private final class GestureEventListener implements SensorEventListener { @Override diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 7c4d4253157e..e39906bdc366 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -88,10 +88,12 @@ import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGest import static com.android.hardware.input.Flags.inputManagerLifecycleSupport; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.hardware.input.Flags.modifierShortcutDump; +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; +import static com.android.server.GestureLauncherService.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; 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { "android.intent.action.VOICE_ASSIST_RETAIL"; /** + * Maximum amount of time in milliseconds between consecutive power onKeyDown events to be + * considered a multi-press, only used for the power button. + * Note: To maintain backwards compatibility for the power button, we are measuring the times + * between consecutive down events instead of the first tap's up event and the second tap's + * down event. + */ + @VisibleForTesting public static final int POWER_MULTI_PRESS_TIMEOUT_MILLIS = + ViewConfiguration.getMultiPressTimeout(); + + /** * Lock protecting internal state. Must not call out into window * manager with lock held. (This lock will be acquired in places * where the window manager is calling in with its own lock held.) @@ -492,6 +504,32 @@ public class PhoneWindowManager implements WindowManagerPolicy { private WindowWakeUpPolicy mWindowWakeUpPolicy; + /** + * The three variables below are used for custom power key gesture detection in + * PhoneWindowManager. They are used to detect when the power button has been double pressed + * and, when it does happen, makes the behavior overrideable by the app. + * + * We cannot use the {@link PowerKeyRule} for this because multi-press power gesture detection + * and behaviors are handled by {@link com.android.server.GestureLauncherService}, and the + * {@link PowerKeyRule} only handles single and long-presses of the power button. As a result, + * overriding the double tap behavior requires custom gesture detection here that mimics the + * logic in {@link com.android.server.GestureLauncherService}. + * + * Long-term, it would be beneficial to move all power gesture detection to + * {@link PowerKeyRule} so that this custom logic isn't required. + */ + // Time of last power down event. + private long mLastPowerDown; + + // Number of power button events consecutively triggered (within a specific timeout threshold). + private int mPowerButtonConsecutiveTaps = 0; + + // Whether a double tap of the power button has been detected. + volatile boolean mDoubleTapPowerDetected; + + // Runnable that is queued on a delay when the first power keyDown event is sent to the app. + private Runnable mPowerKeyDelayedRunnable = null; + boolean mSafeMode; // Whether to allow dock apps with METADATA_DOCK_HOME to temporarily take over the Home key. @@ -1097,6 +1135,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { mPowerKeyHandled = mPowerKeyHandled || hungUp || handledByPowerManager || isKeyGestureTriggered || mKeyCombinationManager.isPowerKeyIntercepted(); + + if (overridePowerKeyBehaviorInFocusedWindow()) { + mPowerKeyHandled |= mDoubleTapPowerDetected; + } + if (!mPowerKeyHandled) { if (!interactive) { wakeUpFromWakeKey(event); @@ -2669,7 +2712,19 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mShouldEarlyShortPressOnPower) { return; } - powerPress(downTime, 1 /*count*/, displayId); + // TODO(b/380433365): Remove deferring single power press action when refactoring. + if (overridePowerKeyBehaviorInFocusedWindow()) { + mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); + mDeferredKeyActionExecutor.queueKeyAction( + KEYCODE_POWER, + downTime, + () -> { + powerPress(downTime, 1 /*count*/, displayId); + }); + } else { + powerPress(downTime, 1 /*count*/, displayId); + } + } @Override @@ -2700,7 +2755,17 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onMultiPress(long downTime, int count, int displayId) { - powerPress(downTime, count, displayId); + if (overridePowerKeyBehaviorInFocusedWindow()) { + mDeferredKeyActionExecutor.cancelQueuedAction(KEYCODE_POWER); + mDeferredKeyActionExecutor.queueKeyAction( + KEYCODE_POWER, + downTime, + () -> { + powerPress(downTime, count, displayId); + }); + } else { + powerPress(downTime, count, displayId); + } } @Override @@ -3477,6 +3542,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + if (overridePowerKeyBehaviorInFocusedWindow() && event.getKeyCode() == KEYCODE_POWER + && event.getAction() == KeyEvent.ACTION_UP + && mDoubleTapPowerDetected) { + mDoubleTapPowerDetected = false; + } + return needToConsumeKey ? keyConsumed : keyNotConsumed; } @@ -3992,6 +4063,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } + case KeyEvent.KEYCODE_POWER: + return interceptPowerKeyBeforeDispatching(focusedToken, event); case KeyEvent.KEYCODE_SCREENSHOT: if (firstDown) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); @@ -4047,6 +4120,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } + case KeyEvent.KEYCODE_POWER: + return interceptPowerKeyBeforeDispatching(focusedToken, event); } if (isValidGlobalKey(keyCode) && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) { @@ -4057,6 +4132,90 @@ public class PhoneWindowManager implements WindowManagerPolicy { return (metaState & KeyEvent.META_META_ON) != 0; } + /** + * Called by interceptKeyBeforeDispatching to handle interception logic for KEYCODE_POWER + * KeyEvents. + * + * @return true if intercepting the key, false if sending to app. + */ + private boolean interceptPowerKeyBeforeDispatching(IBinder focusedToken, KeyEvent event) { + if (!overridePowerKeyBehaviorInFocusedWindow()) { + //Flag disabled: intercept the power key and do not send to app. + return true; + } + if (event.getKeyCode() != KEYCODE_POWER) { + Log.wtf(TAG, "interceptPowerKeyBeforeDispatching received a non-power KeyEvent " + + "with key code: " + event.getKeyCode()); + return false; + } + + // Intercept keys (don't send to app) for 3x, 4x, 5x gestures) + if (mPowerButtonConsecutiveTaps > 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, 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()) { @@ -4631,6 +4790,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } + if (overridePowerKeyBehaviorInFocusedWindow() && keyCode == KEYCODE_POWER) { + handleUnhandledSystemKey(event); + return true; + } + if (useKeyGestureEventHandler()) { return false; } @@ -5465,8 +5629,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyEvent.actionToString(event.getAction()), mPowerKeyHandled ? 1 : 0, mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER)); - // Any activity on the power button stops the accessibility shortcut - result &= ~ACTION_PASS_TO_USER; + if (overridePowerKeyBehaviorInFocusedWindow()) { + result |= ACTION_PASS_TO_USER; + } else { + // Any activity on the power button stops the accessibility shortcut + result &= ~ACTION_PASS_TO_USER; + } + isWakeKey = false; // wake-up will be handled separately if (down) { interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered); @@ -5728,6 +5897,32 @@ public class PhoneWindowManager implements WindowManagerPolicy { } if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) { + if (overridePowerKeyBehaviorInFocusedWindow()) { + if (mGestureLauncherService != null) { + mGestureLauncherService.processPowerKeyDown(event); + } + + if (detectDoubleTapPower(event)) { + mDoubleTapPowerDetected = true; + + // Copy of the event for handler in case the original event gets recycled. + KeyEvent eventCopy = KeyEvent.obtain(event); + mDeferredKeyActionExecutor.queueKeyAction( + KeyEvent.KEYCODE_POWER, + eventCopy.getEventTime(), + () -> { + if (!handleCameraGesture(eventCopy, interactive)) { + mSingleKeyGestureDetector.interceptKey( + eventCopy, interactive, defaultDisplayOn); + } else { + mSingleKeyGestureDetector.reset(); + } + eventCopy.recycle(); + }); + return; + } + } + mPowerKeyHandled = handleCameraGesture(event, interactive); if (mPowerKeyHandled) { // handled by camera gesture. @@ -5739,6 +5934,27 @@ public class PhoneWindowManager implements WindowManagerPolicy { mSingleKeyGestureDetector.interceptKey(event, interactive, defaultDisplayOn); } + private boolean detectDoubleTapPower(KeyEvent event) { + if (event.getKeyCode() != KEYCODE_POWER || event.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + if (event.isLongPress()) { + return false; + } + + final long powerTapInterval = event.getEventTime() - mLastPowerDown; + mLastPowerDown = event.getEventTime(); + if (powerTapInterval >= POWER_MULTI_PRESS_TIMEOUT_MILLIS) { + // Tap too slow for double press + mPowerButtonConsecutiveTaps = 1; + } else { + mPowerButtonConsecutiveTaps++; + } + + return powerTapInterval < POWER_MULTI_PRESS_TIMEOUT_MILLIS + && mPowerButtonConsecutiveTaps == DOUBLE_POWER_TAP_COUNT_THRESHOLD; + } + // The camera gesture will be detected by GestureLauncherService. private boolean handleCameraGesture(KeyEvent event, boolean interactive) { // camera gesture. @@ -7595,6 +7811,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { null) == PERMISSION_GRANTED; } + + boolean canWindowOverridePowerKey(Context context, int uid, int inputFeaturesFlags) { + return canAppOverrideSystemKey(context, uid) + && (inputFeaturesFlags & WindowManager.LayoutParams + .INPUT_FEATURE_RECEIVE_POWER_KEY_DOUBLE_PRESS) != 0; + } } private int getTargetDisplayIdForKeyEvent(KeyEvent event) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index cebe790bb1b9..447d443282bd 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5750,9 +5750,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP || mKeyInterceptionInfo.layoutParamsPrivateFlags != getAttrs().privateFlags || mKeyInterceptionInfo.layoutParamsType != getAttrs().type || mKeyInterceptionInfo.windowTitle != getWindowTag() - || mKeyInterceptionInfo.windowOwnerUid != getOwningUid()) { + || mKeyInterceptionInfo.windowOwnerUid != getOwningUid() + || mKeyInterceptionInfo.inputFeaturesFlags != getAttrs().inputFeatures) { mKeyInterceptionInfo = new KeyInterceptionInfo(getAttrs().type, getAttrs().privateFlags, - getWindowTag().toString(), getOwningUid()); + getWindowTag().toString(), getOwningUid(), getAttrs().inputFeatures); } return mKeyInterceptionInfo; } diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java index 8024915692aa..9850cb09ae2b 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -16,7 +16,12 @@ package com.android.server; -import static com.android.server.GestureLauncherService.CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; +import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP; +import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap; + +import static com.android.server.GestureLauncherService.LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER; +import static com.android.server.GestureLauncherService.LAUNCH_WALLET_ON_DOUBLE_TAP_POWER; +import static com.android.server.GestureLauncherService.POWER_DOUBLE_TAP_MAX_TIME_MS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -24,19 +29,27 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.PendingIntent; import android.app.StatusBarManager; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Resources; import android.os.Looper; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; +import android.service.quickaccesswallet.QuickAccessWalletClient; import android.telecom.TelecomManager; import android.test.mock.MockContentResolver; import android.testing.TestableLooper; @@ -55,6 +68,7 @@ import com.android.server.statusbar.StatusBarManagerInternal; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -62,6 +76,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * Unit tests for {@link GestureLauncherService}. @@ -90,9 +106,32 @@ public class GestureLauncherServiceTest { private @Mock TelecomManager mTelecomManager; private @Mock MetricsLogger mMetricsLogger; @Mock private UiEventLogger mUiEventLogger; + @Mock private QuickAccessWalletClient mQuickAccessWalletClient; private MockContentResolver mContentResolver; private GestureLauncherService mGestureLauncherService; + private Context mInstrumentationContext = + InstrumentationRegistry.getInstrumentation().getContext(); + + private static final String LAUNCH_TEST_WALLET_ACTION = "LAUNCH_TEST_WALLET_ACTION"; + private static final String LAUNCH_FALLBACK_ACTION = "LAUNCH_FALLBACK_ACTION"; + private PendingIntent mGesturePendingIntent = + PendingIntent.getBroadcast( + mInstrumentationContext, + 0, + new Intent(LAUNCH_TEST_WALLET_ACTION), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + + private PendingIntent mFallbackPendingIntent = + PendingIntent.getBroadcast( + mInstrumentationContext, + 0, + new Intent(LAUNCH_FALLBACK_ACTION), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @BeforeClass public static void oneTimeInitialization() { if (Looper.myLooper() == null) { @@ -115,9 +154,49 @@ public class GestureLauncherServiceTest { when(mContext.getContentResolver()).thenReturn(mContentResolver); when(mContext.getSystemService(Context.TELECOM_SERVICE)).thenReturn(mTelecomManager); when(mTelecomManager.createLaunchEmergencyDialerIntent(null)).thenReturn(new Intent()); + when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(true); + + mGestureLauncherService = + new GestureLauncherService( + mContext, mMetricsLogger, mQuickAccessWalletClient, mUiEventLogger); + + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } - mGestureLauncherService = new GestureLauncherService(mContext, mMetricsLogger, - mUiEventLogger); + private WalletLaunchedReceiver registerWalletLaunchedReceiver(String action) { + IntentFilter filter = new IntentFilter(action); + WalletLaunchedReceiver receiver = new WalletLaunchedReceiver(); + mInstrumentationContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED); + return receiver; + } + + /** + * A simple {@link BroadcastReceiver} implementation that counts down a {@link CountDownLatch} + * when a matching message is received + */ + private static final class WalletLaunchedReceiver extends BroadcastReceiver { + private static final int TIMEOUT_SECONDS = 3; + + private final CountDownLatch mLatch; + + WalletLaunchedReceiver() { + mLatch = new CountDownLatch(1); + } + + @Override + public void onReceive(Context context, Intent intent) { + mLatch.countDown(); + context.unregisterReceiver(this); + } + + Boolean waitUntilShown() { + try { + return mLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return false; + } + } } @Test @@ -134,37 +213,123 @@ public class GestureLauncherServiceTest { @Test public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingDisabled() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(false); + withDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test public void testIsCameraDoubleTapPowerSettingEnabled_configFalseSettingEnabled() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(0); - assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( - mContext, FAKE_USER_ID)); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(false); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(0); + assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } } @Test public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingDisabled() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(1); + } assertFalse(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test public void testIsCameraDoubleTapPowerSettingEnabled_configTrueSettingEnabled() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(0); + } assertTrue(mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( mContext, FAKE_USER_ID)); } @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsCameraDoubleTapPowerSettingEnabled_actionWallet() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertFalse( + mGestureLauncherService.isCameraDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertTrue( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled_configDisabled() { + withDoubleTapPowerEnabledConfigValue(false); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertTrue( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled_settingDisabled() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(false); + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + + assertFalse( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testIsWalletDoubleTapPowerSettingEnabled_actionCamera() { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + + assertFalse( + mGestureLauncherService.isWalletDoubleTapPowerSettingEnabled( + mContext, FAKE_USER_ID)); + } + + @Test public void testIsEmergencyGestureSettingEnabled_settingDisabled() { withEmergencyGestureEnabledConfigValue(true); withEmergencyGestureEnabledSettingValue(false); @@ -245,12 +410,9 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_firstPowerDownCameraPowerGestureOnInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); - long eventTime = INITIAL_EVENT_TIME_MILLIS + - CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + long eventTime = INITIAL_EVENT_TIME_MILLIS + POWER_DOUBLE_TAP_MAX_TIME_MS - 1; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); boolean interactive = true; @@ -284,8 +446,12 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -298,7 +464,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -329,8 +495,12 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOffInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -343,7 +513,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -422,10 +592,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnInteractiveSetupComplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -437,7 +604,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -470,15 +637,145 @@ public class GestureLauncherServiceTest { } @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void + testInterceptPowerKeyDown_fiveInboundPresses_walletAndEmergencyEnabled_bothLaunch() { + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableEmergencyGesture(); + enableWalletGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + assertTrue(receiver.waitUntilShown()); + + // 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 trigger the emergency flow + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + verify(mUiEventLogger, times(1)) + .log(GestureLauncherService.GestureLauncherEvent.GESTURE_EMERGENCY_TAP_POWER); + verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected(); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_intervalInBoundsWalletPowerGesture() { + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableWalletGesture(); + enableEmergencyGesture(); + + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + assertTrue(receiver.waitUntilShown()); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_walletGestureOn_quickAccessWalletServiceUnavailable() { + when(mQuickAccessWalletClient.isWalletServiceAvailable()).thenReturn(false); + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableWalletGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, false); + + assertFalse(receiver.waitUntilShown()); + } + + @Test + public void testInterceptPowerKeyDown_walletGestureOn_userSetupIncomplete() { + WalletLaunchedReceiver receiver = registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(mGesturePendingIntent); + enableWalletGesture(); + withUserSetupCompleteValue(false); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + assertFalse(receiver.waitUntilShown()); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_walletPowerGesture_nullPendingIntent() { + WalletLaunchedReceiver gestureReceiver = + registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(null); + WalletLaunchedReceiver fallbackReceiver = + registerWalletLaunchedReceiver(LAUNCH_FALLBACK_ACTION); + setUpWalletFallbackPendingIntent(mFallbackPendingIntent); + enableWalletGesture(); + enableEmergencyGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, true, true); + + assertFalse(gestureReceiver.waitUntilShown()); + assertTrue(fallbackReceiver.waitUntilShown()); + } + + @Test + @RequiresFlagsEnabled(FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) + public void testInterceptPowerKeyDown_walletPowerGesture_intervalOutOfBounds() { + WalletLaunchedReceiver gestureReceiver = + registerWalletLaunchedReceiver(LAUNCH_TEST_WALLET_ACTION); + setUpGetGestureTargetActivityPendingIntent(null); + WalletLaunchedReceiver fallbackReceiver = + registerWalletLaunchedReceiver(LAUNCH_FALLBACK_ACTION); + setUpWalletFallbackPendingIntent(mFallbackPendingIntent); + enableWalletGesture(); + enableEmergencyGesture(); + + // First event + long eventTime = INITIAL_EVENT_TIME_MILLIS; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; + eventTime += interval; + sendPowerKeyDownToGestureLauncherServiceAndAssertValues(eventTime, false, false); + + assertFalse(gestureReceiver.waitUntilShown()); + assertFalse(fallbackReceiver.waitUntilShown()); + } + + @Test public void testInterceptPowerKeyDown_fiveInboundPresses_cameraAndEmergencyEnabled_bothLaunch() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - withEmergencyGestureEnabledConfigValue(true); - withEmergencyGestureEnabledSettingValue(true); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - mGestureLauncherService.updateEmergencyGestureEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); + enableEmergencyGesture(); // First button press does nothing long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -491,7 +788,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; // 2nd button triggers camera eventTime += interval; @@ -580,7 +877,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; // 3 more button presses which should not trigger any gesture (camera gesture disabled) for (int i = 0; i < 3; i++) { eventTime += interval; @@ -634,7 +931,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; // 3 more button presses which should not trigger any gesture, but intercepts action. for (int i = 0; i < 3; i++) { eventTime += interval; @@ -737,7 +1034,7 @@ public class GestureLauncherServiceTest { interactive, outLaunched); assertTrue(intercepted); assertFalse(outLaunched.value); - interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; } } @@ -765,7 +1062,7 @@ public class GestureLauncherServiceTest { interactive, outLaunched); assertTrue(intercepted); assertFalse(outLaunched.value); - interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; } } @@ -916,7 +1213,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT, IGNORED_META_STATE, IGNORED_DEVICE_ID, IGNORED_SCANCODE, @@ -947,9 +1244,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnInteractiveSetupIncomplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); withUserSetupCompleteValue(false); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -962,7 +1257,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -995,9 +1290,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalMidBoundsCameraPowerGestureOnInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1009,7 +1302,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1042,9 +1335,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOnInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1087,8 +1378,12 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffNotInteractive() { - withCameraDoubleTapPowerEnableConfigValue(false); - withCameraDoubleTapPowerDisableSettingValue(1); + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerGestureEnableSettingValue(false); + } else { + withCameraDoubleTapPowerEnableConfigValue(false); + withCameraDoubleTapPowerDisableSettingValue(1); + } mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -1101,7 +1396,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1146,7 +1441,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1223,10 +1518,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnNotInteractiveSetupComplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); - withUserSetupCompleteValue(true); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1238,7 +1530,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1272,9 +1564,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOnNotInteractiveSetupIncomplete() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); withUserSetupCompleteValue(false); long eventTime = INITIAL_EVENT_TIME_MILLIS; @@ -1287,7 +1577,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS - 1; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1332,7 +1622,7 @@ public class GestureLauncherServiceTest { assertFalse(intercepted); assertFalse(outLaunched.value); - final long interval = CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; + final long interval = POWER_DOUBLE_TAP_MAX_TIME_MS; eventTime += interval; keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); @@ -1365,9 +1655,7 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_intervalOutOfBoundsCameraPowerGestureOnNotInteractive() { - withCameraDoubleTapPowerEnableConfigValue(true); - withCameraDoubleTapPowerDisableSettingValue(0); - mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + enableCameraGesture(); long eventTime = INITIAL_EVENT_TIME_MILLIS; KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, @@ -1409,12 +1697,53 @@ public class GestureLauncherServiceTest { } /** + * If processPowerKeyDown is called instead of interceptPowerKeyDown (meaning the double tap + * gesture isn't performed), the emergency gesture is still launched. + */ + @Test + public void + testProcessPowerKeyDown_fiveInboundPresses_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 = 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. */ private long triggerEmergencyGesture() { - return triggerEmergencyGesture(CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1); + return triggerEmergencyGesture(POWER_DOUBLE_TAP_MAX_TIME_MS - 1); } /** @@ -1473,6 +1802,27 @@ public class GestureLauncherServiceTest { .thenReturn(enableConfigValue); } + private void withDoubleTapPowerEnabledConfigValue(boolean enable) { + when(mResources.getBoolean(com.android.internal.R.bool.config_doubleTapPowerGestureEnabled)) + .thenReturn(enable); + } + + private void withDoubleTapPowerGestureEnableSettingValue(boolean enable) { + Settings.Secure.putIntForUser( + mContentResolver, + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE_ENABLED, + enable ? 1 : 0, + UserHandle.USER_CURRENT); + } + + private void withDefaultDoubleTapPowerGestureAction(int action) { + Settings.Secure.putIntForUser( + mContentResolver, + Settings.Secure.DOUBLE_TAP_POWER_BUTTON_GESTURE, + action, + UserHandle.USER_CURRENT); + } + private void withEmergencyGestureEnabledConfigValue(boolean enableConfigValue) { when(mResources.getBoolean( com.android.internal.R.bool.config_emergencyGestureEnabled)) @@ -1510,4 +1860,72 @@ public class GestureLauncherServiceTest { userSetupCompleteValue, UserHandle.USER_CURRENT); } + + private void setUpGetGestureTargetActivityPendingIntent(PendingIntent pendingIntent) { + doAnswer( + invocation -> { + QuickAccessWalletClient.GesturePendingIntentCallback callback = + (QuickAccessWalletClient.GesturePendingIntentCallback) + invocation.getArguments()[1]; + callback.onGesturePendingIntentRetrieved(pendingIntent); + return null; + }) + .when(mQuickAccessWalletClient) + .getGestureTargetActivityPendingIntent(any(), any()); + } + + private void setUpWalletFallbackPendingIntent(PendingIntent pendingIntent) { + doAnswer( + invocation -> { + QuickAccessWalletClient.WalletPendingIntentCallback callback = + (QuickAccessWalletClient.WalletPendingIntentCallback) + invocation.getArguments()[1]; + callback.onWalletPendingIntentRetrieved(pendingIntent); + return null; + }) + .when(mQuickAccessWalletClient) + .getWalletPendingIntent(any(), any()); + } + + private void enableWalletGesture() { + withDefaultDoubleTapPowerGestureAction(LAUNCH_WALLET_ON_DOUBLE_TAP_POWER); + withDoubleTapPowerGestureEnableSettingValue(true); + withDoubleTapPowerEnabledConfigValue(true); + + mGestureLauncherService.updateWalletDoubleTapPowerEnabled(); + withUserSetupCompleteValue(true); + } + + private void enableEmergencyGesture() { + withEmergencyGestureEnabledConfigValue(true); + withEmergencyGestureEnabledSettingValue(true); + mGestureLauncherService.updateEmergencyGestureEnabled(); + withUserSetupCompleteValue(true); + } + + private void enableCameraGesture() { + if (launchWalletOptionOnPowerDoubleTap()) { + withDoubleTapPowerEnabledConfigValue(true); + withDoubleTapPowerGestureEnableSettingValue(true); + withDefaultDoubleTapPowerGestureAction(LAUNCH_CAMERA_ON_DOUBLE_TAP_POWER); + } else { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(0); + } + mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + withUserSetupCompleteValue(true); + } + + private void sendPowerKeyDownToGestureLauncherServiceAndAssertValues( + long eventTime, boolean expectedIntercept, boolean expectedOutLaunchedValue) { + KeyEvent keyEvent = + new KeyEvent( + IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, IGNORED_REPEAT); + boolean interactive = true; + MutableBoolean outLaunched = new MutableBoolean(true); + boolean intercepted = + mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, outLaunched); + assertEquals(intercepted, expectedIntercept); + assertEquals(outLaunched.value, expectedOutLaunchedValue); + } } diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java index 05a1482b9be6..be516e9553c0 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java @@ -18,15 +18,22 @@ package com.android.server.policy; import static android.view.KeyEvent.KEYCODE_POWER; import static android.view.KeyEvent.KEYCODE_VOLUME_UP; +import static com.android.hardware.input.Flags.FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS; +import static com.android.server.policy.PhoneWindowManager.POWER_MULTI_PRESS_TIMEOUT_MILLIS; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_SLEEP; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_GO_TO_SLEEP; +import static org.junit.Assert.assertEquals; + +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.view.Display; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; /** @@ -39,8 +46,12 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { @Before public void setUp() { setUpPhoneWindowManager(); + mPhoneWindowManager.overrideStatusBarManagerInternal(); } + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + /** * Power single press to turn screen on/off. */ @@ -50,6 +61,8 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER); mPhoneWindowManager.assertPowerSleep(); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + // turn screen on when begin from non-interactive. mPhoneWindowManager.overrideDisplayState(Display.STATE_OFF); sendKey(KEYCODE_POWER); @@ -90,7 +103,7 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { mPhoneWindowManager.overrideCanStartDreaming(false); sendKey(KEYCODE_POWER); sendKey(KEYCODE_POWER); - mPhoneWindowManager.assertCameraLaunch(); + mPhoneWindowManager.assertDoublePowerLaunch(); mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); } @@ -101,7 +114,7 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { public void testPowerDoublePress() { sendKey(KEYCODE_POWER); sendKey(KEYCODE_POWER); - mPhoneWindowManager.assertCameraLaunch(); + mPhoneWindowManager.assertDoublePowerLaunch(); } /** @@ -114,6 +127,8 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER, true); mPhoneWindowManager.assertSearchManagerLaunchAssist(); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + // Show global actions. mPhoneWindowManager.overrideLongPressOnPower(LONG_PRESS_POWER_GLOBAL_ACTIONS); sendKey(KEYCODE_POWER, true); @@ -141,4 +156,139 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { sendKey(KEYCODE_POWER); mPhoneWindowManager.assertNoPowerSleep(); } + + /** + * Double press of power when the window handles the power key events. The + * system double power gesture launch should not be performed. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerDoublePress_windowHasOverridePermissionAndKeysHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); + + mPhoneWindowManager.assertNoDoublePowerLaunch(); + } + + /** + * Double press of power when the window doesn't handle the power key events. + * The system default gesture launch should be performed and the app should receive both events. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerDoublePress_windowHasOverridePermissionAndKeysUnHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> false); + + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); + mPhoneWindowManager.assertDoublePowerLaunch(); + assertEquals(getDownKeysDispatched(), 2); + assertEquals(getUpKeysDispatched(), 2); + } + + /** + * Triple press of power when the window handles the power key double press gesture. + * The system default gesture launch should not be performed, and the app only receives the + * first two presses. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerTriplePress_windowHasOverridePermissionAndKeysHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertDidNotLockAfterAppTransitionFinished(); + mPhoneWindowManager.assertNoDoublePowerLaunch(); + assertEquals(getDownKeysDispatched(), 2); + assertEquals(getUpKeysDispatched(), 2); + } + + /** + * Tests a single press, followed by a double press when the window can handle the power key. + * The app should receive all 3 events. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerTriplePressWithDelay_windowHasOverridePermissionAndKeysHandled() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + + sendKey(KEYCODE_POWER); + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertNoDoublePowerLaunch(); + assertEquals(getDownKeysDispatched(), 3); + assertEquals(getUpKeysDispatched(), 3); + } + + /** + * Tests single press when window doesn't handle the power key. Phone should go to sleep. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerSinglePress_windowHasOverridePermissionAndKeyUnhandledByApp() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> false); + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); + + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.assertPowerSleep(); + } + + /** + * Tests single press when the window handles the power key. Phone should go to sleep after a + * delay of {POWER_MULTI_PRESS_TIMEOUT_MILLIS} + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerSinglePress_windowHasOverridePermissionAndKeyHandledByApp() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); + + sendKey(KEYCODE_POWER); + + mPhoneWindowManager.moveTimeForward(POWER_MULTI_PRESS_TIMEOUT_MILLIS); + + mPhoneWindowManager.assertPowerSleep(); + } + + + /** + * Tests 5x press when the window handles the power key. Emergency gesture should still be + * launched. + */ + @Test + @EnableFlags(FLAG_OVERRIDE_POWER_KEY_BEHAVIOR_IN_FOCUSED_WINDOW) + public void testPowerFiveTimesPress_windowHasOverridePermissionAndKeyHandledByApp() { + mPhoneWindowManager.overrideCanWindowOverridePowerKey(true); + setDispatchedKeyHandler(keyEvent -> true); + mPhoneWindowManager.overrideDisplayState(Display.STATE_ON); + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_GO_TO_SLEEP); + + for (int i = 0; i < 5; ++i) { + sendKey(KEYCODE_POWER); + mPhoneWindowManager.moveTimeForward(100); + } + + mPhoneWindowManager.assertEmergencyLaunch(); + assertEquals(getDownKeysDispatched(), 2); + assertEquals(getUpKeysDispatched(), 2); + } } diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java index 9e47a008592c..591950176444 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java @@ -85,7 +85,11 @@ class ShortcutKeyTestBase { private Resources mResources; private PackageManager mPackageManager; TestPhoneWindowManager mPhoneWindowManager; - DispatchedKeyHandler mDispatchedKeyHandler = event -> false; + + DispatchedKeyHandler mDispatchedKeyHandler; + private int mDownKeysDispatched; + private int mUpKeysDispatched; + Context mContext; /** Modifier key to meta state */ @@ -116,6 +120,9 @@ class ShortcutKeyTestBase { XmlResourceParser testBookmarks = mResources.getXml( com.android.frameworks.wmtests.R.xml.bookmarks); doReturn(testBookmarks).when(mResources).getXml(com.android.internal.R.xml.bookmarks); + mDispatchedKeyHandler = event -> false; + mDownKeysDispatched = 0; + mUpKeysDispatched = 0; try { // Keep packageName / className in sync with @@ -278,6 +285,14 @@ class ShortcutKeyTestBase { doReturn(expectedBehavior).when(mResources).getInteger(eq(resId)); } + int getDownKeysDispatched() { + return mDownKeysDispatched; + } + + int getUpKeysDispatched() { + return mUpKeysDispatched; + } + private void interceptKey(KeyEvent keyEvent) { int actions = mPhoneWindowManager.interceptKeyBeforeQueueing(keyEvent); if ((actions & ACTION_PASS_TO_USER) != 0) { @@ -285,6 +300,11 @@ class ShortcutKeyTestBase { if (!mDispatchedKeyHandler.onKeyDispatched(keyEvent)) { mPhoneWindowManager.interceptUnhandledKey(keyEvent); } + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + ++mDownKeysDispatched; + } else { + ++mUpKeysDispatched; + } } } mPhoneWindowManager.dispatchAllPendingEvents(); diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 9db76d47fed7..f06b45e94f77 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -22,6 +22,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.STATE_ON; import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE; +import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong; @@ -45,10 +46,14 @@ import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_SHUT import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM; import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.after; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.description; import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.timeout; @@ -85,7 +90,9 @@ import android.os.VibratorInfo; import android.os.test.TestLooper; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; +import android.service.quickaccesswallet.QuickAccessWalletClient; import android.telecom.TelecomManager; +import android.util.MutableBoolean; import android.view.Display; import android.view.InputEvent; import android.view.KeyCharacterMap; @@ -95,9 +102,12 @@ import android.view.autofill.AutofillManagerInternal; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.accessibility.AccessibilityShortcutController; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.KeyInterceptionInfo; import com.android.server.GestureLauncherService; import com.android.server.LocalServices; +import com.android.server.SystemService; import com.android.server.input.InputManagerInternal; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.pm.UserManagerInternal; @@ -120,6 +130,7 @@ import org.mockito.MockSettings; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.util.List; import java.util.function.Supplier; @@ -132,6 +143,8 @@ class TestPhoneWindowManager { private PhoneWindowManager mPhoneWindowManager; private Context mContext; + private GestureLauncherService mGestureLauncherService; + @Mock private WindowManagerInternal mWindowManagerInternal; @Mock private ActivityManagerInternal mActivityManagerInternal; @@ -163,7 +176,9 @@ class TestPhoneWindowManager { @Mock private DisplayRotation mDisplayRotation; @Mock private DisplayPolicy mDisplayPolicy; @Mock private WindowManagerPolicy.ScreenOnListener mScreenOnListener; - @Mock private GestureLauncherService mGestureLauncherService; + @Mock private QuickAccessWalletClient mQuickAccessWalletClient; + @Mock private MetricsLogger mMetricsLogger; + @Mock private UiEventLogger mUiEventLogger; @Mock private GlobalActions mGlobalActions; @Mock private AccessibilityShortcutController mAccessibilityShortcutController; @@ -192,6 +207,8 @@ class TestPhoneWindowManager { private int mKeyEventPolicyFlags = FLAG_INTERACTIVE; + private int mProcessPowerKeyDownCount = 0; + private class TestTalkbackShortcutController extends TalkbackShortcutController { TestTalkbackShortcutController(Context context) { super(context); @@ -260,6 +277,8 @@ class TestPhoneWindowManager { MockitoAnnotations.initMocks(this); mHandler = new Handler(mTestLooper.getLooper()); mContext = mockingDetails(context).isSpy() ? context : spy(context); + mGestureLauncherService = spy(new GestureLauncherService(mContext, mMetricsLogger, + mQuickAccessWalletClient, mUiEventLogger)); setUp(supportSettingsUpdate); mTestLooper.dispatchAll(); } @@ -272,6 +291,7 @@ class TestPhoneWindowManager { mMockitoSession = mockitoSession() .mockStatic(LocalServices.class, spyStubOnly) .mockStatic(KeyCharacterMap.class) + .mockStatic(GestureLauncherService.class) .strictness(Strictness.LENIENT) .startMocking(); @@ -294,6 +314,16 @@ class TestPhoneWindowManager { () -> LocalServices.getService(eq(PowerManagerInternal.class))); doReturn(mDisplayManagerInternal).when( () -> LocalServices.getService(eq(DisplayManagerInternal.class))); + doReturn(true).when( + () -> GestureLauncherService.isCameraDoubleTapPowerSettingEnabled(any(), anyInt()) + ); + doReturn(true).when( + () -> GestureLauncherService.isEmergencyGestureSettingEnabled(any(), anyInt()) + ); + doReturn(true).when( + () -> GestureLauncherService.isGestureLauncherEnabled(any()) + ); + mGestureLauncherService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START); doReturn(mGestureLauncherService).when( () -> LocalServices.getService(eq(GestureLauncherService.class))); doReturn(mUserManagerInternal).when( @@ -375,7 +405,8 @@ class TestPhoneWindowManager { doNothing().when(mContext).startActivityAsUser(any(), any()); doNothing().when(mContext).startActivityAsUser(any(), any(), any()); - KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0); + KeyInterceptionInfo interceptionInfo = new KeyInterceptionInfo(0, 0, null, 0, + /* inputFeatureFlags = */ 0); doReturn(interceptionInfo) .when(mWindowManagerInternal).getKeyInterceptionInfoFromToken(any()); @@ -393,6 +424,8 @@ class TestPhoneWindowManager { eq(TEST_BROWSER_ROLE_PACKAGE_NAME)); doReturn(mSmsIntent).when(mPackageManager).getLaunchIntentForPackage( eq(TEST_SMS_ROLE_PACKAGE_NAME)); + mProcessPowerKeyDownCount = 0; + captureProcessPowerKeyDownCount(); Mockito.reset(mContext); } @@ -638,6 +671,12 @@ class TestPhoneWindowManager { .when(mButtonOverridePermissionChecker).canAppOverrideSystemKey(any(), anyInt()); } + void overrideCanWindowOverridePowerKey(boolean granted) { + doReturn(granted) + .when(mButtonOverridePermissionChecker).canWindowOverridePowerKey(any(), anyInt(), + anyInt()); + } + void overrideKeyEventPolicyFlags(int flags) { mKeyEventPolicyFlags = flags; } @@ -713,13 +752,59 @@ class TestPhoneWindowManager { verify(mPowerManager, never()).goToSleep(anyLong(), anyInt(), anyInt()); } - void assertCameraLaunch() { + void assertDoublePowerLaunch() { + ArgumentCaptor<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 @@ class TestPhoneWindowManager { 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 43844f6514e8..038c6d7754b5 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -541,7 +541,8 @@ class InputManagerServiceTests { 0 }, "title", - /* uid = */0 + /* uid = */0, + /* inputFeatureFlags = */ 0 ) whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info) } |