• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.service.quicksettings.Tile
21 import android.testing.TestableLooper.RunWithLooper
22 import androidx.test.ext.junit.runners.AndroidJUnit4
23 import androidx.test.filters.SmallTest
24 import com.android.systemui.SysuiTestCase
25 import com.android.systemui.animation.ActivityTransitionAnimator
26 import com.android.systemui.classifier.falsingManager
27 import com.android.systemui.haptics.fakeVibratorHelper
28 import com.android.systemui.kosmos.testScope
29 import com.android.systemui.log.core.FakeLogBuffer
30 import com.android.systemui.qs.qsTileFactory
31 import com.android.systemui.statusbar.policy.keyguardStateController
32 import com.android.systemui.testKosmos
33 import com.google.common.truth.Truth.assertThat
34 import kotlinx.coroutines.test.TestScope
35 import kotlinx.coroutines.test.runTest
36 import org.junit.Before
37 import org.junit.Rule
38 import org.junit.Test
39 import org.junit.runner.RunWith
40 import org.mockito.Mock
41 import org.mockito.junit.MockitoJUnit
42 import org.mockito.junit.MockitoRule
43 import org.mockito.kotlin.times
44 import org.mockito.kotlin.verify
45 import org.mockito.kotlin.whenever
46 
47 @SmallTest
48 @RunWith(AndroidJUnit4::class)
49 @RunWithLooper(setAsMainLooper = true)
50 class QSLongPressEffectTest : SysuiTestCase() {
51 
52     @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
53     private val kosmos = testKosmos()
54     private val vibratorHelper = kosmos.fakeVibratorHelper
55     private val qsTile = kosmos.qsTileFactory.createTile("Test Tile")
56     @Mock private lateinit var callback: QSLongPressEffect.Callback
57     @Mock private lateinit var controller: ActivityTransitionAnimator.Controller
58 
59     private val effectDuration = 400
60     private val lowTickDuration = 12
61     private val spinDuration = 133
62 
63     private lateinit var longPressEffect: QSLongPressEffect
64 
65     @Before
setupnull66     fun setup() {
67         vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] =
68             lowTickDuration
69         vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_SPIN] = spinDuration
70 
71         whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(true)
72         kosmos.falsingManager.setFalseLongTap(false)
73 
74         longPressEffect =
75             QSLongPressEffect(
76                 vibratorHelper,
77                 kosmos.keyguardStateController,
78                 kosmos.falsingManager,
79                 FakeLogBuffer.Factory.create(),
80             )
81         longPressEffect.callback = callback
82         longPressEffect.qsTile = qsTile
83     }
84 
85     @Test
onInitialize_withNegativeDuration_doesNotInitializenull86     fun onInitialize_withNegativeDuration_doesNotInitialize() =
87         testWithScope(false) {
88             // WHEN attempting to initialize with a negative duration
89             val couldInitialize = longPressEffect.initializeEffect(-1)
90 
91             // THEN the effect can't initialized and remains reset
92             assertThat(couldInitialize).isFalse()
93             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
94             assertThat(longPressEffect.hasInitialized).isFalse()
95         }
96 
97     @Test
<lambda>null98     fun onInitialize_withPositiveDuration_initializes() = testWithScope {
99         // WHEN attempting to initialize with a positive duration
100         val couldInitialize = longPressEffect.initializeEffect(effectDuration)
101 
102         // THEN the effect is initialized
103         assertThat(couldInitialize).isTrue()
104         assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
105         assertThat(longPressEffect.hasInitialized).isTrue()
106     }
107 
108     @Test
<lambda>null109     fun onActionDown_whileIdle_startsWait() = testWithScope {
110         // GIVEN an action down event occurs
111         longPressEffect.handleActionDown()
112 
113         // THEN the effect moves to the TIMEOUT_WAIT state
114         assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
115     }
116 
117     @Test
onActionDown_whileClicked_startsWaitnull118     fun onActionDown_whileClicked_startsWait() =
119         testWhileInState(QSLongPressEffect.State.CLICKED) {
120             // GIVEN an action down event occurs
121             longPressEffect.handleActionDown()
122 
123             // THEN the effect moves to the TIMEOUT_WAIT state
124             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
125         }
126 
127     @Test
onActionDown_whileLongClicked_startsWaitnull128     fun onActionDown_whileLongClicked_startsWait() =
129         testWhileInState(QSLongPressEffect.State.LONG_CLICKED) {
130             // GIVEN an action down event occurs
131             longPressEffect.handleActionDown()
132 
133             // THEN the effect moves to the TIMEOUT_WAIT state
134             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
135         }
136 
137     @Test
onActionCancel_whileWaiting_goesIdlenull138     fun onActionCancel_whileWaiting_goesIdle() =
139         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
140             // GIVEN an action cancel occurs
141             longPressEffect.handleActionCancel()
142 
143             // THEN the effect goes back to idle and does not start
144             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
145             assertEffectDidNotStart()
146         }
147 
148     @Test
onWaitComplete_whileWaiting_beginsEffectnull149     fun onWaitComplete_whileWaiting_beginsEffect() =
150         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
151             // GIVEN the pressed timeout is complete
152             longPressEffect.handleTimeoutComplete()
153 
154             // THEN the effect emits the action to start an animator
155             verify(callback, times(1)).onStartAnimator()
156         }
157 
158     @Test
onAnimationStart_whileWaiting_effectBeginsnull159     fun onAnimationStart_whileWaiting_effectBegins() =
160         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
161             // GIVEN that the animator starts
162             longPressEffect.handleAnimationStart()
163 
164             // THEN the effect begins
165             assertEffectStarted()
166         }
167 
168     @Test
onActionUp_whileEffectHasBegun_reversesEffectnull169     fun onActionUp_whileEffectHasBegun_reversesEffect() =
170         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
171             // GIVEN an action up occurs
172             longPressEffect.handleActionUp()
173 
174             // THEN the effect reverses
175             assertEffectReverses(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP)
176         }
177 
178     @Test
<lambda>null179     fun onPlayReverseHaptics_reverseHapticsArePlayed() = testWithScope {
180         // GIVEN a call to play reverse haptics at the effect midpoint
181         val progress = 0.5f
182         longPressEffect.playReverseHaptics(progress)
183 
184         // THEN the expected texture is played
185         val reverseHaptics =
186             LongPressHapticBuilder.createReversedEffect(progress, lowTickDuration, effectDuration)
187         assertThat(reverseHaptics).isNotNull()
188         assertThat(vibratorHelper.hasVibratedWithEffects(reverseHaptics!!)).isTrue()
189     }
190 
191     @Test
onActionCancel_whileEffectHasBegun_reversesEffectnull192     fun onActionCancel_whileEffectHasBegun_reversesEffect() =
193         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
194             // WHEN an action cancel occurs
195             longPressEffect.handleActionCancel()
196 
197             // THEN the effect gets reversed
198             assertEffectReverses(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL)
199         }
200 
201     @Test
onAnimationComplete_keyguardDismissible_effectEndsInLongClickednull202     fun onAnimationComplete_keyguardDismissible_effectEndsInLongClicked() =
203         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
204             // GIVEN that the animation completes
205             longPressEffect.handleAnimationComplete()
206 
207             // THEN the long-press effect completes with a long-click state
208             assertEffectCompleted(QSLongPressEffect.State.LONG_CLICKED)
209         }
210 
211     @Test
onAnimationComplete_keyguardNotDismissible_effectEndsInIdleWithResetnull212     fun onAnimationComplete_keyguardNotDismissible_effectEndsInIdleWithReset() =
213         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
214             // GIVEN that the keyguard is not dismissible
215             whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false)
216 
217             // GIVEN that the animation completes
218             longPressEffect.handleAnimationComplete()
219 
220             // THEN the long-press effect ends in the idle state and the properties are reset
221             assertEffectCompleted(QSLongPressEffect.State.IDLE)
222             verify(callback, times(1)).onResetProperties()
223         }
224 
225     @Test
onAnimationComplete_isFalseLongClick_effectEndsInIdleWithResetnull226     fun onAnimationComplete_isFalseLongClick_effectEndsInIdleWithReset() =
227         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
228             // GIVEN that the long-click is false
229             kosmos.falsingManager.setFalseLongTap(true)
230 
231             // GIVEN that the animation completes
232             longPressEffect.handleAnimationComplete()
233 
234             // THEN the long-press effect ends in the idle state and the properties are reset
235             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
236             verify(callback, times(1)).onResetProperties()
237         }
238 
239     @Test
onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversingAndClicknull240     fun onAnimationComplete_whenRunningBackwardsFromUp_endsWithFinishedReversingAndClick() =
241         testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP) {
242             // GIVEN that the animation completes
243             longPressEffect.handleAnimationComplete()
244 
245             // THEN the callback for finished reversing is used and the effect ends with a click.
246             verify(callback, times(1)).onEffectFinishedReversing()
247             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.CLICKED)
248         }
249 
250     @Test
onAnimationComplete_whenRunningBackwardsFromCancel_endsInIdlenull251     fun onAnimationComplete_whenRunningBackwardsFromCancel_endsInIdle() =
252         testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) {
253             // GIVEN that the animation completes
254             longPressEffect.handleAnimationComplete()
255 
256             // THEN the effect ends in the idle state and the reversed callback is used.
257             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
258             verify(callback, times(1)).onEffectFinishedReversing()
259         }
260 
261     @Test
onActionDown_whileRunningBackwards_cancelsnull262     fun onActionDown_whileRunningBackwards_cancels() =
263         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
264             // GIVEN an action cancel occurs and the effect gets reversed
265             longPressEffect.handleActionCancel()
266 
267             // GIVEN an action down occurs
268             longPressEffect.handleActionDown()
269 
270             // THEN the effect posts an action to cancel the animator
271             verify(callback, times(1)).onCancelAnimator()
272         }
273 
274     @Test
onAnimatorCancel_effectGoesBackToWaitnull275     fun onAnimatorCancel_effectGoesBackToWait() =
276         testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) {
277             // GIVEN that the animator was cancelled
278             longPressEffect.handleAnimationCancel()
279 
280             // THEN the state goes to the timeout wait
281             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
282         }
283 
284     @Test
onAnimationComplete_whileRunningBackwardsFromCancel_goesToIdlenull285     fun onAnimationComplete_whileRunningBackwardsFromCancel_goesToIdle() =
286         testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL) {
287             // GIVEN that the animation completes
288             longPressEffect.handleAnimationComplete()
289 
290             // THEN the state goes to [QSLongPressEffect.State.IDLE]
291             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
292         }
293 
294     @Test
onTileClick_whileWaiting_withQSTile_clicksnull295     fun onTileClick_whileWaiting_withQSTile_clicks() =
296         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
297             // GIVEN that a click was detected
298             val couldClick = longPressEffect.onTileClick()
299 
300             // THEN the click is successful
301             assertThat(couldClick).isTrue()
302         }
303 
304     @Test
onTileClick_whileIdle_withQSTile_clicksnull305     fun onTileClick_whileIdle_withQSTile_clicks() =
306         testWhileInState(QSLongPressEffect.State.IDLE) {
307             // GIVEN that a click was detected
308             val couldClick = longPressEffect.onTileClick()
309 
310             // THEN the click is successful
311             assertThat(couldClick).isTrue()
312         }
313 
314     @Test
onTileClick_whenBouncerIsShowing_ignoresClicknull315     fun onTileClick_whenBouncerIsShowing_ignoresClick() =
316         testWhileInState(QSLongPressEffect.State.IDLE) {
317             // GIVEN that the bouncer is showing
318             whenever(kosmos.keyguardStateController.isPrimaryBouncerShowing).thenReturn(true)
319 
320             // WHEN a click is detected by the tile view
321             val couldClick = longPressEffect.onTileClick()
322 
323             // THEN the click is not successful
324             assertThat(couldClick).isFalse()
325         }
326 
327     @Test
getStateForClick_withUnavailableTile_returnsIdlenull328     fun getStateForClick_withUnavailableTile_returnsIdle() {
329         // GIVEN an unavailable tile
330         qsTile.state?.state = Tile.STATE_UNAVAILABLE
331 
332         // WHEN determining the state of a click action
333         val clickState = longPressEffect.getStateForClick()
334 
335         // THEN the state is IDLE
336         assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE)
337     }
338 
339     @Test
getStateForClick_whenKeyguardsIsShowing_returnsIdlenull340     fun getStateForClick_whenKeyguardsIsShowing_returnsIdle() {
341         // GIVEN an active tile
342         qsTile.state?.state = Tile.STATE_ACTIVE
343 
344         // GIVEN that the keyguard is showing
345         whenever(kosmos.keyguardStateController.isShowing).thenReturn(true)
346 
347         // WHEN determining the state of a click action
348         val clickState = longPressEffect.getStateForClick()
349 
350         // THEN the state is IDLE
351         assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE)
352     }
353 
354     @Test
getStateForClick_withValidTapAndTile_returnsClickednull355     fun getStateForClick_withValidTapAndTile_returnsClicked() {
356         // GIVEN an active tile
357         qsTile.state?.state = Tile.STATE_ACTIVE
358 
359         // GIVEN that the keyguard is not showing
360         whenever(kosmos.keyguardStateController.isShowing).thenReturn(false)
361 
362         // WHEN determining the state of a click action
363         val clickState = longPressEffect.getStateForClick()
364 
365         // THEN the state is CLICKED
366         assertThat(clickState).isEqualTo(QSLongPressEffect.State.CLICKED)
367     }
368 
369     @Test
getStateForClick_withNullTile_returnsIdlenull370     fun getStateForClick_withNullTile_returnsIdle() {
371         // GIVEN that the tile is null
372         longPressEffect.qsTile = null
373 
374         // GIVEN that the keyguard is not showing
375         whenever(kosmos.keyguardStateController.isShowing).thenReturn(false)
376 
377         // WHEN determining the state of a click action
378         val clickState = longPressEffect.getStateForClick()
379 
380         // THEN the state is IDLE
381         assertThat(clickState).isEqualTo(QSLongPressEffect.State.IDLE)
382     }
383 
384     @Test
onLongClickTransitionCancelled_whileInLongClickState_reversesEffectnull385     fun onLongClickTransitionCancelled_whileInLongClickState_reversesEffect() =
386         testWhileInState(QSLongPressEffect.State.LONG_CLICKED) {
387             // GIVEN a transition controller delegate
388             val delegate = longPressEffect.createTransitionControllerDelegate(controller)
389 
390             // WHEN the activity launch animation is cancelled
391             val newOccludedState = false
392             delegate.onTransitionAnimationCancelled(newOccludedState)
393 
394             // THEN the effect reverses and ends in RUNNING_BACKWARDS_FROM_CANCEL
395             assertThat(longPressEffect.state)
396                 .isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL)
397             verify(callback, times(1)).onReverseAnimator(false)
398             verify(controller).onTransitionAnimationCancelled(newOccludedState)
399         }
400 
401     @Test
onTileLongClick_whileIdle_performsLongClicknull402     fun onTileLongClick_whileIdle_performsLongClick() =
403         testWhileInState(QSLongPressEffect.State.IDLE) {
404             // WHEN a long-click is detected by the view
405             val longClicks = longPressEffect.onTileLongClick()
406 
407             // THEN the long click is handled
408             assertThat(longClicks).isTrue()
409         }
410 
testWithScopenull411     private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) =
412         with(kosmos) {
413             testScope.runTest {
414                 if (initialize) {
415                     longPressEffect.initializeEffect(effectDuration)
416                 }
417                 test()
418             }
419         }
420 
testWhileInStatenull421     private fun testWhileInState(
422         state: QSLongPressEffect.State,
423         initialize: Boolean = true,
424         test: suspend TestScope.() -> Unit,
425     ) =
426         with(kosmos) {
427             testScope.runTest {
428                 if (initialize) {
429                     longPressEffect.initializeEffect(effectDuration)
430                 }
431                 // GIVEN a state
432                 longPressEffect.setState(state)
433 
434                 // THEN run the test
435                 test()
436             }
437         }
438 
439     /**
440      * Asserts that the effect started by checking that:
441      * 1. Initial hint haptics are played
442      * 2. The internal state is [QSLongPressEffect.State.RUNNING_FORWARD]
443      */
assertEffectStartednull444     private fun assertEffectStarted() {
445         val longPressHint =
446             LongPressHapticBuilder.createLongPressHint(
447                 lowTickDuration,
448                 spinDuration,
449                 effectDuration,
450             )
451 
452         assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
453         assertThat(longPressHint).isNotNull()
454         assertThat(vibratorHelper.hasVibratedWithEffects(longPressHint!!)).isTrue()
455     }
456 
457     /**
458      * Asserts that the effect did not start by checking that:
459      * 1. No haptics are played
460      * 2. The internal state is not [QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP] or
461      *    [QSLongPressEffect.State.RUNNING_FORWARD] or
462      *    [QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL]
463      */
assertEffectDidNotStartnull464     private fun assertEffectDidNotStart() {
465         assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
466         assertThat(longPressEffect.state)
467             .isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP)
468         assertThat(longPressEffect.state)
469             .isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL)
470         assertThat(vibratorHelper.totalVibrations).isEqualTo(0)
471     }
472 
473     /**
474      * Asserts that the effect completes by checking that:
475      * 1. The final snap haptics are played
476      * 2. The internal state goes back to specified end state.
477      */
assertEffectCompletednull478     private fun assertEffectCompleted(endState: QSLongPressEffect.State) {
479         val snapEffect = LongPressHapticBuilder.createSnapEffect()
480 
481         assertThat(snapEffect).isNotNull()
482         assertThat(vibratorHelper.hasVibratedWithEffects(snapEffect!!)).isTrue()
483         assertThat(longPressEffect.state).isEqualTo(endState)
484     }
485 
486     /**
487      * Assert that the effect gets reverted by checking that the callback to reverse the animator is
488      * used, and that the state is given reversing state.
489      *
490      * @param[reversingState] Either [QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_CANCEL] or
491      *   [QSLongPressEffect.State.RUNNING_BACKWARDS_FROM_UP]
492      */
assertEffectReversesnull493     private fun assertEffectReverses(reversingState: QSLongPressEffect.State) {
494         assertThat(longPressEffect.state).isEqualTo(reversingState)
495         verify(callback, times(1)).onReverseAnimator()
496     }
497 }
498