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