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