Merge "Use custom haptic vibrations in HapticFeedbackVibrationProvider" into main

This commit is contained in:
Yeabkal Wubshit
2023-07-21 17:06:40 +00:00
committed by Android (Google) Code Review
3 changed files with 435 additions and 29 deletions

View File

@@ -21,9 +21,16 @@ import android.content.res.Resources;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Slog;
import android.util.SparseArray;
import android.view.HapticFeedbackConstants;
import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;
/**
* Provides the {@link VibrationEffect} and {@link VibrationAttributes} for haptic feedback.
@@ -31,6 +38,8 @@ import java.io.PrintWriter;
* @hide
*/
public final class HapticFeedbackVibrationProvider {
private static final String TAG = "HapticFeedbackVibrationProvider";
private static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES =
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH);
private static final VibrationAttributes PHYSICAL_EMULATION_VIBRATION_ATTRIBUTES =
@@ -42,15 +51,41 @@ public final class HapticFeedbackVibrationProvider {
private final boolean mHapticTextHandleEnabled;
// Vibrator effect for haptic feedback during boot when safe mode is enabled.
private final VibrationEffect mSafeModeEnabledVibrationEffect;
// Haptic feedback vibration customizations specific to the device.
// If present and valid, a vibration here will be used for an effect.
// Otherwise, the system's default vibration will be used.
@Nullable private final SparseArray<VibrationEffect> mHapticCustomizations;
/** @hide */
public HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator) {
this(res, vibrator, loadHapticCustomizations(res));
}
/** @hide */
@VisibleForTesting HapticFeedbackVibrationProvider(
Resources res,
Vibrator vibrator,
@Nullable SparseArray<VibrationEffect> hapticCustomizations) {
mVibrator = vibrator;
mHapticTextHandleEnabled = res.getBoolean(
com.android.internal.R.bool.config_enableHapticTextHandle);
if (hapticCustomizations != null) {
// Clean up the customizations to remove vibrations which may not ever be used due to
// Vibrator properties or other device configurations.
removeUnsupportedVibrations(hapticCustomizations, vibrator);
if (hapticCustomizations.size() == 0) {
hapticCustomizations = null;
}
}
mHapticCustomizations = hapticCustomizations;
mSafeModeEnabledVibrationEffect =
VibrationSettings.createEffectFromResource(
res, com.android.internal.R.array.config_safeModeEnabledVibePattern);
effectHasCustomization(HapticFeedbackConstants.SAFE_MODE_ENABLED)
? mHapticCustomizations.get(HapticFeedbackConstants.SAFE_MODE_ENABLED)
: VibrationSettings.createEffectFromResource(
res,
com.android.internal.R.array.config_safeModeEnabledVibePattern);
}
/**
@@ -68,7 +103,7 @@ public final class HapticFeedbackVibrationProvider {
case HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE:
case HapticFeedbackConstants.SCROLL_TICK:
case HapticFeedbackConstants.SEGMENT_TICK:
return VibrationEffect.get(VibrationEffect.EFFECT_TICK);
return getVibration(effectId, VibrationEffect.EFFECT_TICK);
case HapticFeedbackConstants.TEXT_HANDLE_MOVE:
if (!mHapticTextHandleEnabled) {
@@ -77,13 +112,16 @@ public final class HapticFeedbackVibrationProvider {
// fallthrough
case HapticFeedbackConstants.CLOCK_TICK:
case HapticFeedbackConstants.SEGMENT_FREQUENT_TICK:
return VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK);
return getVibration(effectId, VibrationEffect.EFFECT_TEXTURE_TICK);
case HapticFeedbackConstants.KEYBOARD_RELEASE:
case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE:
case HapticFeedbackConstants.ENTRY_BUMP:
case HapticFeedbackConstants.DRAG_CROSSING:
return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
return getVibration(
effectId,
VibrationEffect.EFFECT_TICK,
/* fallbackForPredefinedEffect= */ false);
case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS
case HapticFeedbackConstants.VIRTUAL_KEY:
@@ -93,46 +131,42 @@ public final class HapticFeedbackVibrationProvider {
case HapticFeedbackConstants.GESTURE_START:
case HapticFeedbackConstants.SCROLL_ITEM_FOCUS:
case HapticFeedbackConstants.SCROLL_LIMIT:
return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
return getVibration(effectId, VibrationEffect.EFFECT_CLICK);
case HapticFeedbackConstants.LONG_PRESS:
case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON:
case HapticFeedbackConstants.DRAG_START:
case HapticFeedbackConstants.EDGE_SQUEEZE:
return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
return getVibration(effectId, VibrationEffect.EFFECT_HEAVY_CLICK);
case HapticFeedbackConstants.REJECT:
return VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
return getVibration(effectId, VibrationEffect.EFFECT_DOUBLE_CLICK);
case HapticFeedbackConstants.SAFE_MODE_ENABLED:
return mSafeModeEnabledVibrationEffect;
case HapticFeedbackConstants.ASSISTANT_BUTTON:
if (mVibrator.areAllPrimitivesSupported(
VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
VibrationEffect.Composition.PRIMITIVE_TICK)) {
// quiet ramp, short pause, then sharp tick
return VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
.compose();
}
// fallback for devices without composition support
return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
return getAssistantButtonVibration();
case HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE:
return getScaledPrimitiveOrElseEffect(
VibrationEffect.Composition.PRIMITIVE_TICK, 0.4f,
return getVibration(
effectId,
VibrationEffect.Composition.PRIMITIVE_TICK,
/* primitiveScale= */ 0.4f,
VibrationEffect.EFFECT_TEXTURE_TICK);
case HapticFeedbackConstants.TOGGLE_ON:
return getScaledPrimitiveOrElseEffect(
VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f,
return getVibration(
effectId,
VibrationEffect.Composition.PRIMITIVE_TICK,
/* primitiveScale= */ 0.5f,
VibrationEffect.EFFECT_TICK);
case HapticFeedbackConstants.TOGGLE_OFF:
return getScaledPrimitiveOrElseEffect(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK, 0.2f,
return getVibration(
effectId,
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
/* primitiveScale= */ 0.2f,
VibrationEffect.EFFECT_TEXTURE_TICK);
case HapticFeedbackConstants.NO_HAPTICS:
@@ -181,14 +215,100 @@ public final class HapticFeedbackVibrationProvider {
pw.print("mHapticTextHandleEnabled="); pw.println(mHapticTextHandleEnabled);
}
private VibrationEffect getScaledPrimitiveOrElseEffect(
int primitiveId, float scale, int elseEffectId) {
private VibrationEffect getVibration(int effectId, int predefinedVibrationEffectId) {
return getVibration(
effectId, predefinedVibrationEffectId, /* fallbackForPredefinedEffect= */ true);
}
/**
* Returns the customized vibration for {@code hapticFeedbackId}, or
* {@code predefinedVibrationEffectId} if a customization does not exist for the haptic
* feedback.
*
* <p>If a customization does not exist and the default predefined effect is to be returned,
* {@code fallbackForPredefinedEffect} will be used to decide whether or not to fallback
* to a generic pattern if the predefined effect is not hardware supported.
*
* @see VibrationEffect#get(int, boolean)
*/
private VibrationEffect getVibration(
int hapticFeedbackId,
int predefinedVibrationEffectId,
boolean fallbackForPredefinedEffect) {
if (effectHasCustomization(hapticFeedbackId)) {
return mHapticCustomizations.get(hapticFeedbackId);
}
return VibrationEffect.get(predefinedVibrationEffectId, fallbackForPredefinedEffect);
}
/**
* Returns the customized vibration for {@code hapticFeedbackId}, or some fallback vibration if
* a customization does not exist for the ID.
*
* <p>The fallback will be a primitive composition formed of {@code primitiveId} and
* {@code primitiveScale}, if the primitive is supported. Otherwise, it will be a predefined
* vibration of {@code elsePredefinedVibrationEffectId}.
*/
private VibrationEffect getVibration(
int hapticFeedbackId,
int primitiveId,
float primitiveScale,
int elsePredefinedVibrationEffectId) {
if (effectHasCustomization(hapticFeedbackId)) {
return mHapticCustomizations.get(hapticFeedbackId);
}
if (mVibrator.areAllPrimitivesSupported(primitiveId)) {
return VibrationEffect.startComposition()
.addPrimitive(primitiveId, scale)
.addPrimitive(primitiveId, primitiveScale)
.compose();
} else {
return VibrationEffect.get(elseEffectId);
return VibrationEffect.get(elsePredefinedVibrationEffectId);
}
}
private VibrationEffect getAssistantButtonVibration() {
if (effectHasCustomization(HapticFeedbackConstants.ASSISTANT_BUTTON)) {
return mHapticCustomizations.get(HapticFeedbackConstants.ASSISTANT_BUTTON);
}
if (mVibrator.areAllPrimitivesSupported(
VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
VibrationEffect.Composition.PRIMITIVE_TICK)) {
// quiet ramp, short pause, then sharp tick
return VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
.compose();
}
// fallback for devices without composition support
return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
}
private boolean effectHasCustomization(int effectId) {
return mHapticCustomizations != null && mHapticCustomizations.contains(effectId);
}
@Nullable
private static SparseArray<VibrationEffect> loadHapticCustomizations(Resources res) {
try {
return HapticFeedbackCustomization.loadVibrations(res);
} catch (IOException | HapticFeedbackCustomization.CustomizationParserException e) {
Slog.e(TAG, "Unable to load haptic customizations.", e);
return null;
}
}
private static void removeUnsupportedVibrations(
SparseArray<VibrationEffect> customizations, Vibrator vibrator) {
Set<Integer> keysToRemove = new HashSet<>();
for (int i = 0; i < customizations.size(); i++) {
int key = customizations.keyAt(i);
if (!vibrator.areVibrationFeaturesSupported(customizations.get(key))) {
keysToRemove.add(key);
}
}
for (int key : keysToRemove) {
customizations.remove(key);
}
}
}

View File

@@ -0,0 +1,261 @@
/*
* Copyright 2023 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.server.vibrator;
import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK;
import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK;
import static android.os.VibrationEffect.EFFECT_TEXTURE_TICK;
import static android.os.VibrationEffect.EFFECT_TICK;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
import static android.view.HapticFeedbackConstants.CONTEXT_CLICK;
import static android.view.HapticFeedbackConstants.SAFE_MODE_ENABLED;
import static android.view.HapticFeedbackConstants.TEXT_HANDLE_MOVE;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import android.os.VibrationEffect;
import android.os.test.FakeVibrator;
import android.util.AtomicFile;
import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
import com.android.internal.R;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.io.File;
import java.io.FileOutputStream;
public class HapticFeedbackVibrationProviderTest {
@Rule public MockitoRule rule = MockitoJUnit.rule();
private static final VibrationEffect PRIMITIVE_TICK_EFFECT =
VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 0.2497f).compose();
private static final VibrationEffect PRIMITIVE_CLICK_EFFECT =
VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK, 0.3497f).compose();
private Context mContext = InstrumentationRegistry.getContext();
private FakeVibrator mVibrator = new FakeVibrator(mContext);
@Mock private Resources mResourcesMock;
@Test
public void testNonExistentCustomization_useDefault() throws Exception {
// No customization file is set.
HapticFeedbackVibrationProvider hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
.isEqualTo(VibrationEffect.get(EFFECT_TICK));
// The customization file specifies no customization.
setupCustomizationFile("<haptic-feedback-constants></haptic-feedback-constants>");
hapticProvider = new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
.isEqualTo(VibrationEffect.get(EFFECT_TICK));
}
@Test
public void testExceptionParsingCustomizations_useDefault() throws Exception {
setupCustomizationFile("<bad-xml></bad-xml>");
HapticFeedbackVibrationProvider hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
.isEqualTo(VibrationEffect.get(EFFECT_TICK));
}
@Test
public void testUseValidCustomizedVibration() throws Exception {
mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
SparseArray<VibrationEffect> customizations = new SparseArray<>();
customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);
HapticFeedbackVibrationProvider hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
// The override for `CONTEXT_CLICK` is used.
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
.isEqualTo(PRIMITIVE_CLICK_EFFECT);
// `CLOCK_TICK` has no override, so the default vibration is used.
assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK))
.isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
}
@Test
public void testDoNotUseInvalidCustomizedVibration() throws Exception {
mockVibratorPrimitiveSupport(new int[] {});
SparseArray<VibrationEffect> customizations = new SparseArray<>();
customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);
HapticFeedbackVibrationProvider hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
// The override for `CONTEXT_CLICK` is not used because the vibration is not supported.
assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
.isEqualTo(VibrationEffect.get(EFFECT_TICK));
// `CLOCK_TICK` has no override, so the default vibration is used.
assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK))
.isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
}
@Test
public void testHapticTextDisabled_noVibrationReturnedForTextHandleMove() throws Exception {
mockHapticTextSupport(false);
mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
SparseArray<VibrationEffect> customizations = new SparseArray<>();
customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT);
// Test with a customization available for `TEXT_HANDLE_MOVE`.
HapticFeedbackVibrationProvider hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();
// Test with no customization available for `TEXT_HANDLE_MOVE`.
hapticProvider =
new HapticFeedbackVibrationProvider(
mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();
}
@Test
public void testHapticTextEnabled_vibrationReturnedForTextHandleMove() throws Exception {
mockHapticTextSupport(true);
mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
SparseArray<VibrationEffect> customizations = new SparseArray<>();
customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT);
// Test with a customization available for `TEXT_HANDLE_MOVE`.
HapticFeedbackVibrationProvider hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
.isEqualTo(PRIMITIVE_CLICK_EFFECT);
// Test with no customization available for `TEXT_HANDLE_MOVE`.
hapticProvider =
new HapticFeedbackVibrationProvider(
mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
.isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
}
@Test
public void testValidCustomizationPresentForSafeModeEnabled_usedRegardlessOfVibrationResource()
throws Exception {
mockSafeModeEnabledVibration(10, 20, 30, 40);
mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
SparseArray<VibrationEffect> customizations = new SparseArray<>();
customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
HapticFeedbackVibrationProvider hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
.isEqualTo(PRIMITIVE_CLICK_EFFECT);
mockSafeModeEnabledVibration(null);
hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
.isEqualTo(PRIMITIVE_CLICK_EFFECT);
}
@Test
public void testNoValidCustomizationPresentForSafeModeEnabled_resourceBasedVibrationUsed()
throws Exception {
mockSafeModeEnabledVibration(10, 20, 30, 40);
SparseArray<VibrationEffect> customizations = new SparseArray<>();
customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
// Test with a customization that is not supported by the vibrator.
HapticFeedbackVibrationProvider hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
.isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));
// Test with no customizations.
hapticProvider =
new HapticFeedbackVibrationProvider(
mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
.isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));
}
@Test
public void testNoValidCustomizationAndResourcePresentForSafeModeEnabled_noVibrationUsed()
throws Exception {
mockSafeModeEnabledVibration(null);
SparseArray<VibrationEffect> customizations = new SparseArray<>();
customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
// Test with a customization that is not supported by the vibrator.
HapticFeedbackVibrationProvider hapticProvider =
new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();
// Test with no customizations.
hapticProvider =
new HapticFeedbackVibrationProvider(
mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();
}
private void mockVibratorPrimitiveSupport(int... supportedPrimitives) {
mVibrator = new FakeVibrator(mContext, supportedPrimitives);
}
private void mockHapticTextSupport(boolean supported) {
when(mResourcesMock.getBoolean(R.bool.config_enableHapticTextHandle)).thenReturn(supported);
}
private void mockSafeModeEnabledVibration(int... vibrationPattern) {
when(mResourcesMock.getIntArray(R.array.config_safeModeEnabledVibePattern))
.thenReturn(vibrationPattern);
}
private void setupCustomizationFile(String xml) throws Exception {
File file = new File(mContext.getCacheDir(), "test.xml");
file.createNewFile();
AtomicFile atomicXmlFile = new AtomicFile(file);
FileOutputStream fos = atomicXmlFile.startWrite();
fos.write(xml.getBytes());
atomicXmlFile.finishWrite(fos);
when(mResourcesMock.getString(R.string.config_hapticFeedbackCustomizationFile))
.thenReturn(file.getAbsolutePath());
}
}

View File

@@ -18,15 +18,23 @@ package android.os.test;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.vibrator.IVibrator;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorInfo;
/** Fake implementation of {@link Vibrator} for service tests. */
public final class FakeVibrator extends Vibrator {
private final VibratorInfo mVibratorInfo;
public FakeVibrator(Context context) {
this(context, /* supportedPrimitives= */ null);
}
public FakeVibrator(Context context, int[] supportedPrimitives) {
super(context);
mVibratorInfo = buildInfoSupportingPrimitives(supportedPrimitives);
}
@Override
@@ -39,6 +47,11 @@ public final class FakeVibrator extends Vibrator {
return true;
}
@Override
public VibratorInfo getInfo() {
return mVibratorInfo;
}
@Override
public void vibrate(int uid, String opPkg, @NonNull VibrationEffect vibe, String reason,
@NonNull VibrationAttributes attributes) {
@@ -51,4 +64,16 @@ public final class FakeVibrator extends Vibrator {
@Override
public void cancel(int usageFilter) {
}
private static VibratorInfo buildInfoSupportingPrimitives(int... primitives) {
if (primitives == null || primitives.length == 0) {
return VibratorInfo.EMPTY_VIBRATOR_INFO;
}
VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1)
.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
for (int primitive : primitives) {
builder.setSupportedPrimitive(primitive, 10);
}
return builder.build();
}
}