Merge "Add new power button behavior that asks keyguard to show the hub" into main
diff --git a/core/java/android/service/dreams/DreamManagerInternal.java b/core/java/android/service/dreams/DreamManagerInternal.java
index e9bb28c..ad022c5 100644
--- a/core/java/android/service/dreams/DreamManagerInternal.java
+++ b/core/java/android/service/dreams/DreamManagerInternal.java
@@ -60,6 +60,12 @@
     public abstract boolean canStartDreaming(boolean isScreenOn);
 
     /**
+     * Whether or not the device is currently in the user's "when to dream" state, ex.
+     * docked & charging.
+     */
+    public abstract boolean dreamConditionActive();
+
+    /**
      * Register a {@link DreamManagerStateListener}, which will be called when there are changes to
      * dream state.
      *
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 9f731fe..8db94a4 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1214,6 +1214,9 @@
             3 - Really go to sleep and go home (don't doze)
             4 - Go to home
             5 - Dismiss IME if shown. Otherwise go to home
+            6 - Lock if keyguard enabled or go to sleep (doze)
+            7 - Dream if possible or go to sleep (doze)
+            8 - Go to glanceable hub or dream if possible, or sleep if neither is available (doze)
     -->
     <integer name="config_shortPressOnPowerBehavior">1</integer>
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index c0e266f..4c6a1ba 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -177,7 +177,7 @@
         VALIDATORS.put(Global.REQUIRE_PASSWORD_TO_DECRYPT, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.DEVICE_DEMO_MODE, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.AWARE_ALLOWED, BOOLEAN_VALIDATOR);
-        VALIDATORS.put(Global.POWER_BUTTON_SHORT_PRESS, new InclusiveIntegerRangeValidator(0, 7));
+        VALIDATORS.put(Global.POWER_BUTTON_SHORT_PRESS, new InclusiveIntegerRangeValidator(0, 8));
         VALIDATORS.put(Global.POWER_BUTTON_DOUBLE_PRESS, new InclusiveIntegerRangeValidator(0, 3));
         VALIDATORS.put(Global.POWER_BUTTON_TRIPLE_PRESS, new InclusiveIntegerRangeValidator(0, 3));
         VALIDATORS.put(Global.POWER_BUTTON_LONG_PRESS, new InclusiveIntegerRangeValidator(0, 5));
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
index 11b7e9d..a7c078f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
@@ -47,7 +47,7 @@
 constructor(
     configurationInteractor: ConfigurationInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    fromGlanceableHubTransitionInteractor: GlanceableHubToDreamingTransitionViewModel,
+    fromGlanceableHubTransitionViewModel: GlanceableHubToDreamingTransitionViewModel,
     toGlanceableHubTransitionViewModel: DreamingToGlanceableHubTransitionViewModel,
     private val toLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
     private val fromDreamingTransitionInteractor: FromDreamingTransitionInteractor,
@@ -74,7 +74,7 @@
     val dreamOverlayTranslationX: Flow<Float> =
         merge(
                 toGlanceableHubTransitionViewModel.dreamOverlayTranslationX,
-                fromGlanceableHubTransitionInteractor.dreamOverlayTranslationX,
+                fromGlanceableHubTransitionViewModel.dreamOverlayTranslationX,
             )
             .distinctUntilChanged()
 
@@ -97,7 +97,7 @@
         merge(
                 toLockscreenTransitionViewModel.dreamOverlayAlpha,
                 toGlanceableHubTransitionViewModel.dreamOverlayAlpha,
-                fromGlanceableHubTransitionInteractor.dreamOverlayAlpha,
+                fromGlanceableHubTransitionViewModel.dreamOverlayAlpha,
             )
             .distinctUntilChanged()
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 372fdca..efa9c21 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -118,6 +118,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.policy.IKeyguardExitCallback;
+import com.android.internal.policy.IKeyguardService;
 import com.android.internal.policy.IKeyguardStateCallback;
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.statusbar.IStatusBarService;
@@ -139,6 +140,7 @@
 import com.android.systemui.animation.TransitionAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingCollector;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
 import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -256,6 +258,15 @@
     private static final String DELAYED_LOCK_PROFILE_ACTION =
             "com.android.internal.policy.impl.PhoneWindowManager.DELAYED_LOCK";
 
+    /**
+     * String extra key passed in the bundle of {@link IKeyguardService#doKeyguardTimeout(Bundle)}
+     * if the value is {@code true}, indicates to keyguard that the device should show the
+     * glanceable hub upon locking. If the hub is already visible, the device should go to sleep.
+     *
+     * Mirrored from PhoneWindowManager which sends the extra.
+     */
+    public static final String EXTRA_TRIGGER_HUB = "extra_trigger_hub";
+
     private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
 
     // used for handler messages
@@ -354,6 +365,7 @@
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController;
     private final Lazy<ShadeController> mShadeController;
