diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 5816e984704eb..f843af853161d 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 android.view.autofill.AutofillManagerInternal; 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { } 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 0000000000000..62875e5863b32 --- /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 99% 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 cf571815b4cb1..4c698571db16e 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 @@ import java.util.concurrent.TimeUnit; * 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 @@ public class KeyCombinationTests { 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 0000000000000..7c6fd05bcd181 --- /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 0000000000000..6368f4748b266 --- /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 MODIFIER; + static { + final Map 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 0000000000000..0320efee62f42 --- /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()); + } +}