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.internal.jank.Cuj
31 import com.android.internal.util.LatencyTracker
32 import com.android.systemui.SysuiTestCase
33 import com.android.systemui.jank.interactionJankMonitor
34 import com.android.systemui.plugins.NavigationEdgeBackPlugin
35 import com.android.systemui.statusbar.VibratorHelper
36 import com.android.systemui.statusbar.policy.ConfigurationController
37 import com.android.systemui.testKosmos
38 import com.android.systemui.util.time.FakeSystemClock
39 import com.google.common.truth.Truth.assertThat
40 import org.junit.Before
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 import org.mockito.ArgumentMatchers.any
44 import org.mockito.ArgumentMatchers.eq
45 import org.mockito.Mock
46 import org.mockito.Mockito.clearInvocations
47 import org.mockito.Mockito.never
48 import org.mockito.Mockito.verify
49 import org.mockito.MockitoAnnotations
50 
51 @SmallTest
52 @RunWith(AndroidJUnit4::class)
53 @TestableLooper.RunWithLooper(setAsMainLooper = true)
54 class BackPanelControllerTest : SysuiTestCase() {
55     companion object {
56         private const val START_X: Float = 0f
57     }
58 
59     private val kosmos = testKosmos()
60     private lateinit var mBackPanelController: BackPanelController
61     private lateinit var systemClock: FakeSystemClock
62     private lateinit var testableLooper: TestableLooper
63     private var triggerThreshold: Float = 0.0f
64     private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop
65     @Mock private lateinit var vibratorHelper: VibratorHelper
66     @Mock private lateinit var windowManager: WindowManager
67     @Mock private lateinit var configurationController: ConfigurationController
68     @Mock private lateinit var latencyTracker: LatencyTracker
<lambda>null69     private val interactionJankMonitor by lazy { kosmos.interactionJankMonitor }
70     @Mock private lateinit var layoutParams: WindowManager.LayoutParams
71     @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
72 
73     @Before
setupnull74     fun setup() {
75         MockitoAnnotations.initMocks(this)
76         testableLooper = TestableLooper.get(this)
77         systemClock = FakeSystemClock()
78         mBackPanelController =
79             BackPanelController(
80                 context,
81                 windowManager,
82                 ViewConfiguration.get(context),
83                 Handler.createAsync(testableLooper.looper),
84                 systemClock,
85                 vibratorHelper,
86                 configurationController,
87                 latencyTracker,
88                 interactionJankMonitor,
89             )
90         mBackPanelController.setLayoutParams(layoutParams)
91         mBackPanelController.setBackCallback(backCallback)
92         mBackPanelController.setIsLeftPanel(true)
93         triggerThreshold = mBackPanelController.params.staticTriggerThreshold
94     }
95 
96     @Test
handlesActionDownnull97     fun handlesActionDown() {
98         startTouch()
99 
100         assertThat(mBackPanelController.currentState)
101             .isEqualTo(BackPanelController.GestureState.GONE)
102     }
103 
104     @Test
staysHiddenBeforeSlopCrossednull105     fun staysHiddenBeforeSlopCrossed() {
106         startTouch()
107         // Move just enough to not cross the touch slop
108         continueTouch(START_X + touchSlop - 1)
109 
110         assertThat(mBackPanelController.currentState)
111             .isEqualTo(BackPanelController.GestureState.GONE)
112         verify(interactionJankMonitor, never()).begin(any())
113     }
114 
115     @Test
handlesBackCommittednull116     fun handlesBackCommitted() {
117         startTouch()
118         // Move once to cross the touch slop
119         continueTouch(START_X + touchSlop.toFloat() + 1)
120         assertThat(mBackPanelController.currentState)
121             .isEqualTo(BackPanelController.GestureState.ENTRY)
122         verify(interactionJankMonitor).cancel(Cuj.CUJ_BACK_PANEL_ARROW)
123         verify(interactionJankMonitor)
124             .begin(mBackPanelController.getBackPanelView(), Cuj.CUJ_BACK_PANEL_ARROW)
125         // Move again to cross the back trigger threshold
126         continueTouch(START_X + touchSlop + triggerThreshold + 1)
127         // Wait threshold duration and hold touch past trigger threshold
128         moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
129         continueTouch(START_X + touchSlop + triggerThreshold + 1)
130 
131         assertThat(mBackPanelController.currentState)
132             .isEqualTo(BackPanelController.GestureState.ACTIVE)
133         verify(backCallback).setTriggerBack(true)
134         moveTimeForward(100)
135         verify(vibratorHelper)
136             .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE))
137         finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
138         assertThat(mBackPanelController.currentState)
139             .isEqualTo(BackPanelController.GestureState.COMMITTED)
140         verify(backCallback).triggerBack()
141 
142         // Because the Handler that is typically used for transitioning the arrow state from
143         // COMMITTED to GONE is used as an animation-end-listener on a SpringAnimation,
144         // there is no way to meaningfully test that the state becomes GONE and that the tracked
145         // jank interaction is ended. So instead, manually trigger the failsafe, which does
146         // the same thing:
147         mBackPanelController.failsafeRunnable.run()
148         assertThat(mBackPanelController.currentState)
149             .isEqualTo(BackPanelController.GestureState.GONE)
150         verify(interactionJankMonitor).end(Cuj.CUJ_BACK_PANEL_ARROW)
151     }
152 
153     @Test
handlesBackCancellednull154     fun handlesBackCancelled() {
155         startTouch()
156         // Move once to cross the touch slop
157         continueTouch(START_X + touchSlop.toFloat() + 1)
158         assertThat(mBackPanelController.currentState)
159             .isEqualTo(BackPanelController.GestureState.ENTRY)
160         // Move again to cross the back trigger threshold
161         continueTouch(
162             START_X + touchSlop + triggerThreshold -
163                 mBackPanelController.params.deactivationTriggerThreshold
164         )
165         // Wait threshold duration and hold touch before trigger threshold
166         moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
167         continueTouch(
168             START_X + touchSlop + triggerThreshold -
169                 mBackPanelController.params.deactivationTriggerThreshold
170         )
171         clearInvocations(backCallback)
172         moveTimeForward(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION)
173 
174         // Move in the opposite direction to cross the deactivation threshold and cancel back
175         continueTouch(START_X)
176 
177         assertThat(mBackPanelController.currentState)
178             .isEqualTo(BackPanelController.GestureState.INACTIVE)
179         verify(backCallback).setTriggerBack(false)
180         verify(vibratorHelper)
181             .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE))
182 
183         finishTouchActionUp(START_X)
184         verify(backCallback).cancelBack()
185     }
186 
startTouchnull187     private fun startTouch() {
188         mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f))
189     }
190 
continueTouchnull191     private fun continueTouch(x: Float) {
192         mBackPanelController.onMotionEvent(createMotionEvent(ACTION_MOVE, x, 0f))
193     }
194 
finishTouchActionUpnull195     private fun finishTouchActionUp(x: Float) {
196         mBackPanelController.onMotionEvent(createMotionEvent(ACTION_UP, x, 0f))
197     }
198 
createMotionEventnull199     private fun createMotionEvent(action: Int, x: Float, y: Float): MotionEvent {
200         return MotionEvent.obtain(0L, 0L, action, x, y, 0)
201     }
202 
moveTimeForwardnull203     private fun moveTimeForward(millis: Long) {
204         systemClock.advanceTime(millis)
205         testableLooper.moveTimeForward(millis)
206         testableLooper.processAllMessages()
207     }
208 }
209