+    private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor;
     /*
      * Records the user id on request to go away, for validation when WM calls back to start the
      * exit animation.
@@ -1556,6 +1568,7 @@
             SelectedUserInteractor selectedUserInteractor,
             KeyguardInteractor keyguardInteractor,
             KeyguardTransitionBootInteractor transitionBootInteractor,
+            Lazy<CommunalSceneInteractor> communalSceneInteractor,
             WindowManagerOcclusionManager wmOcclusionManager) {
         mContext = context;
         mUserTracker = userTracker;
@@ -1597,6 +1610,7 @@
         mSelectedUserInteractor = selectedUserInteractor;
         mKeyguardInteractor = keyguardInteractor;
         mTransitionBootInteractor = transitionBootInteractor;
+        mCommunalSceneInteractor = communalSceneInteractor;
 
         mStatusBarStateController = statusBarStateController;
         statusBarStateController.addCallback(this);
@@ -2411,6 +2425,13 @@
      * Enable the keyguard if the settings are appropriate.
      */
     private void doKeyguardLocked(Bundle options) {
+        // If the power button behavior requests to open the glanceable hub.
+        if (options != null && options.getBoolean(EXTRA_TRIGGER_HUB)) {
+            // Set the hub to show immediately when the SysUI window shows, then continue to lock
+            // the device.
+            mCommunalSceneInteractor.get().showHubFromPowerButton();
+        }
+
         int currentUserId = mSelectedUserInteractor.getSelectedUserId();
         if (options != null && options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK) != null) {
             LockNowCallback callback = new LockNowCallback(currentUserId,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 908413d..97ad2d7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -41,6 +41,7 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.classifier.FalsingModule;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
 import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -180,6 +181,7 @@
             SelectedUserInteractor selectedUserInteractor,
             KeyguardInteractor keyguardInteractor,
             KeyguardTransitionBootInteractor transitionBootInteractor,
+            Lazy<CommunalSceneInteractor> communalSceneInteractor,
             WindowManagerOcclusionManager windowManagerOcclusionManager) {
         return new KeyguardViewMediator(
                 context,
@@ -231,6 +233,7 @@
                 selectedUserInteractor,
                 keyguardInteractor,
                 transitionBootInteractor,
+                communalSceneInteractor,
                 windowManagerOcclusionManager);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 0c9213c..e8054c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -1502,6 +1502,7 @@
                 mSelectedUserInteractor,
                 mKeyguardInteractor,
                 mKeyguardTransitionBootInteractor,
+                mKosmos::getCommunalSceneInteractor,
                 mock(WindowManagerOcclusionManager.class));
         mViewMediator.mUserChangedCallback = mUserTrackerCallback;
         mViewMediator.start();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
new file mode 100644
index 0000000..86f7966
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2025 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.systemui.keyguard
+
+import android.app.IActivityTaskManager
+import android.internal.statusbar.statusBarService
+import android.os.Bundle
+import android.os.PowerManager
+import android.os.powerManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.uiEventLogger
+import com.android.internal.widget.lockPatternUtils
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.keyguardUnlockAnimationController
+import com.android.keyguard.mediator.ScreenOnCoordinator
+import com.android.keyguard.trustManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.activityTransitionAnimator
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.classifier.falsingCollector
+import com.android.systemui.communal.data.repository.communalSceneRepository
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.dreams.DreamOverlayStateController
+import com.android.systemui.dreams.ui.viewmodel.dreamViewModel
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.flags.systemPropertiesHelper
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionBootInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundScope
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.log.sessionTracker
+import com.android.systemui.navigationbar.navigationModeController
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.process.processWrapper
+import com.android.systemui.settings.userTracker
+import com.android.systemui.shade.shadeController
+import com.android.systemui.statusbar.notificationShadeDepthController
+import com.android.systemui.statusbar.notificationShadeWindowController
+import com.android.systemui.statusbar.phone.dozeParameters
+import com.android.systemui.statusbar.phone.screenOffAnimationController
+import com.android.systemui.statusbar.phone.scrimController
+import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
+import com.android.systemui.statusbar.policy.keyguardStateController
+import com.android.systemui.statusbar.policy.userSwitcherController
+import com.android.systemui.testKosmos
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.DeviceConfigProxy
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.util.time.systemClock
+import com.android.systemui.wallpapers.data.repository.wallpaperRepository
+import com.android.wm.shell.keyguard.KeyguardTransitions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+/** Kotlin version of KeyguardViewMediatorTest to allow for coroutine testing. */
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class KeyguardViewMediatorTestKt : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().useUnconfinedTestDispatcher().also {
+            it.powerManager =
+                mock<PowerManager> {
+                    on { newWakeLock(anyInt(), any()) } doReturn mock<PowerManager.WakeLock>()
+                }
+        }
+
+    private lateinit var testableLooper: TestableLooper
+
+    private val Kosmos.underTest by
+        Kosmos.Fixture {
+            KeyguardViewMediator(
+                mContext,
+                uiEventLogger,
+                sessionTracker,
+                userTracker,
+                falsingCollector,
+                lockPatternUtils,
+                broadcastDispatcher,
+                { statusBarKeyguardViewManager },
+                dismissCallbackRegistry,
+                mock<KeyguardUpdateMonitor>(),
+                dumpManager,
+                fakeExecutor,
+                powerManager,
+                trustManager,
+                userSwitcherController,
+                DeviceConfigProxy(),
+                navigationModeController,
+                keyguardDisplayManager,
+                dozeParameters,
+                statusBarStateController,
+                keyguardStateController,
+                { keyguardUnlockAnimationController },
+                screenOffAnimationController,
+                { notificationShadeDepthController },
+                mock<ScreenOnCoordinator>(),
+                mock<KeyguardTransitions>(),
+                interactionJankMonitor,
+                mock<DreamOverlayStateController>(),
+                JavaAdapter(backgroundScope),
+                wallpaperRepository,
+                { shadeController },
+                { notificationShadeWindowController },
+                { activityTransitionAnimator },
+                { scrimController },
+                mock<IActivityTaskManager>(),
+                statusBarService,
+                featureFlagsClassic,
+                fakeSettings,
+                fakeSettings,
+                systemClock,
+                processWrapper,
+                testDispatcher,
+                { dreamViewModel },
+                { communalTransitionViewModel },
+                systemPropertiesHelper,
+                { mock<WindowManagerLockscreenVisibilityManager>() },
+                selectedUserInteractor,
+                keyguardInteractor,
+                keyguardTransitionBootInteractor,
+                { communalSceneInteractor },
+                mock<WindowManagerOcclusionManager>(),
+            )
+        }
+
+    @Before
+    fun setUp() {
+        testableLooper = TestableLooper.get(this)
+    }
+
+    @Test
+    fun doKeyguardTimeout_changesCommunalScene() =
+        kosmos.runTest {
+            // doKeyguardTimeout message received.
+            val timeoutOptions = Bundle()
+            timeoutOptions.putBoolean(KeyguardViewMediator.EXTRA_TRIGGER_HUB, true)
+            underTest.doKeyguardTimeout(timeoutOptions)
+            testableLooper.processAllMessages()
+
+            // Hub scene is triggered.
+            assertThat(communalSceneRepository.currentScene.value)
+                .isEqualTo(CommunalScenes.Communal)
+        }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModelKosmos.kt
new file mode 100644
index 0000000..de36493
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModelKosmos.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2025 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.systemui.dreams.ui.viewmodel
+
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.keyguard.domain.interactor.fromDreamingTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.viewmodel.dreamingToGlanceableHubTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToDreamingTransitionViewModel
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.settings.userTracker
+
+val Kosmos.dreamViewModel by
+    Kosmos.Fixture {
+        DreamViewModel(
+            communalInteractor = communalInteractor,
+            communalSettingsInteractor = communalSettingsInteractor,
+            configurationInteractor = configurationInteractor,
+            keyguardTransitionInteractor = keyguardTransitionInteractor,
+            fromGlanceableHubTransitionViewModel = glanceableHubToDreamingTransitionViewModel,
+            toGlanceableHubTransitionViewModel = dreamingToGlanceableHubTransitionViewModel,
+            toLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
+            fromDreamingTransitionInteractor = fromDreamingTransitionInteractor,
+            keyguardUpdateMonitor = keyguardUpdateMonitor,
+            userTracker = userTracker,
+            dumpManager = dumpManager,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/KeyguardDisplayManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/KeyguardDisplayManagerKosmos.kt
new file mode 100644
index 0000000..1e46d48
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/KeyguardDisplayManagerKosmos.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025 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.systemui.keyguard
+
+import android.content.testableContext
+import com.android.keyguard.ConnectedDisplayKeyguardPresentation
+import com.android.keyguard.KeyguardDisplayManager
+import com.android.keyguard.KeyguardDisplayManager.DeviceStateHelper
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.navigationbar.navigationBarController
+import com.android.systemui.settings.displayTracker
+import com.android.systemui.shade.data.repository.shadeDisplaysRepository
+import com.android.systemui.statusbar.policy.keyguardStateController
+import org.mockito.kotlin.mock
+
+var Kosmos.keyguardDisplayManager by
+    Kosmos.Fixture {
+        KeyguardDisplayManager(
+            testableContext,
+            { navigationBarController },
+            displayTracker,
+            fakeExecutor,
+            fakeExecutor,
+            mock<DeviceStateHelper>(),
+            keyguardStateController,
+            mock<ConnectedDisplayKeyguardPresentation.Factory>(),
+            { shadeDisplaysRepository },
+            applicationCoroutineScope,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/UserSwitcherControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/UserSwitcherControllerKosmos.kt
new file mode 100644
index 0000000..51168d6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/UserSwitcherControllerKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 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.systemui.statusbar.policy
+
+import android.content.applicationContext
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.user.domain.interactor.guestUserInteractor
+import com.android.systemui.user.domain.interactor.userSwitcherInteractor
+
+val Kosmos.userSwitcherController by Fixture {
+    UserSwitcherController(
+        applicationContext = applicationContext,
+        userSwitcherInteractorLazy = { userSwitcherInteractor },
+        guestUserInteractorLazy = { guestUserInteractor },
+        keyguardInteractorLazy = { keyguardInteractor },
+        activityStarter = activityStarter,
+    )
+}
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 67b1ec3..7e8bb28 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -495,6 +495,34 @@
         }
     }
 
+    @VisibleForTesting
+    boolean dreamConditionActiveInternal() {
+        synchronized (mLock) {
+            return dreamConditionActiveInternalLocked();
+        }
+    }
+
+    private boolean dreamConditionActiveInternalLocked() {
+        if ((mWhenToDream & DREAM_ON_CHARGE) == DREAM_ON_CHARGE) {
+            return mIsCharging;
+        }
+
+        if ((mWhenToDream & DREAM_ON_DOCK) == DREAM_ON_DOCK) {
+            return mIsDocked;
+        }
+
+        if ((mWhenToDream & DREAM_ON_POSTURED) == DREAM_ON_POSTURED) {
+            return mIsPostured;
+        }
+
+        return false;
+    }
+
+    @VisibleForTesting
+    boolean dreamsEnabled() {
+        return mDreamsEnabledSetting;
+    }
+
     /** Whether dreaming can start given user settings and the current dock/charge state. */
     private boolean canStartDreamingInternal(boolean isScreenOn) {
         synchronized (mLock) {
@@ -524,19 +552,9 @@
                 return false;
             }
 
-            if ((mWhenToDream & DREAM_ON_CHARGE) == DREAM_ON_CHARGE) {
-                return mIsCharging;
-            }
-
-            if ((mWhenToDream & DREAM_ON_DOCK) == DREAM_ON_DOCK) {
-                return mIsDocked;
-            }
-
-            if ((mWhenToDream & DREAM_ON_POSTURED) == DREAM_ON_POSTURED) {
-                return mIsPostured;
-            }
-
-            return false;
+            // All dream prerequisites fulfilled, check if device state matches "when to dream"
+            // setting.
+            return dreamConditionActiveInternalLocked();
         }
     }
 
@@ -674,7 +692,8 @@
         }
     }
 
