diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java index 51749a7f85..6bae1baf8c 100644 --- a/src/com/android/launcher3/util/VibratorWrapper.java +++ b/src/com/android/launcher3/util/VibratorWrapper.java @@ -31,6 +31,7 @@ import android.os.Vibrator; import android.provider.Settings; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.Utilities; @@ -49,14 +50,14 @@ public class VibratorWrapper implements SafeCloseable { public static final VibrationEffect EFFECT_CLICK = createPredefined(VibrationEffect.EFFECT_CLICK); - private static final Uri HAPTIC_FEEDBACK_URI = - Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED); + @VisibleForTesting + static final Uri HAPTIC_FEEDBACK_URI = Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED); - private static final float LOW_TICK_SCALE = 0.9f; - private static final float DRAG_TEXTURE_SCALE = 0.03f; - private static final float DRAG_COMMIT_SCALE = 0.5f; - private static final float DRAG_BUMP_SCALE = 0.4f; - private static final int DRAG_TEXTURE_EFFECT_SIZE = 200; + @VisibleForTesting static final float LOW_TICK_SCALE = 0.9f; + @VisibleForTesting static final float DRAG_TEXTURE_SCALE = 0.03f; + @VisibleForTesting static final float DRAG_COMMIT_SCALE = 0.5f; + @VisibleForTesting static final float DRAG_BUMP_SCALE = 0.4f; + @VisibleForTesting static final int DRAG_TEXTURE_EFFECT_SIZE = 200; @Nullable private final VibrationEffect mDragEffect; @@ -73,22 +74,29 @@ public class VibratorWrapper implements SafeCloseable { */ public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK; - private final Context mContext; private final Vibrator mVibrator; private final boolean mHasVibrator; - private final SettingsCache.OnChangeListener mHapticChangeListener = + + private final SettingsCache mSettingsCache; + + @VisibleForTesting + final SettingsCache.OnChangeListener mHapticChangeListener = isEnabled -> mIsHapticFeedbackEnabled = isEnabled; private boolean mIsHapticFeedbackEnabled; private VibratorWrapper(Context context) { - mContext = context; - mVibrator = context.getSystemService(Vibrator.class); + this(context.getSystemService(Vibrator.class), SettingsCache.INSTANCE.get(context)); + } + + @VisibleForTesting + VibratorWrapper(Vibrator vibrator, SettingsCache settingsCache) { + mVibrator = vibrator; mHasVibrator = mVibrator.hasVibrator(); + mSettingsCache = settingsCache; if (mHasVibrator) { - SettingsCache cache = SettingsCache.INSTANCE.get(mContext); - cache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener); - mIsHapticFeedbackEnabled = cache.getValue(HAPTIC_FEEDBACK_URI, 0); + mSettingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener); + mIsHapticFeedbackEnabled = mSettingsCache.getValue(HAPTIC_FEEDBACK_URI, 0); } else { mIsHapticFeedbackEnabled = false; } @@ -98,12 +106,7 @@ public class VibratorWrapper implements SafeCloseable { // Drag texture, Commit, and Bump should only be used for premium phones. // Before using these haptics make sure check if the device can use it - VibrationEffect.Composition dragEffect = VibrationEffect.startComposition(); - for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) { - dragEffect.addPrimitive( - PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE); - } - mDragEffect = dragEffect.compose(); + mDragEffect = getDragEffect(); mCommitEffect = VibrationEffect.startComposition().addPrimitive( VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose(); mBumpEffect = VibrationEffect.startComposition().addPrimitive( @@ -124,8 +127,7 @@ public class VibratorWrapper implements SafeCloseable { @Override public void close() { if (mHasVibrator) { - SettingsCache.INSTANCE.get(mContext) - .unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener); + mSettingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener); } } @@ -215,4 +217,13 @@ public class VibratorWrapper implements SafeCloseable { vibrate(primitiveLowTickEffect); } } + + static VibrationEffect getDragEffect() { + VibrationEffect.Composition dragEffect = VibrationEffect.startComposition(); + for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) { + dragEffect.addPrimitive( + PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE); + } + return dragEffect.compose(); + } } diff --git a/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt new file mode 100644 index 0000000000..330c394199 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt @@ -0,0 +1,192 @@ +/* + * 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.launcher3.util + +import android.media.AudioAttributes +import android.os.SystemClock +import android.os.VibrationEffect +import android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK +import android.os.VibrationEffect.Composition.PRIMITIVE_TICK +import android.os.Vibrator +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.util.VibratorWrapper.HAPTIC_FEEDBACK_URI +import com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC +import com.android.launcher3.util.VibratorWrapper.VIBRATION_ATTRS +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.verifyNoMoreInteractions + +@SmallTest +@RunWith(AndroidJUnit4::class) +class VibratorWrapperTest { + + @Mock private lateinit var settingsCache: SettingsCache + @Mock private lateinit var vibrator: Vibrator + @Captor private lateinit var vibrationEffectCaptor: ArgumentCaptor + + private lateinit var underTest: VibratorWrapper + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + `when`(settingsCache.getValue(HAPTIC_FEEDBACK_URI, 0)).thenReturn(true) + `when`(vibrator.hasVibrator()).thenReturn(true) + `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_TICK)).thenReturn(true) + `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)).thenReturn(true) + `when`(vibrator.getPrimitiveDurations(PRIMITIVE_LOW_TICK)).thenReturn(intArrayOf(10)) + + underTest = VibratorWrapper(vibrator, settingsCache) + } + + @Test + fun init_register_onChangeListener() { + verify(settingsCache).register(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener) + } + + @Test + fun close_unregister_onChangeListener() { + underTest.close() + + verify(settingsCache).unregister(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener) + } + + @Test + fun vibrate() { + underTest.vibrate(OVERVIEW_HAPTIC) + + awaitTasksCompleted() + verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS) + } + + @Test + fun vibrate_primitive_id() { + underTest.vibrate(PRIMITIVE_TICK, 1f, OVERVIEW_HAPTIC) + + awaitTasksCompleted() + verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS)) + val expectedEffect = + VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 1f).compose() + assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect) + } + + @Test + fun vibrate_with_invalid_primitive_id_use_fallback_effect() { + underTest.vibrate(-1, 1f, OVERVIEW_HAPTIC) + + awaitTasksCompleted() + verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS) + } + + @Test + fun vibrate_for_taskbar_unstash() { + underTest.vibrateForTaskbarUnstash() + + awaitTasksCompleted() + verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS)) + val expectedEffect = + VibrationEffect.startComposition() + .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.LOW_TICK_SCALE) + .compose() + assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect) + } + + @Test + fun vibrate_for_drag_bump() { + underTest.vibrateForDragBump() + + awaitTasksCompleted() + verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS)) + val expectedEffect = + VibrationEffect.startComposition() + .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.DRAG_BUMP_SCALE) + .compose() + assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect) + } + + @Test + fun vibrate_for_drag_commit() { + underTest.vibrateForDragCommit() + + awaitTasksCompleted() + verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS)) + val expectedEffect = + VibrationEffect.startComposition() + .addPrimitive(PRIMITIVE_TICK, VibratorWrapper.DRAG_COMMIT_SCALE) + .compose() + assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect) + } + + @Test + fun vibrate_for_drag_texture() { + SystemClock.setCurrentTimeMillis(40000) + + underTest.vibrateForDragTexture() + + awaitTasksCompleted() + verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS)) + assertThat(vibrationEffectCaptor.value).isEqualTo(VibratorWrapper.getDragEffect()) + } + + @Test + fun vibrate_for_drag_texture_within_time_window_noOp() { + SystemClock.setCurrentTimeMillis(40000) + underTest.vibrateForDragTexture() + awaitTasksCompleted() + reset(vibrator) + + underTest.vibrateForDragTexture() + + verifyNoMoreInteractions(vibrator) + } + + @Test + fun haptic_feedback_disabled_no_vibrate() { + `when`(vibrator.hasVibrator()).thenReturn(false) + underTest = VibratorWrapper(vibrator, settingsCache) + + underTest.vibrate(OVERVIEW_HAPTIC) + + awaitTasksCompleted() + verify(vibrator, never()) + .vibrate(any(VibrationEffect::class.java), any(AudioAttributes::class.java)) + } + + @Test + fun cancel_vibrate() { + underTest.cancelVibrate() + + awaitTasksCompleted() + verify(vibrator).cancel() + } + + private fun awaitTasksCompleted() { + Executors.UI_HELPER_EXECUTOR.submit { null }.get() + } +}