1 /*
2  * Copyright (C) 2024 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.haptics.qs
18 
19 import android.os.VibrationEffect
20 import android.testing.TestableLooper.RunWithLooper
21 import androidx.test.ext.junit.runners.AndroidJUnit4
22 import androidx.test.filters.SmallTest
23 import com.android.systemui.SysuiTestCase
24 import com.android.systemui.haptics.vibratorHelper
25 import com.android.systemui.kosmos.testScope
26 import com.android.systemui.qs.qsTileFactory
27 import com.android.systemui.statusbar.policy.keyguardStateController
28 import com.android.systemui.testKosmos
29 import com.google.common.truth.Truth.assertThat
30 import kotlinx.coroutines.test.TestScope
31 import kotlinx.coroutines.test.runTest
32 import org.junit.Before
33 import org.junit.Rule
34 import org.junit.Test
35 import org.junit.runner.RunWith
36 import org.mockito.Mock
37 import org.mockito.junit.MockitoJUnit
38 import org.mockito.junit.MockitoRule
39 import org.mockito.kotlin.times
40 import org.mockito.kotlin.verify
41 import org.mockito.kotlin.whenever
42 
43 @SmallTest
44 @RunWith(AndroidJUnit4::class)
45 @RunWithLooper(setAsMainLooper = true)
46 class QSLongPressEffectTest : SysuiTestCase() {
47 
48     @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
49     private val kosmos = testKosmos()
50     private val vibratorHelper = kosmos.vibratorHelper
51     private val qsTile = kosmos.qsTileFactory.createTile("Test Tile")
52     @Mock private lateinit var callback: QSLongPressEffect.Callback
53 
54     private val effectDuration = 400
55     private val lowTickDuration = 12
56     private val spinDuration = 133
57 
58     private lateinit var longPressEffect: QSLongPressEffect
59 
60     @Before
setupnull61     fun setup() {
62         vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] =
63             lowTickDuration
64         vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_SPIN] = spinDuration
65 
66         whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(true)
67 
68         longPressEffect =
69             QSLongPressEffect(
70                 vibratorHelper,
71                 kosmos.keyguardStateController,
72             )
73         longPressEffect.callback = callback
74         longPressEffect.qsTile = qsTile
75     }
76 
77     @Test
onInitialize_withNegativeDuration_doesNotInitializenull78     fun onInitialize_withNegativeDuration_doesNotInitialize() =
79         testWithScope(false) {
80             // WHEN attempting to initialize with a negative duration
81             val couldInitialize = longPressEffect.initializeEffect(-1)
82 
83             // THEN the effect can't initialized and remains reset
84             assertThat(couldInitialize).isFalse()
85             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
86             assertThat(longPressEffect.hasInitialized).isFalse()
87         }
88 
89     @Test
<lambda>null90     fun onInitialize_withPositiveDuration_initializes() = testWithScope {
91         // WHEN attempting to initialize with a positive duration
92         val couldInitialize = longPressEffect.initializeEffect(effectDuration)
93 
94         // THEN the effect is initialized
95         assertThat(couldInitialize).isTrue()
96         assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
97         assertThat(longPressEffect.hasInitialized).isTrue()
98     }
99 
100     @Test
<lambda>null101     fun onActionDown_whileIdle_startsWait() = testWithScope {
102         // GIVEN an action down event occurs
103         longPressEffect.handleActionDown()
104 
105         // THEN the effect moves to the TIMEOUT_WAIT state
106         assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
107     }
108 
109     @Test
onActionCancel_whileWaiting_goesIdlenull110     fun onActionCancel_whileWaiting_goesIdle() =
111         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
112             // GIVEN an action cancel occurs
113             longPressEffect.handleActionCancel()
114 
115             // THEN the effect goes back to idle and does not start
116             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
117             assertEffectDidNotStart()
118         }
119 
120     @Test
onWaitComplete_whileWaiting_beginsEffectnull121     fun onWaitComplete_whileWaiting_beginsEffect() =
122         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
123             // GIVEN the pressed timeout is complete
124             longPressEffect.handleTimeoutComplete()
125 
126             // THEN the effect emits the action to start an animator
127             verify(callback, times(1)).onStartAnimator()
128         }
129 
130     @Test
onAnimationStart_whileWaiting_effectBeginsnull131     fun onAnimationStart_whileWaiting_effectBegins() =
132         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
133             // GIVEN that the animator starts
134             longPressEffect.handleAnimationStart()
135 
136             // THEN the effect begins
137             assertEffectStarted()
138         }
139 
140     @Test
onActionUp_whileEffectHasBegun_reversesEffectnull141     fun onActionUp_whileEffectHasBegun_reversesEffect() =
142         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
143             // GIVEN an action up occurs
144             longPressEffect.handleActionUp()
145 
146             // THEN the effect reverses
147             assertEffectReverses()
148         }
149 
150     @Test
<lambda>null151     fun onPlayReverseHaptics_reverseHapticsArePlayed() = testWithScope {
152         // GIVEN a call to play reverse haptics at the effect midpoint
153         val progress = 0.5f
154         longPressEffect.playReverseHaptics(progress)
155 
156         // THEN the expected texture is played
157         val reverseHaptics =
158             LongPressHapticBuilder.createReversedEffect(
159                 progress,
160                 lowTickDuration,
161                 effectDuration,
162             )
163         assertThat(reverseHaptics).isNotNull()
164         assertThat(vibratorHelper.hasVibratedWithEffects(reverseHaptics!!)).isTrue()
165     }
166 
167     @Test
onActionCancel_whileEffectHasBegun_reversesEffectnull168     fun onActionCancel_whileEffectHasBegun_reversesEffect() =
169         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
170             // WHEN an action cancel occurs
171             longPressEffect.handleActionCancel()
172 
173             // THEN the effect gets reversed
174             assertEffectReverses()
175         }
176 
177     @Test
onAnimationComplete_keyguardDismissible_effectEndsWithPreparenull178     fun onAnimationComplete_keyguardDismissible_effectEndsWithPrepare() =
179         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
180             // GIVEN that the animation completes
181             longPressEffect.handleAnimationComplete()
182 
183             // THEN the long-press effect completes and the view is called to prepare
184             assertEffectCompleted()
185             verify(callback, times(1)).onPrepareForLaunch()
186         }
187 
188     @Test
onAnimationComplete_keyguardNotDismissible_effectEndsWithResetnull189     fun onAnimationComplete_keyguardNotDismissible_effectEndsWithReset() =
190         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
191             // GIVEN that the keyguard is not dismissible
192             whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false)
193 
194             // GIVEN that the animation completes
195             longPressEffect.handleAnimationComplete()
196 
197             // THEN the long-press effect completes and the properties are called to reset
198             assertEffectCompleted()
199             verify(callback, times(1)).onResetProperties()
200         }
201 
202     @Test
onActionDown_whileRunningBackwards_cancelsnull203     fun onActionDown_whileRunningBackwards_cancels() =
204         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
205             // GIVEN an action cancel occurs and the effect gets reversed
206             longPressEffect.handleActionCancel()
207 
208             // GIVEN an action down occurs
209             longPressEffect.handleActionDown()
210 
211             // THEN the effect posts an action to cancel the animator
212             verify(callback, times(1)).onCancelAnimator()
213         }
214 
215     @Test
onAnimatorCancel_effectGoesBackToWaitnull216     fun onAnimatorCancel_effectGoesBackToWait() =
217         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
218             // GIVEN that the animator was cancelled
219             longPressEffect.handleAnimationCancel()
220 
221             // THEN the state goes to the timeout wait
222             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
223         }
224 
225     @Test
onAnimationComplete_whileRunningBackwards_goesToIdlenull226     fun onAnimationComplete_whileRunningBackwards_goesToIdle() =
227         testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS) {
228             // GIVEN an action cancel occurs and the effect gets reversed
229             longPressEffect.handleActionCancel()
230 
231             // GIVEN that the animation completes
232             longPressEffect.handleAnimationComplete()
233 
234             // THEN the state goes to [QSLongPressEffect.State.IDLE]
235             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
236         }
237 
238     @Test
onTileClick_whileWaiting_withQSTile_clicksnull239     fun onTileClick_whileWaiting_withQSTile_clicks() =
240         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
241             // GIVEN that a click was detected
242             val couldClick = longPressEffect.onTileClick()
243 
244             // THEN the click is successful
245             assertThat(couldClick).isTrue()
246         }
247 
248     @Test
onTileClick_whileWaiting_withoutQSTile_cannotClicknull249     fun onTileClick_whileWaiting_withoutQSTile_cannotClick() =
250         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
251             // GIVEN that no QSTile has been set
252             longPressEffect.qsTile = null
253 
254             // GIVEN that a click was detected
255             val couldClick = longPressEffect.onTileClick()
256 
257             // THEN the click is not successful
258             assertThat(couldClick).isFalse()
259         }
260 
testWithScopenull261     private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) =
262         with(kosmos) {
263             testScope.runTest {
264                 if (initialize) {
265                     longPressEffect.initializeEffect(effectDuration)
266                 }
267                 test()
268             }
269         }
270 
testWhileInStatenull271     private fun testWhileInState(
272         state: QSLongPressEffect.State,
273         initialize: Boolean = true,
274         test: suspend TestScope.() -> Unit,
275     ) =
276         with(kosmos) {
277             testScope.runTest {
278                 if (initialize) {
279                     longPressEffect.initializeEffect(effectDuration)
280                 }
281                 // GIVEN a state
282                 longPressEffect.setState(state)
283 
284                 // THEN run the test
285                 test()
286             }
287         }
288 
289     /**
290      * Asserts that the effect started by checking that:
291      * 1. Initial hint haptics are played
292      * 2. The internal state is [QSLongPressEffect.State.RUNNING_FORWARD]
293      */
assertEffectStartednull294     private fun assertEffectStarted() {
295         val longPressHint =
296             LongPressHapticBuilder.createLongPressHint(
297                 lowTickDuration,
298                 spinDuration,
299                 effectDuration,
300             )
301 
302         assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
303         assertThat(longPressHint).isNotNull()
304         assertThat(vibratorHelper.hasVibratedWithEffects(longPressHint!!)).isTrue()
305     }
306 
307     /**
308      * Asserts that the effect did not start by checking that:
309      * 1. No haptics are played
310      * 2. The internal state is not [QSLongPressEffect.State.RUNNING_BACKWARDS] or
311      *    [QSLongPressEffect.State.RUNNING_FORWARD]
312      */
assertEffectDidNotStartnull313     private fun assertEffectDidNotStart() {
314         assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
315         assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
316         assertThat(vibratorHelper.totalVibrations).isEqualTo(0)
317     }
318 
319     /**
320      * Asserts that the effect completes by checking that:
321      * 1. The final snap haptics are played
322      * 2. The internal state goes back to [QSLongPressEffect.State.IDLE]
323      */
assertEffectCompletednull324     private fun assertEffectCompleted() {
325         val snapEffect = LongPressHapticBuilder.createSnapEffect()
326 
327         assertThat(snapEffect).isNotNull()
328         assertThat(vibratorHelper.hasVibratedWithEffects(snapEffect!!)).isTrue()
329         assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
330     }
331 
332     /**
333      * Assert that the effect gets reverted by checking that:
334      * 1. The internal state is [QSLongPressEffect.State.RUNNING_BACKWARDS]
335      * 2. An action to reverse the animator is emitted
336      */
assertEffectReversesnull337     private fun assertEffectReverses() {
338         assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
339         verify(callback, times(1)).onReverseAnimator()
340     }
341 }
342