-    private void setDevicePosturedInternal(boolean isPostured) {
+    @VisibleForTesting
+    void setDevicePosturedInternal(boolean isPostured) {
         Slog.d(TAG, "Device postured: " + isPostured);
         synchronized (mLock) {
             mIsPostured = isPostured;
@@ -1390,6 +1409,11 @@
         }
 
         @Override
+        public boolean dreamConditionActive() {
+            return dreamConditionActiveInternal();
+        }
+
+        @Override
         public void requestDream() {
             requestDreamInternal();
         }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 4860b7cd..0aaa0fe 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -231,6 +231,7 @@
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.internal.os.RoSystemProperties;
 import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.policy.IKeyguardService;
 import com.android.internal.policy.IShortcutService;
 import com.android.internal.policy.KeyInterceptionInfo;
 import com.android.internal.policy.LogDecelerateInterpolator;
@@ -309,6 +310,7 @@
     static final int SHORT_PRESS_POWER_CLOSE_IME_OR_GO_HOME = 5;
     static final int SHORT_PRESS_POWER_LOCK_OR_SLEEP = 6;
     static final int SHORT_PRESS_POWER_DREAM_OR_SLEEP = 7;
+    static final int SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP = 8;
 
     // must match: config_LongPressOnPowerBehavior in config.xml
     // The config value can be overridden using Settings.Global.POWER_BUTTON_LONG_PRESS
@@ -403,6 +405,13 @@
 
     public static final String TRACE_WAIT_FOR_ALL_WINDOWS_DRAWN_METHOD = "waitForAllWindowsDrawn";
 
+    /**
+     * String extra key passed in the bundle of {@link IKeyguardService#doKeyguardTimeout(Bundle)}
+     * if the value is {@code true}, indicates to keyguard that the device should show the
+     * glanceable hub upon locking. If the hub is already visible, the device should go to sleep.
+     */
+    public static final String EXTRA_TRIGGER_HUB = "extra_trigger_hub";
+
     private static final int POWER_BUTTON_SUPPRESSION_DELAY_DEFAULT_MILLIS = 800;
 
     /**
@@ -1154,7 +1163,8 @@
         }
     }
 
-    private void powerPress(long eventTime, int count, int displayId) {
+    @VisibleForTesting
+    void powerPress(long eventTime, int count, int displayId) {
         // SideFPS still needs to know about suppressed power buttons, in case it needs to block
         // an auth attempt.
         if (count == 1) {
@@ -1229,6 +1239,43 @@
                             () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
                     break;
                 }
+                case SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP: {
+                    // With this power button behavior, the following behavior is expected from each
+                    // system space on a power button short press:
+                    // - Unlocked: go to hub if available, dream if not, screen off if neither
+                    // - Lock screen, hub, or dream: go to screen off
+                    // - Screen off: go to hub if available, dream if not, lock screen if enabled,
+                    //               unlocked if lockscreen is disabled
+                    // TODO(b/394657933): consolidate policy into SysUI
+                    final boolean hubEnabled = Settings.Secure.getIntForUser(
+                            mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED,
+                            1, mCurrentUserId) == 1;
+
+                    if (mDreamManagerInternal.isDreaming() || isKeyguardShowing()) {
+                        // If the device is already dreaming or on keyguard, go to sleep.
+                        sleepDefaultDisplayFromPowerButton(eventTime, 0);
+                        break;
+                    }
+
+                    // Check isLockScreenDisabled to exclude NONE lock screen option, which cannot
+                    // show hub.
+                    boolean keyguardAvailable = !mLockPatternUtils.isLockScreenDisabled(
+                            mCurrentUserId);
+                    if (mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled
+                            && keyguardAvailable && mDreamManagerInternal.dreamConditionActive()) {
+                        // If the hub can be launched, send a message to keyguard.
+                        Bundle options = new Bundle();
+                        options.putBoolean(EXTRA_TRIGGER_HUB, true);
+                        lockNow(options);
+                    } else {
+                        // If the hub cannot be run, attempt to dream instead.
+                        attemptToDreamFromShortPowerButtonPress(
+                                /* isScreenOn */ true,
+                                /* noDreamAction */
+                                () -> sleepDefaultDisplayFromPowerButton(eventTime, 0));
+                    }
+                    break;
+                }
             }
         }
     }
