Merge "Tap to wake up dreaming for lockscreen hosted dream" into udc-qpr-dev
diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
index eb1ca66..809edc0 100644
--- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
@@ -70,6 +70,19 @@
         }
     }
 
+    /**
+     * Wakes up the device if dreaming with a screensaver.
+     *
+     * @param why a string explaining why we're waking the device for debugging purposes. Should be
+     *   in SCREAMING_SNAKE_CASE.
+     * @param wakeReason the PowerManager-based reason why we're waking the device.
+     */
+    fun wakeUpIfDreaming(why: String, @PowerManager.WakeReason wakeReason: Int) {
+        if (statusBarStateController.isDreaming) {
+            repository.wakeUp(why, wakeReason)
+        }
+    }
+
     companion object {
         private const val FSI_WAKE_WHY = "full_screen_intent"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt
new file mode 100644
index 0000000..45fc68a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 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.systemui.shade
+
+import android.os.PowerManager
+import android.view.GestureDetector
+import android.view.MotionEvent
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.StatusBarState
+import javax.inject.Inject
+
+/**
+ * This gestureListener will wake up by tap when the device is dreaming but not dozing, and the
+ * selected screensaver is hosted in lockscreen. Tap is gated by the falsing manager.
+ *
+ * Touches go through the [NotificationShadeWindowViewController].
+ */
+@SysUISingleton
+class LockscreenHostedDreamGestureListener
+@Inject
+constructor(
+    private val falsingManager: FalsingManager,
+    private val powerInteractor: PowerInteractor,
+    private val statusBarStateController: StatusBarStateController,
+    private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+    private val keyguardRepository: KeyguardRepository,
+    private val shadeLogger: ShadeLogger,
+) : GestureDetector.SimpleOnGestureListener() {
+    private val TAG = this::class.simpleName
+
+    override fun onSingleTapUp(e: MotionEvent): Boolean {
+        if (shouldHandleMotionEvent()) {
+            if (!falsingManager.isFalseTap(LOW_PENALTY)) {
+                shadeLogger.d("$TAG#onSingleTapUp tap handled, requesting wakeUpIfDreaming")
+                powerInteractor.wakeUpIfDreaming(
+                    "DREAMING_SINGLE_TAP",
+                    PowerManager.WAKE_REASON_TAP
+                )
+            } else {
+                shadeLogger.d("$TAG#onSingleTapUp false tap ignored")
+            }
+            return true
+        }
+        return false
+    }
+
+    private fun shouldHandleMotionEvent(): Boolean {
+        return keyguardRepository.isActiveDreamLockscreenHosted.value &&
+            statusBarStateController.state == StatusBarState.KEYGUARD &&
+            !primaryBouncerInteractor.isBouncerShowing()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index f6db9e4..108ea68 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade;
 
+import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
 import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
@@ -102,9 +103,12 @@
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     private final AmbientState mAmbientState;
     private final PulsingGestureListener mPulsingGestureListener;
+    private final LockscreenHostedDreamGestureListener mLockscreenHostedDreamGestureListener;
     private final NotificationInsetsController mNotificationInsetsController;
     private final boolean mIsTrackpadCommonEnabled;
+    private final FeatureFlags mFeatureFlags;
     private GestureDetector mPulsingWakeupGestureHandler;
+    private GestureDetector mDreamingWakeupGestureHandler;
     private View mBrightnessMirror;
     private boolean mTouchActive;
     private boolean mTouchCancelled;
@@ -156,6 +160,7 @@
             NotificationInsetsController notificationInsetsController,
             AmbientState ambientState,
             PulsingGestureListener pulsingGestureListener,
+            LockscreenHostedDreamGestureListener lockscreenHostedDreamGestureListener,
             KeyguardBouncerViewModel keyguardBouncerViewModel,
             KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
             KeyguardMessageAreaController.Factory messageAreaControllerFactory,
@@ -187,8 +192,10 @@
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mAmbientState = ambientState;
         mPulsingGestureListener = pulsingGestureListener;
+        mLockscreenHostedDreamGestureListener = lockscreenHostedDreamGestureListener;
         mNotificationInsetsController = notificationInsetsController;
         mIsTrackpadCommonEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_COMMON);
+        mFeatureFlags = featureFlags;
 
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -237,7 +244,10 @@
         mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller);
         mPulsingWakeupGestureHandler = new GestureDetector(mView.getContext(),
                 mPulsingGestureListener);
-
+        if (mFeatureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) {
+            mDreamingWakeupGestureHandler = new GestureDetector(mView.getContext(),
+                    mLockscreenHostedDreamGestureListener);
+        }
         mView.setLayoutInsetsController(mNotificationInsetsController);
         mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() {
             @Override
@@ -291,6 +301,10 @@
 
                 mFalsingCollector.onTouchEvent(ev);
                 mPulsingWakeupGestureHandler.onTouchEvent(ev);
+                if (mDreamingWakeupGestureHandler != null
+                        && mDreamingWakeupGestureHandler.onTouchEvent(ev)) {
+                    return true;
+                }
                 if (mStatusBarKeyguardViewManager.dispatchTouchEvent(ev)) {
                     return true;
                 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
index 45bb931..435a1f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
@@ -182,6 +182,32 @@
         assertThat(repository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_APPLICATION)
     }
 
+    @Test
+    fun wakeUpIfDreaming_dreaming_woken() {
+        // GIVEN device is dreaming
+        whenever(statusBarStateController.isDreaming).thenReturn(true)
+
+        // WHEN wakeUpIfDreaming is called
+        underTest.wakeUpIfDreaming("testReason", PowerManager.WAKE_REASON_GESTURE)
+
+        // THEN device is woken up
+        assertThat(repository.lastWakeWhy).isEqualTo("testReason")
+        assertThat(repository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
+    }
+
+    @Test
+    fun wakeUpIfDreaming_notDreaming_notWoken() {
+        // GIVEN device is not dreaming
+        whenever(statusBarStateController.isDreaming).thenReturn(false)
+
+        // WHEN wakeUpIfDreaming is called
+        underTest.wakeUpIfDreaming("why", PowerManager.WAKE_REASON_TAP)
+
+        // THEN device is not woken
+        assertThat(repository.lastWakeWhy).isNull()
+        assertThat(repository.lastWakeReason).isNull()
+    }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt
new file mode 100644
index 0000000..24d62fb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 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.systemui.shade
+
+import android.os.PowerManager
+import android.testing.AndroidTestingRunner
+import android.view.MotionEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+class LockscreenHostedDreamGestureListenerTest : SysuiTestCase() {
+    @Mock private lateinit var falsingManager: FalsingManager
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var shadeLogger: ShadeLogger
+    @Mock private lateinit var screenOffAnimationController: ScreenOffAnimationController
+    @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
+
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private lateinit var powerRepository: FakePowerRepository
+    private lateinit var keyguardRepository: FakeKeyguardRepository
+    private lateinit var underTest: LockscreenHostedDreamGestureListener
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        powerRepository = FakePowerRepository()
+        keyguardRepository = FakeKeyguardRepository()
+
+        underTest =
+            LockscreenHostedDreamGestureListener(
+                falsingManager,
+                PowerInteractor(
+                    powerRepository,
+                    keyguardRepository,
+                    falsingCollector,
+                    screenOffAnimationController,
+                    statusBarStateController,
+                ),
+                statusBarStateController,
+                primaryBouncerInteractor,
+                keyguardRepository,
+                shadeLogger,
+            )
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(primaryBouncerInteractor.isBouncerShowing()).thenReturn(false)
+    }
+
+    @Test
+    fun testGestureDetector_onSingleTap_whileDreaming() =
+        testScope.runTest {
+            // GIVEN device dreaming and the dream is hosted in lockscreen
+            whenever(statusBarStateController.isDreaming).thenReturn(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            testScope.runCurrent()
+
+            // GIVEN the falsing manager does NOT think the tap is a false tap
+            whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false)
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN wake up device if dreaming
+            Truth.assertThat(powerRepository.lastWakeWhy).isNotNull()
+            Truth.assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_TAP)
+        }
+
+    @Test
+    fun testGestureDetector_onSingleTap_notOnKeyguard() =
+        testScope.runTest {
+            // GIVEN device dreaming and the dream is hosted in lockscreen
+            whenever(statusBarStateController.isDreaming).thenReturn(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            testScope.runCurrent()
+
+            // GIVEN shade is open
+            whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+
+            // GIVEN the falsing manager does NOT think the tap is a false tap
+            whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false)
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN the falsing manager never gets a call
+            verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt())
+        }
+
+    @Test
+    fun testGestureDetector_onSingleTap_bouncerShown() =
+        testScope.runTest {
+            // GIVEN device dreaming and the dream is hosted in lockscreen
+            whenever(statusBarStateController.isDreaming).thenReturn(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            testScope.runCurrent()
+
+            // GIVEN bouncer is expanded
+            whenever(primaryBouncerInteractor.isBouncerShowing()).thenReturn(true)
+
+            // GIVEN the falsing manager does NOT think the tap is a false tap
+            whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false)
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN the falsing manager never gets a call
+            verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt())
+        }
+
+    @Test
+    fun testGestureDetector_onSingleTap_falsing() =
+        testScope.runTest {
+            // GIVEN device dreaming and the dream is hosted in lockscreen
+            whenever(statusBarStateController.isDreaming).thenReturn(true)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+            testScope.runCurrent()
+
+            // GIVEN the falsing manager thinks the tap is a false tap
+            whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(true)
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN the device doesn't wake up
+            Truth.assertThat(powerRepository.lastWakeWhy).isNull()
+            Truth.assertThat(powerRepository.lastWakeReason).isNull()
+        }
+
+    @Test
+    fun testSingleTap_notDreaming_noFalsingCheck() =
+        testScope.runTest {
+            // GIVEN device not dreaming with lockscreen hosted dream
+            whenever(statusBarStateController.isDreaming).thenReturn(false)
+            keyguardRepository.setIsActiveDreamLockscreenHosted(false)
+            testScope.runCurrent()
+
+            // WHEN there's a tap
+            underTest.onSingleTapUp(upEv)
+
+            // THEN the falsing manager never gets a call
+            verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt())
+        }
+}
+
+private val upEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 5fb3a79..2a398c55 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -111,6 +111,8 @@
     @Mock private lateinit var lockIconViewController: LockIconViewController
     @Mock private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController
     @Mock private lateinit var pulsingGestureListener: PulsingGestureListener
