Loading src/com/android/launcher3/util/VibratorWrapper.java +33 −22 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; } Loading @@ -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( Loading @@ -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); } } Loading Loading @@ -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(); } } tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt 0 → 100644 +192 −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.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<VibrationEffect> 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<Any> { null }.get() } } Loading
src/com/android/launcher3/util/VibratorWrapper.java +33 −22 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; } Loading @@ -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( Loading @@ -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); } } Loading Loading @@ -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(); } }
tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt 0 → 100644 +192 −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.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<VibrationEffect> 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<Any> { null }.get() } }