@@ -1279,7 +1326,10 @@
      */
     private void attemptToDreamFromShortPowerButtonPress(
             boolean isScreenOn, Runnable noDreamAction) {
-        if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP) {
+        if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP
+                && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) {
+            // If the power button behavior isn't one that should be able to trigger the dream, give
+            // up.
             noDreamAction.run();
             return;
         }
@@ -5132,8 +5182,7 @@
     }
 
     /**
-     * Updates the occluded state of the Keyguard immediately via
-     * {@link com.android.internal.policy.IKeyguardService}.
+     * Updates the occluded state of the Keyguard immediately via {@link IKeyguardService}.
      *
      * @param isOccluded Whether the Keyguard is occluded by another window.
      * @return Whether the flags have changed and we have to redo the layout.
diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamManagerServiceMockingTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamManagerServiceMockingTest.java
deleted file mode 100644
index 992b853..0000000
--- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamManagerServiceMockingTest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2023 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.dreams;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManagerInternal;
-import android.content.ContextWrapper;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
-import android.os.PowerManagerInternal;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.Settings;
-
-import androidx.test.InstrumentationRegistry;
-
-import com.android.internal.util.test.LocalServiceKeeperRule;
-import com.android.server.SystemService;
-import com.android.server.testutils.TestHandler;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-/**
- * Collection of tests for exercising the {@link DreamManagerService} lifecycle.
- */
-public class DreamManagerServiceMockingTest {
-    private ContextWrapper mContextSpy;
-    private Resources mResourcesSpy;
-
-    @Mock
-    private ActivityManagerInternal mActivityManagerInternalMock;
-
-    @Mock
-    private PowerManagerInternal mPowerManagerInternalMock;
-
-    @Mock
-    private UserManager mUserManagerMock;
-
-    @Rule
-    public LocalServiceKeeperRule mLocalServiceKeeperRule = new LocalServiceKeeperRule();
-
-    private TestHandler mTestHandler;
-    private MockitoSession mMockitoSession;
-
-    @Before
-    public void setUp() throws Exception {
-        mTestHandler = new TestHandler(/* callback= */ null);
-        MockitoAnnotations.initMocks(this);
-        mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
-        mResourcesSpy = spy(mContextSpy.getResources());
-        when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
-
-        mLocalServiceKeeperRule.overrideLocalService(
-                ActivityManagerInternal.class, mActivityManagerInternalMock);
-        mLocalServiceKeeperRule.overrideLocalService(
-                PowerManagerInternal.class, mPowerManagerInternalMock);
-
-        when(mContextSpy.getSystemService(UserManager.class)).thenReturn(mUserManagerMock);
-        mMockitoSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.LENIENT)
-                .mockStatic(Settings.Secure.class)
-                .startMocking();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        mMockitoSession.finishMocking();
-    }
-
-    private DreamManagerService createService() {
-        return new DreamManagerService(mContextSpy, mTestHandler);
-    }
-
-    @Test
-    public void testSettingsQueryUserChange() {
-        final DreamManagerService service = createService();
-        final SystemService.TargetUser from =
-                new SystemService.TargetUser(mock(UserInfo.class));
-        final SystemService.TargetUser to =
-                new SystemService.TargetUser(mock(UserInfo.class));
-        service.onUserSwitching(from, to);
-        verify(() -> Settings.Secure.getIntForUser(any(),
-                eq(Settings.Secure.SCREENSAVER_ENABLED),
-                anyInt(),
-                eq(UserHandle.USER_CURRENT)));
-    }
-}
diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamManagerServiceTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamManagerServiceTest.java
new file mode 100644
index 0000000..4efc258
--- /dev/null
+++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamManagerServiceTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2023 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.dreams;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManagerInternal;
+import android.content.BroadcastReceiver;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.os.BatteryManager;
+import android.os.BatteryManagerInternal;
+import android.os.PowerManagerInternal;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.testing.TestableContext;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.LocalServiceKeeperRule;
+import com.android.server.SystemService;
+import com.android.server.input.InputManagerInternal;
+import com.android.server.testutils.TestHandler;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Test class for {@link DreamManagerService}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamManagerServiceTest {
+    private ContextWrapper mContextSpy;
+
+    @Mock
+    private ActivityManagerInternal mActivityManagerInternalMock;
+    @Mock
+    private BatteryManagerInternal mBatteryManagerInternal;
+
+    @Mock
+    private InputManagerInternal mInputManagerInternal;
+    @Mock
+    private PowerManagerInternal mPowerManagerInternalMock;
+
+    @Mock
+    private BatteryManager mBatteryManager;
+    @Mock
+    private UserManager mUserManagerMock;
+
+    @Rule
+    public LocalServiceKeeperRule mLocalServiceKeeperRule = new LocalServiceKeeperRule();
+
+
+    @Rule
+    public final TestableContext mContext = new TestableContext(
+            getInstrumentation().getContext());
+
+    private TestHandler mTestHandler;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestHandler = new TestHandler(/* callback= */ null);
+        MockitoAnnotations.initMocks(this);
+        mContextSpy = spy(mContext);
+
+        mLocalServiceKeeperRule.overrideLocalService(
+                ActivityManagerInternal.class, mActivityManagerInternalMock);
+        mLocalServiceKeeperRule.overrideLocalService(
+                BatteryManagerInternal.class, mBatteryManagerInternal);
+        mLocalServiceKeeperRule.overrideLocalService(
+                InputManagerInternal.class, mInputManagerInternal);
+        mLocalServiceKeeperRule.overrideLocalService(
+                PowerManagerInternal.class, mPowerManagerInternalMock);
+
+        when(mContextSpy.getSystemService(BatteryManager.class)).thenReturn(mBatteryManager);
+        when(mContextSpy.getSystemService(UserManager.class)).thenReturn(mUserManagerMock);
+    }
+
+    private DreamManagerService createService() {
+        return new DreamManagerService(mContextSpy, mTestHandler);
+    }
+
+    @Test
+    public void testSettingsQueryUserChange() {
+        // Enable dreams.
+        Settings.Secure.putIntForUser(mContextSpy.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ENABLED, 1,
+                UserHandle.USER_CURRENT);
+
+        // Initialize dream service so settings are read.
+        final DreamManagerService service = createService();
+        service.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+        // Dreams are enabled.
+        assertThat(service.dreamsEnabled()).isTrue();
+
+        // Disable dreams.
+        Settings.Secure.putIntForUser(mContextSpy.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ENABLED, 0,
+                UserHandle.USER_CURRENT);
+
+        // Switch users, dreams are disabled.
+        service.onUserSwitching(null, null);
+        assertThat(service.dreamsEnabled()).isFalse();
+    }
+
+    @Test
+    public void testDreamConditionActive_onDock() {
+        // Enable dreaming on dock.
+        Settings.Secure.putIntForUser(mContextSpy.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 1,
+                UserHandle.USER_CURRENT);
+
+        // Initialize service so settings are read.
+        final DreamManagerService service = createService();
+        service.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+        assertThat(service.dreamConditionActiveInternal()).isFalse();
+
+        // Dock event receiver is registered.
+        ArgumentCaptor<BroadcastReceiver> receiverCaptor = ArgumentCaptor.forClass(
+                BroadcastReceiver.class);
+        verify(mContextSpy).registerReceiver(receiverCaptor.capture(),
+                argThat((arg) -> arg.hasAction(Intent.ACTION_DOCK_EVENT)));
+
+        // Device is docked.
+        Intent dockIntent = new Intent(Intent.ACTION_DOCK_EVENT);
+        dockIntent.putExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_HE_DESK);
+        receiverCaptor.getValue().onReceive(null, dockIntent);
+
+        // Dream condition is active.
+        assertThat(service.dreamConditionActiveInternal()).isTrue();
+    }
+
+    @Test
+    public void testDreamConditionActive_postured() {
+        // Enable dreaming while postured.
+        Settings.Secure.putIntForUser(mContextSpy.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 0,
+                UserHandle.USER_CURRENT);
+        Settings.Secure.putIntForUser(mContextSpy.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, 1,
+                UserHandle.USER_CURRENT);
+
+        // Initialize service so settings are read.
+        final DreamManagerService service = createService();
+        service.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+        assertThat(service.dreamConditionActiveInternal()).isFalse();
+
+        // Device is postured.
+        service.setDevicePosturedInternal(true);
+
+        // Dream condition is active.
+        assertThat(service.dreamConditionActiveInternal()).isTrue();
+    }
+
+    @Test
+    public void testDreamConditionActive_charging() {
+        // Enable dreaming while charging only.
+        Settings.Secure.putIntForUser(mContextSpy.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1,
+                UserHandle.USER_CURRENT);
+
+        // Device is charging.
+        when(mBatteryManager.isCharging()).thenReturn(true);
+
+        // Initialize service so settings are read.
+        final DreamManagerService service = createService();
+        service.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+        // Dream condition is active.
+        assertThat(service.dreamConditionActiveInternal()).isTrue();
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index c73ce23..32a3b7f 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -16,6 +16,7 @@
 
 package com.android.server.policy;
 
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
@@ -33,6 +34,8 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.policy.PhoneWindowManager.EXTRA_TRIGGER_HUB;
+import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -47,13 +50,20 @@
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.hardware.input.InputManager;
+import android.os.Bundle;
 import android.os.PowerManager;
+import android.os.PowerManagerInternal;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+import android.service.dreams.DreamManagerInternal;
+import android.testing.TestableContext;
+import android.view.contentprotection.flags.Flags;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.server.LocalServices;
+import com.android.internal.util.test.LocalServiceKeeperRule;
+import com.android.server.input.InputManagerInternal;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.policy.keyguard.KeyguardServiceDelegate;
 import com.android.server.statusbar.StatusBarManagerInternal;
@@ -66,6 +76,9 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 /**
  * Test class for {@link PhoneWindowManager}.
@@ -76,28 +89,62 @@
 @Presubmit
 @SmallTest
 public class PhoneWindowManagerTests {
-
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
+    @Rule
+    public LocalServiceKeeperRule mLocalServiceKeeperRule = new LocalServiceKeeperRule();
+
+    @Rule
+    public final TestableContext mContext = spy(
+            new TestableContext(getInstrumentation().getContext()));
+
     PhoneWindowManager mPhoneWindowManager;
+    @Mock
     private ActivityTaskManagerInternal mAtmInternal;
+    @Mock
+    private DreamManagerInternal mDreamManagerInternal;
+    @Mock
+    private InputManagerInternal mInputManagerInternal;
+    @Mock
+    private PowerManagerInternal mPowerManagerInternal;
+    @Mock
     private StatusBarManagerInternal mStatusBarManagerInternal;
-    private Context mContext;
+    @Mock
+    private UserManagerInternal mUserManagerInternal;
+
+    @Mock
+    private PowerManager mPowerManager;
+    @Mock
+    private DisplayPolicy mDisplayPolicy;
+    @Mock
+    private KeyguardServiceDelegate mKeyguardServiceDelegate;
 
     @Before
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager);
+
         mPhoneWindowManager = spy(new PhoneWindowManager());
         spyOn(ActivityManager.getService());
-        mContext = getInstrumentation().getTargetContext();
-        spyOn(mContext);
-        mAtmInternal = mock(ActivityTaskManagerInternal.class);
-        LocalServices.addService(ActivityTaskManagerInternal.class, mAtmInternal);
+
+        mLocalServiceKeeperRule.overrideLocalService(ActivityTaskManagerInternal.class,
+                mAtmInternal);
         mPhoneWindowManager.mActivityTaskManagerInternal = mAtmInternal;
-        LocalServices.addService(WindowManagerInternal.class, mock(WindowManagerInternal.class));
-        mStatusBarManagerInternal = mock(StatusBarManagerInternal.class);
-        LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal);
-        mPhoneWindowManager.mKeyguardDelegate = mock(KeyguardServiceDelegate.class);
+        mLocalServiceKeeperRule.overrideLocalService(DreamManagerInternal.class,
+                mDreamManagerInternal);
+        mLocalServiceKeeperRule.overrideLocalService(InputManagerInternal.class,
+                mInputManagerInternal);
+        mLocalServiceKeeperRule.overrideLocalService(PowerManagerInternal.class,
+                mPowerManagerInternal);
+        mLocalServiceKeeperRule.overrideLocalService(StatusBarManagerInternal.class,
+                mStatusBarManagerInternal);
+        mLocalServiceKeeperRule.overrideLocalService(UserManagerInternal.class,
+                mUserManagerInternal);
+        mLocalServiceKeeperRule.overrideLocalService(WindowManagerInternal.class,
+                mock(WindowManagerInternal.class));
+
+        mPhoneWindowManager.mKeyguardDelegate = mKeyguardServiceDelegate;
         final InputManager im = mock(InputManager.class);
         doNothing().when(im).registerKeyGestureEventHandler(any());
         doReturn(im).when(mContext).getSystemService(eq(Context.INPUT_SERVICE));
@@ -107,9 +154,6 @@
     public void tearDown() {
         reset(ActivityManager.getService());
         reset(mContext);
-        LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
-        LocalServices.removeServiceForTest(WindowManagerInternal.class);
-        LocalServices.removeServiceForTest(StatusBarManagerInternal.class);
     }
 
     @Test
@@ -138,28 +182,20 @@
     public void testScreenTurnedOff() {
         doNothing().when(mPhoneWindowManager).updateSettings(any());
         doNothing().when(mPhoneWindowManager).initializeHdmiState();
-        final boolean[] isScreenTurnedOff = { false };
-        final DisplayPolicy displayPolicy = mock(DisplayPolicy.class);
-        doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff(
+        final boolean[] isScreenTurnedOff = {false};
+        doAnswer(invocation -> isScreenTurnedOff[0] = true).when(mDisplayPolicy).screenTurnedOff(
                 anyBoolean());
-        doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly();
-        doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully();
+        doAnswer(invocation -> !isScreenTurnedOff[0]).when(mDisplayPolicy).isScreenOnEarly();
+        doAnswer(invocation -> !isScreenTurnedOff[0]).when(mDisplayPolicy).isScreenOnFully();
 
-        mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy;
-        mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
-        final PowerManager pm = mock(PowerManager.class);
-        doReturn(true).when(pm).isInteractive();
-        doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE));
-
-        mContext.getMainThreadHandler().runWithScissors(() -> mPhoneWindowManager.init(
-                new PhoneWindowManager.Injector(mContext,
-                        mock(WindowManagerPolicy.WindowManagerFuncs.class))), 0);
+        when(mPowerManager.isInteractive()).thenReturn(true);
+        initPhoneWindowManager();
         assertThat(isScreenTurnedOff[0]).isFalse();
         assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
 
         // Skip sleep-token for non-sleep-screen-off.
         mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
-        verify(displayPolicy).screenTurnedOff(false /* acquireSleepToken */);
+        verify(mDisplayPolicy).screenTurnedOff(false /* acquireSleepToken */);
         assertThat(isScreenTurnedOff[0]).isTrue();
 
         // Apply sleep-token for sleep-screen-off.
