Loading packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt +237 −212 Original line number Diff line number Diff line Loading @@ -16,25 +16,22 @@ package com.android.systemui.haptics.slider import android.os.VibrationAttributes import android.os.VibrationEffect import android.view.VelocityTracker import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.haptics.vibratorHelper import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.fakeSystemClock import kotlin.math.max import kotlin.test.assertEquals import kotlin.test.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest Loading @@ -42,10 +39,10 @@ import org.mockito.MockitoAnnotations class SliderHapticFeedbackProviderTest : SysuiTestCase() { @Mock private lateinit var velocityTracker: VelocityTracker @Mock private lateinit var vibratorHelper: VibratorHelper private val kosmos = testKosmos() private val config = SliderHapticFeedbackConfig() private val clock = FakeSystemClock() private val lowTickDuration = 12 // Mocked duration of a low tick private val dragTextureThresholdMillis = Loading @@ -55,32 +52,42 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) whenever(vibratorHelper.getPrimitiveDurations(any())) .thenReturn(intArrayOf(lowTickDuration)) whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true) whenever(velocityTracker.getAxisVelocity(config.velocityAxis)) .thenReturn(config.maxVelocityToScale) kosmos.vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] = lowTickDuration sliderHapticFeedbackProvider = SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, config, clock) SliderHapticFeedbackProvider( kosmos.vibratorHelper, velocityTracker, config, kosmos.fakeSystemClock, ) } @Test fun playHapticAtLowerBookend_playsClick() { fun playHapticAtLowerBookend_playsClick() = with(kosmos) { val vibration = VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() sliderHapticFeedbackProvider.onLowerBookend() verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java)) assertTrue(vibratorHelper.hasVibratedWithEffects(vibration)) } @Test fun playHapticAtLowerBookend_twoTimes_playsClickOnlyOnce() { fun playHapticAtLowerBookend_twoTimes_playsClickOnlyOnce() = with(kosmos) { val vibration = VibrationEffect.startComposition() .addPrimitive( Loading @@ -92,43 +99,49 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { sliderHapticFeedbackProvider.onLowerBookend() sliderHapticFeedbackProvider.onLowerBookend() verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java)) assertTrue(vibratorHelper.hasVibratedWithEffects(vibration)) } @Test fun playHapticAtUpperBookend_playsClick() { fun playHapticAtUpperBookend_playsClick() = with(kosmos) { val vibration = VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() sliderHapticFeedbackProvider.onUpperBookend() verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java)) assertTrue(vibratorHelper.hasVibratedWithEffects(vibration)) } @Test fun playHapticAtUpperBookend_twoTimes_playsClickOnlyOnce() { fun playHapticAtUpperBookend_twoTimes_playsClickOnlyOnce() = with(kosmos) { val vibration = VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() sliderHapticFeedbackProvider.onUpperBookend() sliderHapticFeedbackProvider.onUpperBookend() verify(vibratorHelper, times(1)) .vibrate(eq(vibration), any(VibrationAttributes::class.java)) assertEquals(/* expected=*/ 1, vibratorHelper.timesVibratedWithEffect(vibration)) } @Test fun playHapticAtProgress_onQuickSuccession_playsLowTicksOnce() { fun playHapticAtProgress_onQuickSuccession_playsLowTicksOnce() = with(kosmos) { // GIVEN max velocity and slider progress val progress = 1f val expectedScale = Loading @@ -142,43 +155,44 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { } // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN two calls to play occur immediately sliderHapticFeedbackProvider.onProgress(progress) sliderHapticFeedbackProvider.onProgress(progress) // THEN the correct composition only plays once verify(vibratorHelper, times(1)) .vibrate(eq(ticks.compose()), any(VibrationAttributes::class.java)) assertEquals(/* expected=*/ 1, vibratorHelper.timesVibratedWithEffect(ticks.compose())) } @Test fun playHapticAtProgress_beforeNextDragThreshold_playsLowTicksOnce() { fun playHapticAtProgress_beforeNextDragThreshold_playsLowTicksOnce() = with(kosmos) { // GIVEN max velocity and a slider progress at half progress val firstProgress = 0.5f val firstTicks = generateTicksComposition(config.maxVelocityToScale, firstProgress) // Given a second slider progress event smaller than the progress threshold val secondProgress = firstProgress + max(0f, config.deltaProgressForDragThreshold - 0.01f) val secondProgress = firstProgress + max(0f, config.deltaProgressForDragThreshold - 0.01f) // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN two calls to play occur with the required threshold separation (time and progress) // WHEN two calls to play occur with the required threshold separation (time and // progress) sliderHapticFeedbackProvider.onProgress(firstProgress) clock.advanceTime(dragTextureThresholdMillis.toLong()) fakeSystemClock.advanceTime(dragTextureThresholdMillis.toLong()) sliderHapticFeedbackProvider.onProgress(secondProgress) // THEN Only the first compositions plays verify(vibratorHelper, times(1)) .vibrate(eq(firstTicks), any(VibrationAttributes::class.java)) verify(vibratorHelper, times(1)) .vibrate(any(VibrationEffect::class.java), any(VibrationAttributes::class.java)) assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(firstTicks)) assertEquals(/* expected= */ 1, vibratorHelper.totalVibrations) } @Test fun playHapticAtProgress_afterNextDragThreshold_playsLowTicksTwice() { fun playHapticAtProgress_afterNextDragThreshold_playsLowTicksTwice() = with(kosmos) { // GIVEN max velocity and a slider progress at half progress val firstProgress = 0.5f val firstTicks = generateTicksComposition(config.maxVelocityToScale, firstProgress) Loading @@ -188,22 +202,22 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { val secondTicks = generateTicksComposition(config.maxVelocityToScale, secondProgress) // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN two calls to play occur with the required threshold separation (time and progress) // WHEN two calls to play occur with the required threshold separation (time and // progress) sliderHapticFeedbackProvider.onProgress(firstProgress) clock.advanceTime(dragTextureThresholdMillis.toLong()) fakeSystemClock.advanceTime(dragTextureThresholdMillis.toLong()) sliderHapticFeedbackProvider.onProgress(secondProgress) // THEN the correct compositions play verify(vibratorHelper, times(1)) .vibrate(eq(firstTicks), any(VibrationAttributes::class.java)) verify(vibratorHelper, times(1)) .vibrate(eq(secondTicks), any(VibrationAttributes::class.java)) assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(firstTicks)) assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(secondTicks)) } @Test fun playHapticAtLowerBookend_afterPlayingAtProgress_playsTwice() { fun playHapticAtLowerBookend_afterPlayingAtProgress_playsTwice() = with(kosmos) { // GIVEN max velocity and slider progress val progress = 1f val expectedScale = Loading @@ -219,7 +233,9 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() Loading @@ -231,12 +247,15 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { sliderHapticFeedbackProvider.onLowerBookend() // THEN there are two bookend vibrations verify(vibratorHelper, times(2)) .vibrate(eq(bookendVibration), any(VibrationAttributes::class.java)) assertEquals( /* expected= */ 2, vibratorHelper.timesVibratedWithEffect(bookendVibration) ) } @Test fun playHapticAtUpperBookend_afterPlayingAtProgress_playsTwice() { fun playHapticAtUpperBookend_afterPlayingAtProgress_playsTwice() = with(kosmos) { // GIVEN max velocity and slider progress val progress = 1f val expectedScale = Loading @@ -252,7 +271,9 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() Loading @@ -264,16 +285,19 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { sliderHapticFeedbackProvider.onUpperBookend() // THEN there are two bookend vibrations verify(vibratorHelper, times(2)) .vibrate(eq(bookendVibration), any(VibrationAttributes::class.java)) assertEquals( /* expected= */ 2, vibratorHelper.timesVibratedWithEffect(bookendVibration) ) } fun dragTextureLastProgress_afterDragTextureHaptics_keepsLastDragTextureProgress() { fun dragTextureLastProgress_afterDragTextureHaptics_keepsLastDragTextureProgress() = with(kosmos) { // GIVEN max velocity and a slider progress at half progress val progress = 0.5f // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN a drag texture plays sliderHapticFeedbackProvider.onProgress(progress) Loading @@ -283,12 +307,13 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { } @Test fun dragTextureLastProgress_afterDragTextureHaptics_resetsOnHandleReleased() { fun dragTextureLastProgress_afterDragTextureHaptics_resetsOnHandleReleased() = with(kosmos) { // GIVEN max velocity and a slider progress at half progress val progress = 0.5f // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN a drag texture plays sliderHapticFeedbackProvider.onProgress(progress) Loading packages/SystemUI/tests/utils/src/com/android/systemui/haptics/EmptyVibrator.kt 0 → 100644 +40 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.haptics import android.os.VibrationAttributes import android.os.VibrationEffect import android.os.Vibrator /** A simple empty vibrator required for the [FakeVibratorHelper] */ class EmptyVibrator : Vibrator() { override fun cancel() {} override fun cancel(usageFilter: Int) {} override fun hasAmplitudeControl(): Boolean = true override fun hasVibrator(): Boolean = true override fun vibrate( uid: Int, opPkg: String, vibe: VibrationEffect, reason: String, attributes: VibrationAttributes, ) {} } packages/SystemUI/tests/utils/src/com/android/systemui/haptics/FakeVibratorHelper.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.haptics import android.annotation.SuppressLint import android.media.AudioAttributes import android.os.VibrationAttributes import android.os.VibrationEffect import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock /** A fake [VibratorHelper] that only keeps track of the latest vibration effects delivered */ @SuppressLint("VisibleForTests") class FakeVibratorHelper : VibratorHelper(EmptyVibrator(), FakeExecutor(FakeSystemClock())) { /** A customizable map of primitive ids and their durations in ms */ val primitiveDurations: HashMap<Int, Int> = ALL_PRIMITIVE_DURATIONS private val vibrationEffectHistory = ArrayList<VibrationEffect>() val totalVibrations: Int get() = vibrationEffectHistory.size override fun vibrate(effect: VibrationEffect) { vibrationEffectHistory.add(effect) } override fun vibrate(effect: VibrationEffect, attributes: VibrationAttributes) = vibrate(effect) override fun vibrate(effect: VibrationEffect, attributes: AudioAttributes) = vibrate(effect) override fun vibrate( uid: Int, opPkg: String?, vibe: VibrationEffect, reason: String?, attributes: VibrationAttributes, ) = vibrate(vibe) override fun getPrimitiveDurations(vararg primitiveIds: Int): IntArray = primitiveIds.map { primitiveDurations[it] ?: 0 }.toIntArray() fun hasVibratedWithEffects(vararg effects: VibrationEffect): Boolean = vibrationEffectHistory.containsAll(effects.toList()) fun timesVibratedWithEffect(effect: VibrationEffect): Int = vibrationEffectHistory.count { it == effect } companion object { val ALL_PRIMITIVE_DURATIONS = hashMapOf( VibrationEffect.Composition.PRIMITIVE_NOOP to 0, VibrationEffect.Composition.PRIMITIVE_CLICK to 12, VibrationEffect.Composition.PRIMITIVE_THUD to 300, VibrationEffect.Composition.PRIMITIVE_SPIN to 133, VibrationEffect.Composition.PRIMITIVE_QUICK_RISE to 150, VibrationEffect.Composition.PRIMITIVE_SLOW_RISE to 500, VibrationEffect.Composition.PRIMITIVE_QUICK_FALL to 100, VibrationEffect.Composition.PRIMITIVE_TICK to 5, VibrationEffect.Composition.PRIMITIVE_LOW_TICK to 12, ) } } packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt 0 → 100644 +21 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.haptics import com.android.systemui.kosmos.Kosmos var Kosmos.vibratorHelper by Kosmos.Fixture { FakeVibratorHelper() } Loading
packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt +237 −212 Original line number Diff line number Diff line Loading @@ -16,25 +16,22 @@ package com.android.systemui.haptics.slider import android.os.VibrationAttributes import android.os.VibrationEffect import android.view.VelocityTracker import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.haptics.vibratorHelper import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.fakeSystemClock import kotlin.math.max import kotlin.test.assertEquals import kotlin.test.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest Loading @@ -42,10 +39,10 @@ import org.mockito.MockitoAnnotations class SliderHapticFeedbackProviderTest : SysuiTestCase() { @Mock private lateinit var velocityTracker: VelocityTracker @Mock private lateinit var vibratorHelper: VibratorHelper private val kosmos = testKosmos() private val config = SliderHapticFeedbackConfig() private val clock = FakeSystemClock() private val lowTickDuration = 12 // Mocked duration of a low tick private val dragTextureThresholdMillis = Loading @@ -55,32 +52,42 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) whenever(vibratorHelper.getPrimitiveDurations(any())) .thenReturn(intArrayOf(lowTickDuration)) whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true) whenever(velocityTracker.getAxisVelocity(config.velocityAxis)) .thenReturn(config.maxVelocityToScale) kosmos.vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] = lowTickDuration sliderHapticFeedbackProvider = SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, config, clock) SliderHapticFeedbackProvider( kosmos.vibratorHelper, velocityTracker, config, kosmos.fakeSystemClock, ) } @Test fun playHapticAtLowerBookend_playsClick() { fun playHapticAtLowerBookend_playsClick() = with(kosmos) { val vibration = VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() sliderHapticFeedbackProvider.onLowerBookend() verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java)) assertTrue(vibratorHelper.hasVibratedWithEffects(vibration)) } @Test fun playHapticAtLowerBookend_twoTimes_playsClickOnlyOnce() { fun playHapticAtLowerBookend_twoTimes_playsClickOnlyOnce() = with(kosmos) { val vibration = VibrationEffect.startComposition() .addPrimitive( Loading @@ -92,43 +99,49 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { sliderHapticFeedbackProvider.onLowerBookend() sliderHapticFeedbackProvider.onLowerBookend() verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java)) assertTrue(vibratorHelper.hasVibratedWithEffects(vibration)) } @Test fun playHapticAtUpperBookend_playsClick() { fun playHapticAtUpperBookend_playsClick() = with(kosmos) { val vibration = VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() sliderHapticFeedbackProvider.onUpperBookend() verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java)) assertTrue(vibratorHelper.hasVibratedWithEffects(vibration)) } @Test fun playHapticAtUpperBookend_twoTimes_playsClickOnlyOnce() { fun playHapticAtUpperBookend_twoTimes_playsClickOnlyOnce() = with(kosmos) { val vibration = VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() sliderHapticFeedbackProvider.onUpperBookend() sliderHapticFeedbackProvider.onUpperBookend() verify(vibratorHelper, times(1)) .vibrate(eq(vibration), any(VibrationAttributes::class.java)) assertEquals(/* expected=*/ 1, vibratorHelper.timesVibratedWithEffect(vibration)) } @Test fun playHapticAtProgress_onQuickSuccession_playsLowTicksOnce() { fun playHapticAtProgress_onQuickSuccession_playsLowTicksOnce() = with(kosmos) { // GIVEN max velocity and slider progress val progress = 1f val expectedScale = Loading @@ -142,43 +155,44 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { } // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN two calls to play occur immediately sliderHapticFeedbackProvider.onProgress(progress) sliderHapticFeedbackProvider.onProgress(progress) // THEN the correct composition only plays once verify(vibratorHelper, times(1)) .vibrate(eq(ticks.compose()), any(VibrationAttributes::class.java)) assertEquals(/* expected=*/ 1, vibratorHelper.timesVibratedWithEffect(ticks.compose())) } @Test fun playHapticAtProgress_beforeNextDragThreshold_playsLowTicksOnce() { fun playHapticAtProgress_beforeNextDragThreshold_playsLowTicksOnce() = with(kosmos) { // GIVEN max velocity and a slider progress at half progress val firstProgress = 0.5f val firstTicks = generateTicksComposition(config.maxVelocityToScale, firstProgress) // Given a second slider progress event smaller than the progress threshold val secondProgress = firstProgress + max(0f, config.deltaProgressForDragThreshold - 0.01f) val secondProgress = firstProgress + max(0f, config.deltaProgressForDragThreshold - 0.01f) // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN two calls to play occur with the required threshold separation (time and progress) // WHEN two calls to play occur with the required threshold separation (time and // progress) sliderHapticFeedbackProvider.onProgress(firstProgress) clock.advanceTime(dragTextureThresholdMillis.toLong()) fakeSystemClock.advanceTime(dragTextureThresholdMillis.toLong()) sliderHapticFeedbackProvider.onProgress(secondProgress) // THEN Only the first compositions plays verify(vibratorHelper, times(1)) .vibrate(eq(firstTicks), any(VibrationAttributes::class.java)) verify(vibratorHelper, times(1)) .vibrate(any(VibrationEffect::class.java), any(VibrationAttributes::class.java)) assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(firstTicks)) assertEquals(/* expected= */ 1, vibratorHelper.totalVibrations) } @Test fun playHapticAtProgress_afterNextDragThreshold_playsLowTicksTwice() { fun playHapticAtProgress_afterNextDragThreshold_playsLowTicksTwice() = with(kosmos) { // GIVEN max velocity and a slider progress at half progress val firstProgress = 0.5f val firstTicks = generateTicksComposition(config.maxVelocityToScale, firstProgress) Loading @@ -188,22 +202,22 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { val secondTicks = generateTicksComposition(config.maxVelocityToScale, secondProgress) // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN two calls to play occur with the required threshold separation (time and progress) // WHEN two calls to play occur with the required threshold separation (time and // progress) sliderHapticFeedbackProvider.onProgress(firstProgress) clock.advanceTime(dragTextureThresholdMillis.toLong()) fakeSystemClock.advanceTime(dragTextureThresholdMillis.toLong()) sliderHapticFeedbackProvider.onProgress(secondProgress) // THEN the correct compositions play verify(vibratorHelper, times(1)) .vibrate(eq(firstTicks), any(VibrationAttributes::class.java)) verify(vibratorHelper, times(1)) .vibrate(eq(secondTicks), any(VibrationAttributes::class.java)) assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(firstTicks)) assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(secondTicks)) } @Test fun playHapticAtLowerBookend_afterPlayingAtProgress_playsTwice() { fun playHapticAtLowerBookend_afterPlayingAtProgress_playsTwice() = with(kosmos) { // GIVEN max velocity and slider progress val progress = 1f val expectedScale = Loading @@ -219,7 +233,9 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() Loading @@ -231,12 +247,15 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { sliderHapticFeedbackProvider.onLowerBookend() // THEN there are two bookend vibrations verify(vibratorHelper, times(2)) .vibrate(eq(bookendVibration), any(VibrationAttributes::class.java)) assertEquals( /* expected= */ 2, vibratorHelper.timesVibratedWithEffect(bookendVibration) ) } @Test fun playHapticAtUpperBookend_afterPlayingAtProgress_playsTwice() { fun playHapticAtUpperBookend_afterPlayingAtProgress_playsTwice() = with(kosmos) { // GIVEN max velocity and slider progress val progress = 1f val expectedScale = Loading @@ -252,7 +271,9 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { VibrationEffect.startComposition() .addPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), sliderHapticFeedbackProvider.scaleOnEdgeCollision( config.maxVelocityToScale ), ) .compose() Loading @@ -264,16 +285,19 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { sliderHapticFeedbackProvider.onUpperBookend() // THEN there are two bookend vibrations verify(vibratorHelper, times(2)) .vibrate(eq(bookendVibration), any(VibrationAttributes::class.java)) assertEquals( /* expected= */ 2, vibratorHelper.timesVibratedWithEffect(bookendVibration) ) } fun dragTextureLastProgress_afterDragTextureHaptics_keepsLastDragTextureProgress() { fun dragTextureLastProgress_afterDragTextureHaptics_keepsLastDragTextureProgress() = with(kosmos) { // GIVEN max velocity and a slider progress at half progress val progress = 0.5f // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN a drag texture plays sliderHapticFeedbackProvider.onProgress(progress) Loading @@ -283,12 +307,13 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { } @Test fun dragTextureLastProgress_afterDragTextureHaptics_resetsOnHandleReleased() { fun dragTextureLastProgress_afterDragTextureHaptics_resetsOnHandleReleased() = with(kosmos) { // GIVEN max velocity and a slider progress at half progress val progress = 0.5f // GIVEN system running for 1s clock.advanceTime(1000) fakeSystemClock.advanceTime(1000) // WHEN a drag texture plays sliderHapticFeedbackProvider.onProgress(progress) Loading
packages/SystemUI/tests/utils/src/com/android/systemui/haptics/EmptyVibrator.kt 0 → 100644 +40 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.haptics import android.os.VibrationAttributes import android.os.VibrationEffect import android.os.Vibrator /** A simple empty vibrator required for the [FakeVibratorHelper] */ class EmptyVibrator : Vibrator() { override fun cancel() {} override fun cancel(usageFilter: Int) {} override fun hasAmplitudeControl(): Boolean = true override fun hasVibrator(): Boolean = true override fun vibrate( uid: Int, opPkg: String, vibe: VibrationEffect, reason: String, attributes: VibrationAttributes, ) {} }
packages/SystemUI/tests/utils/src/com/android/systemui/haptics/FakeVibratorHelper.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.haptics import android.annotation.SuppressLint import android.media.AudioAttributes import android.os.VibrationAttributes import android.os.VibrationEffect import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock /** A fake [VibratorHelper] that only keeps track of the latest vibration effects delivered */ @SuppressLint("VisibleForTests") class FakeVibratorHelper : VibratorHelper(EmptyVibrator(), FakeExecutor(FakeSystemClock())) { /** A customizable map of primitive ids and their durations in ms */ val primitiveDurations: HashMap<Int, Int> = ALL_PRIMITIVE_DURATIONS private val vibrationEffectHistory = ArrayList<VibrationEffect>() val totalVibrations: Int get() = vibrationEffectHistory.size override fun vibrate(effect: VibrationEffect) { vibrationEffectHistory.add(effect) } override fun vibrate(effect: VibrationEffect, attributes: VibrationAttributes) = vibrate(effect) override fun vibrate(effect: VibrationEffect, attributes: AudioAttributes) = vibrate(effect) override fun vibrate( uid: Int, opPkg: String?, vibe: VibrationEffect, reason: String?, attributes: VibrationAttributes, ) = vibrate(vibe) override fun getPrimitiveDurations(vararg primitiveIds: Int): IntArray = primitiveIds.map { primitiveDurations[it] ?: 0 }.toIntArray() fun hasVibratedWithEffects(vararg effects: VibrationEffect): Boolean = vibrationEffectHistory.containsAll(effects.toList()) fun timesVibratedWithEffect(effect: VibrationEffect): Int = vibrationEffectHistory.count { it == effect } companion object { val ALL_PRIMITIVE_DURATIONS = hashMapOf( VibrationEffect.Composition.PRIMITIVE_NOOP to 0, VibrationEffect.Composition.PRIMITIVE_CLICK to 12, VibrationEffect.Composition.PRIMITIVE_THUD to 300, VibrationEffect.Composition.PRIMITIVE_SPIN to 133, VibrationEffect.Composition.PRIMITIVE_QUICK_RISE to 150, VibrationEffect.Composition.PRIMITIVE_SLOW_RISE to 500, VibrationEffect.Composition.PRIMITIVE_QUICK_FALL to 100, VibrationEffect.Composition.PRIMITIVE_TICK to 5, VibrationEffect.Composition.PRIMITIVE_LOW_TICK to 12, ) } }
packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt 0 → 100644 +21 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.haptics import com.android.systemui.kosmos.Kosmos var Kosmos.vibratorHelper by Kosmos.Fixture { FakeVibratorHelper() }