1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.navigationbar.gestural
18 
19 import android.os.Handler
20 import android.testing.TestableLooper
21 import android.view.HapticFeedbackConstants
22 import android.view.MotionEvent
23 import android.view.MotionEvent.ACTION_DOWN
24 import android.view.MotionEvent.ACTION_MOVE
25 import android.view.MotionEvent.ACTION_UP
26 import android.view.ViewConfiguration
27 import android.view.WindowManager
28 import androidx.test.ext.junit.runners.AndroidJUnit4
29 import androidx.test.filters.SmallTest
30 import com.android.app.viewcapture.ViewCaptureAwareWindowManager
31 import com.android.internal.jank.Cuj
32 import com.android.internal.util.LatencyTracker
33 import com.android.systemui.SysuiTestCase
34 import com.android.systemui.jank.interactionJankMonitor
35 import com.android.systemui.plugins.NavigationEdgeBackPlugin
36 import com.android.systemui.statusbar.VibratorHelper
37 import com.android.systemui.statusbar.policy.ConfigurationController
38 import com.android.systemui.testKosmos
39 import com.android.systemui.util.time.FakeSystemClock
40 import com.google.common.truth.Truth.assertThat
41 import org.junit.Before
42 import org.junit.Test
43 import org.junit.runner.RunWith
44 import org.mockito.ArgumentMatchers.any
45 import org.mockito.ArgumentMatchers.eq
46 import org.mockito.Mock
47 import org.mockito.Mockito.clearInvocations
48 import org.mockito.Mockito.never
49 import org.mockito.Mockito.verify
50 import org.mockito.MockitoAnnotations
51 
52 @SmallTest
53 @RunWith(AndroidJUnit4::class)
54 @TestableLooper.RunWithLooper(setAsMainLooper = true)
55 class BackPanelControllerTest : SysuiTestCase() {
56     companion object {
57         private const val START_X: Float = 0f
58     }
59 
60     private val kosmos = testKosmos()
61     private lateinit var mBackPanelController: BackPanelController
62     private lateinit var systemClock: FakeSystemClock
63     private lateinit var testableLooper: TestableLooper
64     private var triggerThreshold: Float = 0.0f
65     private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop
66     @Mock private lateinit var vibratorHelper: VibratorHelper
67     @Mock private lateinit var viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager
68     @Mock private lateinit var configurationController: ConfigurationController
69     @Mock private lateinit var latencyTracker: LatencyTracker
<lambda>null70     private val interactionJankMonitor by lazy { kosmos.interactionJankMonitor }
71     @Mock private lateinit var layoutParams: WindowManager.LayoutParams
72     @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
73 
74     @Before
setupnull75     fun setup() {
76         MockitoAnnotations.initMocks(this)
77         testableLooper = TestableLooper.get(this)
78         systemClock = FakeSystemClock()
79         mBackPanelController =
80             BackPanelController(
81                 context,
82                 viewCaptureAwareWindowManager,
83                 ViewConfiguration.get(context),
84                 Handler.createAsync(testableLooper.looper),
85                 systemClock,
86                 vibratorHelper,
87                 configurationController,
88                 latencyTracker,
89                 interactionJankMonitor,
90             )
91         mBackPanelController.setLayoutParams(layoutParams)
92         mBackPanelController.setBackCallback(backCallback)
93         mBackPanelController.setIsLeftPanel(true)
94         triggerThreshold = mBackPanelController.params.staticTriggerThreshold
95     }
96 
97     @Test
handlesActionDownnull98     fun handlesActionDown() {
99         startTouch()
100 
101         assertThat(mBackPanelController.currentState)
102             .isEqualTo(BackPanelController.GestureState.GONE)
103     }
104 
105     @Test
staysHiddenBeforeSlopCrossednull106     fun staysHiddenBeforeSlopCrossed() {
107         startTouch()
108         // Move just enough to not cross the touch slop
109         continueTouch(START_X + touchSlop - 1)
110 
111         assertThat(mBackPanelController.currentState)
112             .isEqualTo(BackPanelController.GestureState.GONE)
113         verify(interactionJankMonitor, never()).begin(any())
114     }
115 
116     @Test
handlesBackCommittednull117     fun handlesBackCommitted() {
118         startTouch()
119         // Move once to cross the touch slop
120         continueTouch(START_X + touchSlop.toFloat() + 1)
121         assertThat(mBackPanelController.currentState)
122             .isEqualTo(BackPanelController.GestureState.ENTRY)
123         verify(interactionJankMonitor).cancel(Cuj.CUJ_BACK_PANEL_ARROW)
124         verify(interactionJankMonitor)
125             .begin(mBackPanelController.getBackPanelView(), Cuj.CUJ_BACK_PANEL_ARROW)
126         // Move again to cross the back trigger threshold
127         continueTouch(START_X + touchSlop + triggerThreshold + 1)
128         // Wait threshold duration and hold touch past trigger threshold
129         moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
130         continueTouch(START_X + touchSlop + triggerThreshold + 1)
131 
132         assertThat(mBackPanelController.currentState)
133             .isEqualTo(BackPanelController.GestureState.ACTIVE)
134         verify(backCallback).setTriggerBack(true)
135         moveTimeForward(100)
136         verify(vibratorHelper)
137             .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE))
138         finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
139         assertThat(mBackPanelController.currentState)
140             .isEqualTo(BackPanelController.GestureState.COMMITTED)
141         verify(backCallback).triggerBack()
142 
143         // Because the Handler that is typically used for transitioning the arrow state from
144         // COMMITTED to GONE is used as an animation-end-listener on a SpringAnimation,
145         // there is no way to meaningfully test that the state becomes GONE and that the tracked
146         // jank interaction is ended. So instead, manually trigger the failsafe, which does
147         // the same thing:
148         mBackPanelController.failsafeRunnable.run()
149         assertThat(mBackPanelController.currentState)
150             .isEqualTo(BackPanelController.GestureState.GONE)
151         verify(interactionJankMonitor).end(Cuj.CUJ_BACK_PANEL_ARROW)
152     }
153 
154     @Test
handlesBackCancellednull155     fun handlesBackCancelled() {
156         startTouch()
157         // Move once to cross the touch slop
158         continueTouch(START_X + touchSlop.toFloat() + 1)
159         assertThat(mBackPanelController.currentState)
160             .isEqualTo(BackPanelController.GestureState.ENTRY)
161         // Move again to cross the back trigger threshold
162         continueTouch(
163             START_X + touchSlop + triggerThreshold -
164                 mBackPanelController.params.deactivationTriggerThreshold
165         )
166         // Wait threshold duration and hold touch before trigger threshold
167         moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
168         continueTouch(
169             START_X + touchSlop + triggerThreshold -
170                 mBackPanelController.params.deactivationTriggerThreshold
171         )
172         clearInvocations(backCallback)
173         moveTimeForward(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION)
174 
175         // Move in the opposite direction to cross the deactivation threshold and cancel back
176         continueTouch(START_X)
177 
178         assertThat(mBackPanelController.currentState)
179             .isEqualTo(BackPanelController.GestureState.INACTIVE)
180         verify(backCallback).setTriggerBack(false)
181         verify(vibratorHelper)
182             .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE))
183 
184         finishTouchActionUp(START_X)
185         verify(backCallback).cancelBack()
186     }
187 
startTouchnull188     private fun startTouch() {
189         mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f))
190     }
191 
continueTouchnull192     private fun continueTouch(x: Float) {
193         mBackPanelController.onMotionEvent(createMotionEvent(ACTION_MOVE, x, 0f))
194     }
195 
finishTouchActionUpnull196     private fun finishTouchActionUp(x: Float) {
197         mBackPanelController.onMotionEvent(createMotionEvent(ACTION_UP, x, 0f))
198     }
199 
createMotionEventnull200     private fun createMotionEvent(action: Int, x: Float, y: Float): MotionEvent {
201         return MotionEvent.obtain(0L, 0L, action, x, y, 0)
202     }
203 
moveTimeForwardnull204     private fun moveTimeForward(millis: Long) {
205         systemClock.advanceTime(millis)
206         testableLooper.moveTimeForward(millis)
207         testableLooper.processAllMessages()
208     }
209 }
210