@@ -167,7 +203,7 @@
         mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
         assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue();
         mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
-        verify(displayPolicy).screenTurnedOff(true /* acquireSleepToken */);
+        verify(mDisplayPolicy).screenTurnedOff(true /* acquireSleepToken */);
 
         mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
         assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
@@ -175,8 +211,7 @@
 
     @Test
     public void testCheckAddPermission_withoutAccessibilityOverlay_noAccessibilityAppOpLogged() {
-        mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
-                .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
         int[] outAppOp = new int[1];
         assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_WALLPAPER,
                 /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY));
@@ -185,8 +220,7 @@
 
     @Test
     public void testCheckAddPermission_withAccessibilityOverlay() {
-        mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
-                .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
         int[] outAppOp = new int[1];
         assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY,
                 /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY));
@@ -195,8 +229,7 @@
 
     @Test
     public void testCheckAddPermission_withAccessibilityOverlay_flagDisabled() {
-        mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
-                .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
+        mSetFlagsRule.disableFlags(Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED);
         int[] outAppOp = new int[1];
         assertEquals(ADD_OKAY, mPhoneWindowManager.checkAddPermission(TYPE_ACCESSIBILITY_OVERLAY,
                 /* isRoundedCornerOverlay= */ false, "test.pkg", outAppOp, DEFAULT_DISPLAY));
@@ -217,10 +250,107 @@
         verify(mStatusBarManagerInternal, never()).dismissKeyboardShortcutsMenu();
     }
 