+    @Mock
+    private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
     @Mock private lateinit var notificationInsetsController: NotificationInsetsController
     @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@@ -147,6 +149,7 @@
         featureFlags.set(Flags.DUAL_SHADE, false)
         featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
+        featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
 
         val inputProxy = MultiShadeInputProxy()
         testScope = TestScope()
@@ -183,6 +186,7 @@
                 notificationInsetsController,
                 ambientState,
                 pulsingGestureListener,
+                mLockscreenHostedDreamGestureListener,
                 keyguardBouncerViewModel,
                 keyguardBouncerComponentFactory,
                 mock(KeyguardMessageAreaController.Factory::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 544137e..d9eb9b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -113,6 +113,8 @@
     @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController
     @Mock private lateinit var ambientState: AmbientState
     @Mock private lateinit var pulsingGestureListener: PulsingGestureListener
+    @Mock
+    private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
     @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
     @Mock private lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
     @Mock private lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@@ -161,6 +163,7 @@
         featureFlags.set(Flags.DUAL_SHADE, false)
         featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
         featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
+        featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
         val inputProxy = MultiShadeInputProxy()
         testScope = TestScope()
         val multiShadeInteractor =
@@ -196,6 +199,7 @@
                 notificationInsetsController,
                 ambientState,
                 pulsingGestureListener,
+                mLockscreenHostedDreamGestureListener,
                 keyguardBouncerViewModel,
                 keyguardBouncerComponentFactory,
                 Mockito.mock(KeyguardMessageAreaController.Factory::class.java),