Merge "Introduce system shortcut key unit tests"
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 5816e98..f843af8 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -185,6 +185,7 @@
import com.android.internal.R;
import com.android.internal.accessibility.AccessibilityShortcutController;
import com.android.internal.accessibility.util.AccessibilityUtils;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.AssistUtils;
import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.internal.logging.MetricsLogger;
@@ -1544,9 +1545,6 @@
}
void showGlobalActionsInternal() {
- if (mGlobalActions == null) {
- mGlobalActions = new GlobalActions(mContext, mWindowManagerFuncs);
- }
final boolean keyguardShowing = isKeyguardShowingAndNotOccluded();
mGlobalActions.showDialog(keyguardShowing, isDeviceProvisioned());
// since it took two seconds of long press to bring this up,
@@ -1869,11 +1867,45 @@
mDefaultDisplayPolicy = mDefaultDisplayRotation.getDisplayPolicy();
}
+ /** Point of injection for test dependencies. */
+ @VisibleForTesting
+ static class Injector {
+ private final Context mContext;
+ private final WindowManagerFuncs mWindowManagerFuncs;
+
+ Injector(Context context, WindowManagerFuncs funcs) {
+ mContext = context;
+ mWindowManagerFuncs = funcs;
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ WindowManagerFuncs getWindowManagerFuncs() {
+ return mWindowManagerFuncs;
+ }
+
+ AccessibilityShortcutController getAccessibilityShortcutController(
+ Context context, Handler handler, int initialUserId) {
+ return new AccessibilityShortcutController(context, handler, initialUserId);
+ }
+
+ GlobalActions getGlobalActions() {
+ return new GlobalActions(mContext, mWindowManagerFuncs);
+ }
+ }
+
/** {@inheritDoc} */
@Override
- public void init(Context context, WindowManagerFuncs windowManagerFuncs) {
- mContext = context;
- mWindowManagerFuncs = windowManagerFuncs;
+ public void init(Context context, WindowManagerFuncs funcs) {
+ init(new Injector(context, funcs));
+ }
+
+ @VisibleForTesting
+ void init(Injector injector) {
+ mContext = injector.getContext();
+ mWindowManagerFuncs = injector.getWindowManagerFuncs();
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
@@ -1888,8 +1920,9 @@
mHasFeatureLeanback = mPackageManager.hasSystemFeature(FEATURE_LEANBACK);
mHasFeatureAuto = mPackageManager.hasSystemFeature(FEATURE_AUTOMOTIVE);
mHasFeatureHdmiCec = mPackageManager.hasSystemFeature(FEATURE_HDMI_CEC);
- mAccessibilityShortcutController =
- new AccessibilityShortcutController(mContext, new Handler(), mCurrentUserId);
+ mAccessibilityShortcutController = injector.getAccessibilityShortcutController(
+ mContext, new Handler(), mCurrentUserId);
+ mGlobalActions = injector.getGlobalActions();
mLogger = new MetricsLogger();
mScreenOffSleepTokenAcquirer = mActivityTaskManagerInternal
@@ -1904,7 +1937,7 @@
res.getBoolean(com.android.internal.R.bool.config_wakeOnBackKeyPress);
// Init display burn-in protection
- boolean burnInProtectionEnabled = context.getResources().getBoolean(
+ boolean burnInProtectionEnabled = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableBurnInProtection);
// Allow a system property to override this. Used by developer settings.
boolean burnInProtectionDevMode =
@@ -1922,7 +1955,7 @@
maxVertical = -4;
maxRadius = (isRoundWindow()) ? 6 : -1;
} else {
- Resources resources = context.getResources();
+ Resources resources = mContext.getResources();
minHorizontal = resources.getInteger(
com.android.internal.R.integer.config_burnInProtectionMinHorizontalOffset);
maxHorizontal = resources.getInteger(
@@ -1935,21 +1968,21 @@
com.android.internal.R.integer.config_burnInProtectionMaxRadius);
}
mBurnInProtectionHelper = new BurnInProtectionHelper(
- context, minHorizontal, maxHorizontal, minVertical, maxVertical, maxRadius);
+ mContext, minHorizontal, maxHorizontal, minVertical, maxVertical, maxRadius);
}
mHandler = new PolicyHandler();
mWakeGestureListener = new MyWakeGestureListener(mContext, mHandler);
mSettingsObserver = new SettingsObserver(mHandler);
mSettingsObserver.observe();
- mModifierShortcutManager = new ModifierShortcutManager(context);
- mUiMode = context.getResources().getInteger(
+ mModifierShortcutManager = new ModifierShortcutManager(mContext);
+ mUiMode = mContext.getResources().getInteger(
com.android.internal.R.integer.config_defaultUiModeType);
mHomeIntent = new Intent(Intent.ACTION_MAIN, null);
mHomeIntent.addCategory(Intent.CATEGORY_HOME);
mHomeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
- mEnableCarDockHomeCapture = context.getResources().getBoolean(
+ mEnableCarDockHomeCapture = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableCarDockHomeLaunch);
mCarDockIntent = new Intent(Intent.ACTION_MAIN, null);
mCarDockIntent.addCategory(Intent.CATEGORY_CAR_DOCK);
@@ -1964,7 +1997,7 @@
mVrHeadsetHomeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
- mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+ mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mBroadcastWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
"PhoneWindowManager.mBroadcastWakeLock");
mPowerKeyWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
@@ -2036,9 +2069,9 @@
readConfigurationDependentBehaviors();
- mDisplayFoldController = DisplayFoldController.create(context, DEFAULT_DISPLAY);
+ mDisplayFoldController = DisplayFoldController.create(mContext, DEFAULT_DISPLAY);
- mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+ mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(
Context.ACCESSIBILITY_SERVICE);
// register for dock events
@@ -2048,7 +2081,7 @@
filter.addAction(UiModeManager.ACTION_ENTER_DESK_MODE);
filter.addAction(UiModeManager.ACTION_EXIT_DESK_MODE);
filter.addAction(Intent.ACTION_DOCK_EVENT);
- Intent intent = context.registerReceiver(mDockReceiver, filter);
+ Intent intent = mContext.registerReceiver(mDockReceiver, filter);
if (intent != null) {
// Retrieve current sticky dock event broadcast.
mDefaultDisplayPolicy.setDockMode(intent.getIntExtra(Intent.EXTRA_DOCK_STATE,
@@ -2059,13 +2092,13 @@
filter = new IntentFilter();
filter.addAction(Intent.ACTION_DREAMING_STARTED);
filter.addAction(Intent.ACTION_DREAMING_STOPPED);
- context.registerReceiver(mDreamReceiver, filter);
+ mContext.registerReceiver(mDreamReceiver, filter);
// register for multiuser-relevant broadcasts
filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
- context.registerReceiver(mMultiuserReceiver, filter);
+ mContext.registerReceiver(mMultiuserReceiver, filter);
- mVibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
+ mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
mSafeModeEnabledVibePattern = getLongIntArray(mContext.getResources(),
com.android.internal.R.array.config_safeModeEnabledVibePattern);
diff --git a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
new file mode 100644
index 0000000..62875e5
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.policy;
+
+import static android.view.KeyEvent.KEYCODE_POWER;
+import static android.view.KeyEvent.KEYCODE_VOLUME_DOWN;
+import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
+
+import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS;
+import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
+
+import android.view.ViewConfiguration;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for combination key shortcuts.
+ *
+ * Build/Install/Run:
+ * atest WmTests:CombinationKeyTests
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CombinationKeyTests extends ShortcutKeyTestBase {
+ private static final long A11Y_KEY_HOLD_MILLIS = 3500;
+
+ /**
+ * Power-VolDown to take screenshot.
+ */
+ @Test
+ public void testPowerVolumeDown() {
+ sendKeyCombination(new int[]{KEYCODE_POWER, KEYCODE_VOLUME_DOWN},
+ ViewConfiguration.get(mContext).getScreenshotChordKeyTimeout());
+ mPhoneWindowManager.assertTakeScreenshotCalled();
+ }
+
+ /**
+ * Power-VolUp to show global actions or mute audio. (Phone default behavior)
+ */
+ @Test
+ public void testPowerVolumeUp() {
+ // Show global actions.
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
+ sendKeyCombination(new int[]{KEYCODE_POWER, KEYCODE_VOLUME_UP}, 0);
+ mPhoneWindowManager.assertShowGlobalActionsCalled();
+
+ // Mute audio (hold over 100ms).
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
+ sendKeyCombination(new int[]{KEYCODE_POWER, KEYCODE_VOLUME_UP}, 100);
+ mPhoneWindowManager.assertVolumeMute();
+ }
+
+ /**
+ * VolDown-VolUp and hold 3 secs to enable accessibility service.
+ */
+ @Test
+ public void testVolumeDownVolumeUp() {
+ sendKeyCombination(new int[]{KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP}, A11Y_KEY_HOLD_MILLIS);
+ mPhoneWindowManager.assertAccessibilityKeychordCalled();
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
similarity index 98%
rename from services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java
rename to services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
index cf57181..4c69857 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
@@ -43,11 +43,11 @@
* Test class for {@link KeyCombinationManager}.
*
* Build/Install/Run:
- * atest KeyCombinationTests
+ * atest KeyCombinationManagerTests
*/
@SmallTest
-public class KeyCombinationTests {
+public class KeyCombinationManagerTests {
private KeyCombinationManager mKeyCombinationManager;
private final CountDownLatch mAction1Triggered = new CountDownLatch(1);
@@ -228,4 +228,4 @@
mKeyCombinationManager.interceptKey(firstKeyDown, true);
assertTrue(mKeyCombinationManager.getKeyInterceptTimeout(KEYCODE_VOLUME_UP) > eventTime);
}
-}
\ No newline at end of file
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
new file mode 100644
index 0000000..7c6fd05
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.policy;
+
+import static android.view.KeyEvent.KEYCODE_POWER;
+import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
+
+import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT;
+import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS;
+
+import android.view.Display;
+
+import org.junit.Test;
+
+/**
+ * Test class for power key gesture.
+ *
+ * Build/Install/Run:
+ * atest WmTests:PowerKeyGestureTests
+ */
+public class PowerKeyGestureTests extends ShortcutKeyTestBase {
+ /**
+ * Power single press to turn screen on/off.
+ */
+ @Test
+ public void testPowerSinglePress() {
+ sendKey(KEYCODE_POWER);
+ mPhoneWindowManager.assertPowerSleep();
+
+ // turn screen on when begin from non-interactive.
+ mPhoneWindowManager.overrideDisplayState(Display.STATE_OFF);
+ sendKey(KEYCODE_POWER);
+ mPhoneWindowManager.assertPowerWakeUp();
+ mPhoneWindowManager.assertNoPowerSleep();
+ }
+
+ /**
+ * Power double press to trigger camera.
+ */
+ @Test
+ public void testPowerDoublePress() {
+ sendKey(KEYCODE_POWER);
+ sendKey(KEYCODE_POWER);
+ mPhoneWindowManager.assertCameraLaunch();
+ }
+
+ /**
+ * Power long press to show assistant or global actions.
+ */
+ @Test
+ public void testPowerLongPress() {
+ // Show assistant.
+ mPhoneWindowManager.overrideLongPressOnPower(LONG_PRESS_POWER_ASSISTANT);
+ sendKey(KEYCODE_POWER, true);
+ mPhoneWindowManager.assertAssistLaunch();
+
+ // Show global actions.
+ mPhoneWindowManager.overrideLongPressOnPower(LONG_PRESS_POWER_GLOBAL_ACTIONS);
+ sendKey(KEYCODE_POWER, true);
+ mPhoneWindowManager.assertShowGlobalActionsCalled();
+ }
+
+ /**
+ * Ignore power press if combination key already triggered.
+ */
+ @Test
+ public void testIgnoreSinglePressWhenCombinationKeyTriggered() {
+ sendKeyCombination(new int[]{KEYCODE_POWER, KEYCODE_VOLUME_UP}, 0);
+ mPhoneWindowManager.assertNoPowerSleep();
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
new file mode 100644
index 0000000..6368f47
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.policy;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.KeyEvent.KEYCODE_ALT_LEFT;
+import static android.view.KeyEvent.KEYCODE_ALT_RIGHT;
+import static android.view.KeyEvent.KEYCODE_CTRL_LEFT;
+import static android.view.KeyEvent.KEYCODE_CTRL_RIGHT;
+import static android.view.KeyEvent.KEYCODE_META_LEFT;
+import static android.view.KeyEvent.KEYCODE_META_RIGHT;
+import static android.view.KeyEvent.KEYCODE_SHIFT_LEFT;
+import static android.view.KeyEvent.KEYCODE_SHIFT_RIGHT;
+import static android.view.KeyEvent.META_ALT_LEFT_ON;
+import static android.view.KeyEvent.META_ALT_ON;
+import static android.view.KeyEvent.META_ALT_RIGHT_ON;
+import static android.view.KeyEvent.META_CTRL_LEFT_ON;
+import static android.view.KeyEvent.META_CTRL_ON;
+import static android.view.KeyEvent.META_CTRL_RIGHT_ON;
+import static android.view.KeyEvent.META_META_LEFT_ON;
+import static android.view.KeyEvent.META_META_ON;
+import static android.view.KeyEvent.META_META_RIGHT_ON;
+import static android.view.KeyEvent.META_SHIFT_LEFT_ON;
+import static android.view.KeyEvent.META_SHIFT_ON;
+import static android.view.KeyEvent.META_SHIFT_RIGHT_ON;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER;
+
+import static java.util.Collections.unmodifiableMap;
+
+import android.content.Context;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.ViewConfiguration;
+
+import org.junit.After;
+import org.junit.Before;
+
+import java.util.Map;
+
+class ShortcutKeyTestBase {
+ TestPhoneWindowManager mPhoneWindowManager;
+ final Context mContext = getInstrumentation().getTargetContext();
+
+ /** Modifier key to meta state */
+ private static final Map<Integer, Integer> MODIFIER;
+ static {
+ final Map<Integer, Integer> map = new ArrayMap<>();
+ map.put(KEYCODE_CTRL_LEFT, META_CTRL_LEFT_ON | META_CTRL_ON);
+ map.put(KEYCODE_CTRL_RIGHT, META_CTRL_RIGHT_ON | META_CTRL_ON);
+ map.put(KEYCODE_ALT_LEFT, META_ALT_LEFT_ON | META_ALT_ON);
+ map.put(KEYCODE_ALT_RIGHT, META_ALT_RIGHT_ON | META_ALT_ON);
+ map.put(KEYCODE_SHIFT_LEFT, META_SHIFT_LEFT_ON | META_SHIFT_ON);
+ map.put(KEYCODE_SHIFT_RIGHT, META_SHIFT_RIGHT_ON | META_SHIFT_ON);
+ map.put(KEYCODE_META_LEFT, META_META_LEFT_ON | META_META_ON);
+ map.put(KEYCODE_META_RIGHT, META_META_RIGHT_ON | META_META_ON);
+
+ MODIFIER = unmodifiableMap(map);
+ }
+
+ @Before
+ public void setUp() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ mPhoneWindowManager = new TestPhoneWindowManager(mContext);
+ }
+
+ @After
+ public void tearDown() {
+ mPhoneWindowManager.tearDown();
+ }
+
+ void sendKeyCombination(int[] keyCodes, long duration) {
+ final long downTime = SystemClock.uptimeMillis();
+ final int count = keyCodes.length;
+ final KeyEvent[] events = new KeyEvent[count];
+ int metaState = 0;
+ for (int i = 0; i < count; i++) {
+ final int keyCode = keyCodes[i];
+ final KeyEvent event = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode,
+ 0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
+ 0 /*flags*/, InputDevice.SOURCE_KEYBOARD);
+ event.setDisplayId(DEFAULT_DISPLAY);
+ events[i] = event;
+ // The order is important here, metaState could be updated and applied to the next key.
+ metaState |= MODIFIER.getOrDefault(keyCode, 0);
+ }
+
+ for (KeyEvent event: events) {
+ interceptKey(event);
+ }
+
+ try {
+ Thread.sleep(duration);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ for (KeyEvent event: events) {
+ final long eventTime = SystemClock.uptimeMillis();
+ final int keyCode = event.getKeyCode();
+ final KeyEvent upEvent = new KeyEvent(downTime, eventTime, KeyEvent.ACTION_UP, keyCode,
+ 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, 0 /*flags*/,
+ InputDevice.SOURCE_KEYBOARD);
+ interceptKey(upEvent);
+ metaState &= ~MODIFIER.getOrDefault(keyCode, 0);
+ }
+ }
+
+ void sendKey(int keyCode) {
+ sendKey(keyCode, false);
+ }
+
+ void sendKey(int keyCode, boolean longPress) {
+ final long downTime = SystemClock.uptimeMillis();
+ final KeyEvent event = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, keyCode,
+ 0 /*repeat*/, 0 /*metaState*/, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
+ 0 /*flags*/, InputDevice.SOURCE_KEYBOARD);
+ event.setDisplayId(DEFAULT_DISPLAY);
+ interceptKey(event);
+
+ if (longPress) {
+ final long nextDownTime = downTime + ViewConfiguration.getLongPressTimeout();
+ final KeyEvent nextDownevent = new KeyEvent(downTime, nextDownTime,
+ KeyEvent.ACTION_DOWN, keyCode, 1 /*repeat*/, 0 /*metaState*/,
+ KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
+ KeyEvent.FLAG_LONG_PRESS /*flags*/, InputDevice.SOURCE_KEYBOARD);
+ interceptKey(nextDownevent);
+ }
+
+ final long eventTime = longPress
+ ? SystemClock.uptimeMillis() + ViewConfiguration.getLongPressTimeout()
+ : SystemClock.uptimeMillis();
+ final KeyEvent upEvent = new KeyEvent(downTime, eventTime, KeyEvent.ACTION_UP, keyCode,
+ 0 /*repeat*/, 0 /*metaState*/, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
+ 0 /*flags*/, InputDevice.SOURCE_KEYBOARD);
+ interceptKey(upEvent);
+ }
+
+ private void interceptKey(KeyEvent keyEvent) {
+ int actions = mPhoneWindowManager.interceptKeyBeforeQueueing(keyEvent);
+ if ((actions & ACTION_PASS_TO_USER) != 0) {
+ if (0 == mPhoneWindowManager.interceptKeyBeforeDispatching(keyEvent)) {
+ mPhoneWindowManager.dispatchUnhandledKey(keyEvent);
+ }
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
new file mode 100644
index 0000000..0320efe
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE;
+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.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;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyString;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
+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.LONG_PRESS_POWER_GO_TO_VOICE_ASSIST;
+import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_NOTHING;
+import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_SHUT_OFF;
+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.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.withSettings;
+
+import android.app.ActivityManagerInternal;
+import android.app.AppOpsManager;
+import android.app.NotificationManager;
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.input.InputManagerInternal;
+import android.media.AudioManagerInternal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.PowerManager;
+import android.os.PowerManagerInternal;
+import android.os.Vibrator;
+import android.service.dreams.DreamManagerInternal;
+import android.telecom.TelecomManager;
+import android.view.Display;
+import android.view.KeyEvent;
+import android.view.autofill.AutofillManagerInternal;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.internal.accessibility.AccessibilityShortcutController;
+import com.android.server.GestureLauncherService;
+import com.android.server.LocalServices;
+import com.android.server.vr.VrManagerInternal;
+import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.DisplayPolicy;
+import com.android.server.wm.DisplayRotation;
+import com.android.server.wm.WindowManagerInternal;
+
+import org.mockito.Mock;
+import org.mockito.MockSettings;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+class TestPhoneWindowManager {
+ private static final long SHORTCUT_KEY_DELAY_MILLIS = 150;
+
+ private PhoneWindowManager mPhoneWindowManager;
+ private Context mContext;
+
+ @Mock private WindowManagerInternal mWindowManagerInternal;
+ @Mock private ActivityManagerInternal mActivityManagerInternal;
+ @Mock private ActivityTaskManagerInternal mActivityTaskManagerInternal;
+ @Mock private InputManagerInternal mInputManagerInternal;
+ @Mock private DreamManagerInternal mDreamManagerInternal;
+ @Mock private PowerManagerInternal mPowerManagerInternal;
+ @Mock private DisplayManagerInternal mDisplayManagerInternal;
+ @Mock private AppOpsManager mAppOpsManager;
+ @Mock private DisplayManager mDisplayManager;
+ @Mock private PackageManager mPackageManager;
+ @Mock private TelecomManager mTelecomManager;
+ @Mock private NotificationManager mNotificationManager;
+ @Mock private Vibrator mVibrator;
+ @Mock private PowerManager mPowerManager;
+ @Mock private WindowManagerPolicy.WindowManagerFuncs mWindowManagerFuncsImpl;
+ @Mock private AudioManagerInternal mAudioManagerInternal;
+ @Mock private SearchManager mSearchManager;
+
+ @Mock private Display mDisplay;
+ @Mock private DisplayRotation mDisplayRotation;
+ @Mock private DisplayPolicy mDisplayPolicy;
+ @Mock private WindowManagerPolicy.ScreenOnListener mScreenOnListener;
+ @Mock private GestureLauncherService mGestureLauncherService;
+ @Mock private GlobalActions mGlobalActions;
+ @Mock private AccessibilityShortcutController mAccessibilityShortcutController;
+
+ private StaticMockitoSession mMockitoSession;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+
+ private class TestInjector extends PhoneWindowManager.Injector {
+ TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) {
+ super(context, funcs);
+ }
+
+ AccessibilityShortcutController getAccessibilityShortcutController(
+ Context context, Handler handler, int initialUserId) {
+ return mAccessibilityShortcutController;
+ }
+
+ GlobalActions getGlobalActions() {
+ return mGlobalActions;
+ }
+ }
+
+ TestPhoneWindowManager(Context context) {
+ MockitoAnnotations.initMocks(this);
+ mHandlerThread = new HandlerThread("fake window manager");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mHandler.runWithScissors(()-> setUp(context), 0 /* timeout */);
+ }
+
+ private void setUp(Context context) {
+ mPhoneWindowManager = spy(new PhoneWindowManager());
+ mContext = spy(context);
+
+ // Use stubOnly() to reduce memory usage if it doesn't need verification.
+ final MockSettings spyStubOnly = withSettings().stubOnly()
+ .defaultAnswer(CALLS_REAL_METHODS);
+ // Return mocked services: LocalServices.getService
+ mMockitoSession = mockitoSession()
+ .mockStatic(LocalServices.class, spyStubOnly)
+ .startMocking();
+
+ doReturn(mWindowManagerInternal).when(
+ () -> LocalServices.getService(eq(WindowManagerInternal.class)));
+ doReturn(mActivityManagerInternal).when(
+ () -> LocalServices.getService(eq(ActivityManagerInternal.class)));
+ doReturn(mActivityTaskManagerInternal).when(
+ () -> LocalServices.getService(eq(ActivityTaskManagerInternal.class)));
+ doReturn(mInputManagerInternal).when(
+ () -> LocalServices.getService(eq(InputManagerInternal.class)));
+ doReturn(mDreamManagerInternal).when(
+ () -> LocalServices.getService(eq(DreamManagerInternal.class)));
+ doReturn(mPowerManagerInternal).when(
+ () -> LocalServices.getService(eq(PowerManagerInternal.class)));
+ doReturn(mDisplayManagerInternal).when(
+ () -> LocalServices.getService(eq(DisplayManagerInternal.class)));
+ doReturn(mGestureLauncherService).when(
+ () -> LocalServices.getService(eq(GestureLauncherService.class)));
+ doReturn(null).when(() -> LocalServices.getService(eq(VrManagerInternal.class)));
+ doReturn(null).when(() -> LocalServices.getService(eq(AutofillManagerInternal.class)));
+
+ doReturn(mAppOpsManager).when(mContext).getSystemService(eq(AppOpsManager.class));
+ doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(false).when(mPackageManager).hasSystemFeature(any());
+ try {
+ doThrow(new PackageManager.NameNotFoundException("test")).when(mPackageManager)
+ .getActivityInfo(any(), anyInt());
+ doReturn(new String[] { "testPackage" }).when(mPackageManager)
+ .canonicalToCurrentPackageNames(any());
+ } catch (PackageManager.NameNotFoundException ignored) { }
+
+ doReturn(mTelecomManager).when(mContext).getSystemService(eq(Context.TELECOM_SERVICE));
+ doReturn(mNotificationManager).when(mContext)
+ .getSystemService(eq(NotificationManager.class));
+ doReturn(mVibrator).when(mContext).getSystemService(eq(Context.VIBRATOR_SERVICE));
+
+ final PowerManager.WakeLock wakeLock = mock(PowerManager.WakeLock.class);
+ doReturn(wakeLock).when(mPowerManager).newWakeLock(anyInt(), anyString());
+ doReturn(mPowerManager).when(mContext).getSystemService(eq(Context.POWER_SERVICE));
+ doReturn(true).when(mPowerManager).isInteractive();
+
+ doReturn(mDisplay).when(mDisplayManager).getDisplay(eq(DEFAULT_DISPLAY));
+ doReturn(STATE_ON).when(mDisplay).getState();
+ doReturn(true).when(mDisplayPolicy).isAwake();
+ doNothing().when(mDisplayPolicy).takeScreenshot(anyInt(), anyInt());
+ doReturn(mDisplayPolicy).when(mDisplayRotation).getDisplayPolicy();
+ doReturn(mScreenOnListener).when(mDisplayPolicy).getScreenOnListener();
+ mPhoneWindowManager.setDefaultDisplay(new WindowManagerPolicy.DisplayContentInfo() {
+ @Override
+ public DisplayRotation getDisplayRotation() {
+ return mDisplayRotation;
+ }
+ @Override
+ public Display getDisplay() {
+ return mDisplay;
+ }
+ });
+
+ doNothing().when(mPhoneWindowManager).initializeHdmiState();
+ doNothing().when(mPhoneWindowManager).updateSettings();
+ doNothing().when(mPhoneWindowManager).screenTurningOn(anyInt(), any());
+ doNothing().when(mPhoneWindowManager).screenTurnedOn(anyInt());
+ doNothing().when(mPhoneWindowManager).startedWakingUp(anyInt());
+ doNothing().when(mPhoneWindowManager).finishedWakingUp(anyInt());
+
+ mPhoneWindowManager.init(new TestInjector(mContext, mWindowManagerFuncsImpl));
+ mPhoneWindowManager.systemReady();
+ mPhoneWindowManager.systemBooted();
+
+ overrideLaunchAccessibility();
+ }
+
+ void tearDown() {
+ mHandlerThread.quitSafely();
+ mMockitoSession.finishMocking();
+ }
+
+ // Override accessibility setting and perform function.
+ private void overrideLaunchAccessibility() {
+ doReturn(true).when(mAccessibilityShortcutController)
+ .isAccessibilityShortcutAvailable(anyBoolean());
+ doNothing().when(mAccessibilityShortcutController).performAccessibilityShortcut();
+ }
+
+ int interceptKeyBeforeQueueing(KeyEvent event) {
+ return mPhoneWindowManager.interceptKeyBeforeQueueing(event, FLAG_INTERACTIVE);
+ }
+
+ long interceptKeyBeforeDispatching(KeyEvent event) {
+ return mPhoneWindowManager.interceptKeyBeforeDispatching(null /*focusedToken*/,
+ event, FLAG_INTERACTIVE);
+ }
+
+ void dispatchUnhandledKey(KeyEvent event) {
+ mPhoneWindowManager.dispatchUnhandledKey(null /*focusedToken*/, event, FLAG_INTERACTIVE);
+ }
+
+ void waitForIdle() {
+ mHandler.runWithScissors(() -> { }, 0 /* timeout */);
+ }
+
+ /**
+ * Below functions will override the setting or the policy behavior.
+ */
+ void overridePowerVolumeUp(int behavior) {
+ mPhoneWindowManager.mPowerVolUpBehavior = behavior;
+
+ // override mRingerToggleChord as mute so we could trigger the behavior.
+ if (behavior == POWER_VOLUME_UP_BEHAVIOR_MUTE) {
+ mPhoneWindowManager.mRingerToggleChord = VOLUME_HUSH_MUTE;
+ doReturn(mAudioManagerInternal).when(
+ () -> LocalServices.getService(eq(AudioManagerInternal.class)));
+ }
+ }
+
+ // Override assist perform function.
+ void overrideLongPressOnPower(int behavior) {
+ mPhoneWindowManager.mLongPressOnPowerBehavior = behavior;
+
+ switch (behavior) {
+ case LONG_PRESS_POWER_NOTHING:
+ case LONG_PRESS_POWER_GLOBAL_ACTIONS:
+ case LONG_PRESS_POWER_SHUT_OFF:
+ case LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM:
+ case LONG_PRESS_POWER_GO_TO_VOICE_ASSIST:
+ break;
+ case LONG_PRESS_POWER_ASSISTANT:
+ doNothing().when(mPhoneWindowManager).sendCloseSystemWindows();
+ doReturn(true).when(mPhoneWindowManager).isUserSetupComplete();
+ doReturn(mContext).when(mContext).createContextAsUser(any(), anyInt());
+ doReturn(mSearchManager).when(mContext)
+ .getSystemService(eq(Context.SEARCH_SERVICE));
+ mPhoneWindowManager.mLongPressOnPowerAssistantTimeoutMs = 500;
+ break;
+ }
+ }
+
+ void overrideDisplayState(int state) {
+ doReturn(state).when(mDisplay).getState();
+ Mockito.reset(mPowerManager);
+ }
+
+ /**
+ * Below functions will check the policy behavior could be invoked.
+ */
+ void assertTakeScreenshotCalled() {
+ waitForIdle();
+ verify(mDisplayPolicy, timeout(SHORTCUT_KEY_DELAY_MILLIS))
+ .takeScreenshot(anyInt(), anyInt());
+ }
+
+ void assertShowGlobalActionsCalled() {
+ waitForIdle();
+ verify(mPhoneWindowManager).showGlobalActions();
+ verify(mGlobalActions, timeout(SHORTCUT_KEY_DELAY_MILLIS))
+ .showDialog(anyBoolean(), anyBoolean());
+ verify(mPowerManager, timeout(SHORTCUT_KEY_DELAY_MILLIS))
+ .userActivity(anyLong(), anyBoolean());
+ }
+
+ void assertVolumeMute() {
+ waitForIdle();
+ verify(mAudioManagerInternal, timeout(SHORTCUT_KEY_DELAY_MILLIS))
+ .silenceRingerModeInternal(eq("volume_hush"));
+ }
+
+ void assertAccessibilityKeychordCalled() {
+ waitForIdle();
+ verify(mAccessibilityShortcutController,
+ timeout(SHORTCUT_KEY_DELAY_MILLIS)).performAccessibilityShortcut();
+ }
+
+ void assertPowerSleep() {
+ waitForIdle();
+ verify(mPowerManager,
+ timeout(SHORTCUT_KEY_DELAY_MILLIS)).goToSleep(anyLong(), anyInt(), anyInt());
+ }
+
+ void assertPowerWakeUp() {
+ waitForIdle();
+ verify(mPowerManager,
+ timeout(SHORTCUT_KEY_DELAY_MILLIS)).wakeUp(anyLong(), anyInt(), anyString());
+ }
+
+ void assertNoPowerSleep() {
+ waitForIdle();
+ verify(mPowerManager, never()).goToSleep(anyLong(), anyInt(), anyInt());
+ }
+
+ void assertCameraLaunch() {
+ waitForIdle();
+ // GestureLauncherService should receive interceptPowerKeyDown twice.
+ verify(mGestureLauncherService, times(2))
+ .interceptPowerKeyDown(any(), anyBoolean(), any());
+ }
+
+ void assertAssistLaunch() {
+ waitForIdle();
+ verify(mSearchManager, timeout(SHORTCUT_KEY_DELAY_MILLIS)).launchAssist(any());
+ }
+}