+    @Test
+    public void powerPress_hubOrDreamOrSleep_goesToSleepFromDream() {
+        when(mDisplayPolicy.isAwake()).thenReturn(true);
+        initPhoneWindowManager();
+
+        // Set power button behavior.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.POWER_BUTTON_SHORT_PRESS, SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP);
+        mPhoneWindowManager.updateSettings(null);
+
+        // Device is dreaming.
+        when(mDreamManagerInternal.isDreaming()).thenReturn(true);
+
+        // Power button pressed.
+        int eventTime = 0;
+        mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+        // Device goes to sleep.
+        verify(mPowerManager).goToSleep(eventTime, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, 0);
+    }
+
+    @Test
+    public void powerPress_hubOrDreamOrSleep_hubAvailableLocks() {
+        when(mDisplayPolicy.isAwake()).thenReturn(true);
+        mContext.getTestablePermissions().setPermission(android.Manifest.permission.DEVICE_POWER,
+                PERMISSION_GRANTED);
+        initPhoneWindowManager();
+
+        // Set power button behavior.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.POWER_BUTTON_SHORT_PRESS, SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP);
+        mPhoneWindowManager.updateSettings(null);
+
+        // Set up hub prerequisites.
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                Settings.Secure.GLANCEABLE_HUB_ENABLED, 1);
+        when(mUserManagerInternal.isUserUnlocked(any(Integer.class))).thenReturn(true);
+        when(mDreamManagerInternal.dreamConditionActive()).thenReturn(true);
+
+        // Power button pressed.
+        int eventTime = 0;
+        mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+        // Lock requested with the proper bundle options.
+        ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mPhoneWindowManager).lockNow(bundleCaptor.capture());
+        assertThat(bundleCaptor.getValue().getBoolean(EXTRA_TRIGGER_HUB)).isTrue();
+    }
+
+    @Test
+    public void powerPress_hubOrDreamOrSleep_hubNotAvailableDreams() {
+        when(mDisplayPolicy.isAwake()).thenReturn(true);
+        initPhoneWindowManager();
+
+        // Set power button behavior.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.POWER_BUTTON_SHORT_PRESS, SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP);
+        mPhoneWindowManager.updateSettings(null);
+
+        // Hub is not available.
+        Settings.Secure.putInt(mContext.getContentResolver(),
+                Settings.Secure.GLANCEABLE_HUB_ENABLED, 0);
+        when(mDreamManagerInternal.canStartDreaming(any(Boolean.class))).thenReturn(true);
+
+        // Power button pressed.
+        int eventTime = 0;
+        mPhoneWindowManager.powerPress(eventTime, 1, 0);
+
+        // Dream is requested.
+        verify(mDreamManagerInternal).requestDream();
+    }
+
+    private void initPhoneWindowManager() {
+        mPhoneWindowManager.mDefaultDisplayPolicy = mDisplayPolicy;
+        mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
+        mContext.getMainThreadHandler().runWithScissors(() -> mPhoneWindowManager.init(
+                new TestInjector(mContext, mock(WindowManagerPolicy.WindowManagerFuncs.class))), 0);
+    }
+
     private void mockStartDockOrHome() throws Exception {
         doNothing().when(ActivityManager.getService()).stopAppSwitches();
         when(mAtmInternal.startHomeOnDisplay(
                 anyInt(), anyString(), anyInt(), anyBoolean(), anyBoolean())).thenReturn(false);
         mPhoneWindowManager.mUserManagerInternal = mock(UserManagerInternal.class);
     }
+
+    private class TestInjector extends PhoneWindowManager.Injector {
+        TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) {
+            super(context, funcs);
+        }
+
+        KeyguardServiceDelegate getKeyguardServiceDelegate() {
+            return mKeyguardServiceDelegate;
+        }
+
+        /**
+         * {@code WindowWakeUpPolicy} registers a local service in its constructor, easier to just
+         * mock it out so we don't have to unregister it after every test.
+         */
+        WindowWakeUpPolicy getWindowWakeUpPolicy() {
+            return mock(WindowWakeUpPolicy.class);
+        }
+    }
 }