Expose vibration primitives durations in Vibrator
Fix: 182477149
Test: VibratorTest
Change-Id: I4dd500fe6551645454ac9af24499098a74fd866e
diff --git a/core/api/current.txt b/core/api/current.txt
index 0639d72..0ee4a1a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -32064,6 +32064,7 @@
method @NonNull public boolean[] arePrimitivesSupported(@NonNull int...);
method @RequiresPermission(android.Manifest.permission.VIBRATE) public abstract void cancel();
method public int getId();
+ method @NonNull public int[] getPrimitiveDurations(@NonNull int...);
method public abstract boolean hasAmplitudeControl();
method public abstract boolean hasVibrator();
method @Deprecated @RequiresPermission(android.Manifest.permission.VIBRATE) public void vibrate(long);
diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java
index 2876775..b3502f3 100644
--- a/core/java/android/os/Vibrator.java
+++ b/core/java/android/os/Vibrator.java
@@ -552,6 +552,27 @@
}
/**
+ * Query the estimated durations of the given primitives.
+ *
+ * The returned array will be the same length as the query array and the value at a given index
+ * will contain the duration in milliseconds of the effect at the same index in the querying
+ * array.
+ *
+ * @param primitiveIds Which primitives to query for.
+ * @return The duration of each primitive, with zeroes for primitives that are not supported.
+ */
+ @NonNull
+ public int[] getPrimitiveDurations(
+ @NonNull @VibrationEffect.Composition.PrimitiveType int... primitiveIds) {
+ VibratorInfo info = getInfo();
+ int[] durations = new int[primitiveIds.length];
+ for (int i = 0; i < primitiveIds.length; i++) {
+ durations[i] = info.getPrimitiveDuration(primitiveIds[i]);
+ }
+ return durations;
+ }
+
+ /**
* Turn the vibrator off.
*/
@RequiresPermission(android.Manifest.permission.VIBRATE)
diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java
index c7d66f0..d73469c 100644
--- a/core/java/android/os/VibratorInfo.java
+++ b/core/java/android/os/VibratorInfo.java
@@ -25,6 +25,7 @@
import android.util.MathUtils;
import android.util.Range;
import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
import java.util.ArrayList;
import java.util.Arrays;
@@ -51,7 +52,7 @@
@Nullable
private final SparseBooleanArray mSupportedBraking;
@Nullable
- private final SparseBooleanArray mSupportedPrimitives;
+ private final SparseIntArray mSupportedPrimitives;
private final float mQFactor;
private final FrequencyMapping mFrequencyMapping;
@@ -60,19 +61,37 @@
mCapabilities = in.readLong();
mSupportedEffects = in.readSparseBooleanArray();
mSupportedBraking = in.readSparseBooleanArray();
- mSupportedPrimitives = in.readSparseBooleanArray();
+ mSupportedPrimitives = in.readSparseIntArray();
mQFactor = in.readFloat();
mFrequencyMapping = in.readParcelable(VibratorInfo.class.getClassLoader());
}
- /** @hide */
+ /**
+ * Default constructor.
+ *
+ * @param id The vibrator id.
+ * @param capabilities All capability flags of the vibrator, defined in IVibrator.CAP_*.
+ * @param supportedEffects All supported predefined effects, enum values from {@link
+ * android.hardware.vibrator.Effect}.
+ * @param supportedBraking All supported braking types, enum values from {@link Braking}.
+ * @param supportedPrimitives All supported primitive effects, enum values from {@link
+ * android.hardware.vibrator.CompositePrimitive}.
+ * @param primitiveDurations A mapping of primitive durations, where indexes are enum values
+ * from {@link android.hardware.vibrator.CompositePrimitive} and the
+ * values are estimated durations in milliseconds.
+ * @param qFactor The vibrator quality factor.
+ * @param frequencyMapping The description of the vibrator supported frequencies and max
+ * amplitude mappings.
+ * @hide
+ */
public VibratorInfo(int id, long capabilities, int[] supportedEffects, int[] supportedBraking,
- int[] supportedPrimitives, float qFactor, @NonNull FrequencyMapping frequencyMapping) {
+ int[] supportedPrimitives, int[] primitiveDurations, float qFactor,
+ @NonNull FrequencyMapping frequencyMapping) {
mId = id;
mCapabilities = capabilities;
mSupportedEffects = toSparseBooleanArray(supportedEffects);
mSupportedBraking = toSparseBooleanArray(supportedBraking);
- mSupportedPrimitives = toSparseBooleanArray(supportedPrimitives);
+ mSupportedPrimitives = toSparseIntArray(supportedPrimitives, primitiveDurations);
mQFactor = qFactor;
mFrequencyMapping = frequencyMapping;
}
@@ -100,7 +119,7 @@
dest.writeLong(mCapabilities);
dest.writeSparseBooleanArray(mSupportedEffects);
dest.writeSparseBooleanArray(mSupportedBraking);
- dest.writeSparseBooleanArray(mSupportedPrimitives);
+ dest.writeSparseIntArray(mSupportedPrimitives);
dest.writeFloat(mQFactor);
dest.writeParcelable(mFrequencyMapping, flags);
}
@@ -119,18 +138,41 @@
return false;
}
VibratorInfo that = (VibratorInfo) o;
+ if (mSupportedPrimitives == null || that.mSupportedPrimitives == null) {
+ if (mSupportedPrimitives != that.mSupportedPrimitives) {
+ return false;
+ }
+ } else {
+ if (mSupportedPrimitives.size() != that.mSupportedPrimitives.size()) {
+ return false;
+ }
+ for (int i = 0; i < mSupportedPrimitives.size(); i++) {
+ if (mSupportedPrimitives.keyAt(i) != that.mSupportedPrimitives.keyAt(i)) {
+ return false;
+ }
+ if (mSupportedPrimitives.valueAt(i) != that.mSupportedPrimitives.valueAt(i)) {
+ return false;
+ }
+ }
+ }
return mId == that.mId && mCapabilities == that.mCapabilities
&& Objects.equals(mSupportedEffects, that.mSupportedEffects)
&& Objects.equals(mSupportedBraking, that.mSupportedBraking)
- && Objects.equals(mSupportedPrimitives, that.mSupportedPrimitives)
&& Objects.equals(mQFactor, that.mQFactor)
&& Objects.equals(mFrequencyMapping, that.mFrequencyMapping);
}
@Override
public int hashCode() {
- return Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedBraking,
- mSupportedPrimitives, mQFactor, mFrequencyMapping);
+ int hashCode = Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedBraking,
+ mQFactor, mFrequencyMapping);
+ if (mSupportedPrimitives != null) {
+ for (int i = 0; i < mSupportedPrimitives.size(); i++) {
+ hashCode = 31 * hashCode + mSupportedPrimitives.keyAt(i);
+ hashCode = 31 * hashCode + mSupportedPrimitives.valueAt(i);
+ }
+ }
+ return hashCode;
}
@Override
@@ -206,7 +248,19 @@
public boolean isPrimitiveSupported(
@VibrationEffect.Composition.PrimitiveType int primitiveId) {
return hasCapability(IVibrator.CAP_COMPOSE_EFFECTS) && mSupportedPrimitives != null
- && mSupportedPrimitives.get(primitiveId);
+ && (mSupportedPrimitives.indexOfKey(primitiveId) >= 0);
+ }
+
+ /**
+ * Query the estimated duration of given primitive.
+ *
+ * @param primitiveId Which primitives to query for.
+ * @return The duration in milliseconds estimated for the primitive, or zero if primitive not
+ * supported.
+ */
+ public int getPrimitiveDuration(
+ @VibrationEffect.Composition.PrimitiveType int primitiveId) {
+ return mSupportedPrimitives.get(primitiveId);
}
/**
@@ -364,14 +418,37 @@
return names;
}
+ /**
+ * Create a {@link SparseBooleanArray} from given {@code supportedKeys} where each key is mapped
+ * to {@code true}.
+ */
@Nullable
- private static SparseBooleanArray toSparseBooleanArray(int[] values) {
- if (values == null) {
+ private static SparseBooleanArray toSparseBooleanArray(int[] supportedKeys) {
+ if (supportedKeys == null) {
return null;
}
SparseBooleanArray array = new SparseBooleanArray();
- for (int value : values) {
- array.put(value, true);
+ for (int key : supportedKeys) {
+ array.put(key, true);
+ }
+ return array;
+ }
+
+ /**
+ * Create a {@link SparseIntArray} from given {@code supportedKeys} where each key is mapped
+ * to the value indexed by it.
+ *
+ * <p>If {@code values} is null or does not contain a given key as a index, then zero is stored
+ * to the sparse array so it can still be used to query the supported keys.
+ */
+ @Nullable
+ private static SparseIntArray toSparseIntArray(int[] supportedKeys, int[] values) {
+ if (supportedKeys == null) {
+ return null;
+ }
+ SparseIntArray array = new SparseIntArray();
+ for (int key : supportedKeys) {
+ array.put(key, (values == null || key >= values.length) ? 0 : values[key]);
}
return array;
}
@@ -419,7 +496,20 @@
in.createFloatArray());
}
- /** @hide */
+ /**
+ * Default constructor.
+ *
+ * @param minFrequencyHz Minimum supported frequency, in hertz.
+ * @param resonantFrequencyHz The vibrator resonant frequency, in hertz.
+ * @param frequencyResolutionHz The frequency resolution, in hertz, used by the max
+ * amplitudes mapping.
+ * @param suggestedSafeRangeHz The suggested range, in hertz, for the safe relative
+ * frequency range represented by [-1, 1].
+ * @param maxAmplitudes The max amplitude supported by each supported frequency,
+ * starting at minimum frequency with jumps of frequency
+ * resolution.
+ * @hide
+ */
public FrequencyMapping(float minFrequencyHz, float resonantFrequencyHz,
float frequencyResolutionHz, float suggestedSafeRangeHz, float[] maxAmplitudes) {
mMinFrequencyHz = minFrequencyHz;
@@ -547,8 +637,10 @@
@Override
public int hashCode() {
- return Objects.hash(mMinFrequencyHz, mFrequencyResolutionHz, mFrequencyResolutionHz,
- mSuggestedSafeRangeHz, mMaxAmplitudes);
+ int hashCode = Objects.hash(mMinFrequencyHz, mFrequencyResolutionHz,
+ mFrequencyResolutionHz, mSuggestedSafeRangeHz);
+ hashCode = 31 * hashCode + Arrays.hashCode(mMaxAmplitudes);
+ return hashCode;
}
@Override
@@ -587,6 +679,7 @@
private int[] mSupportedEffects = null;
private int[] mSupportedBraking = null;
private int[] mSupportedPrimitives = null;
+ private int[] mPrimitiveDurations = new int[0];
private float mQFactor = Float.NaN;
private FrequencyMapping mFrequencyMapping =
new FrequencyMapping(Float.NaN, Float.NaN, Float.NaN, Float.NaN, null);
@@ -627,6 +720,16 @@
return this;
}
+ /** Configure the duration of a {@link android.hardware.vibrator.CompositePrimitive}. */
+ @NonNull
+ public Builder setPrimitiveDuration(int primitiveId, int duration) {
+ if (mPrimitiveDurations.length <= primitiveId) {
+ mPrimitiveDurations = Arrays.copyOf(mPrimitiveDurations, primitiveId + 1);
+ }
+ mPrimitiveDurations[primitiveId] = duration;
+ return this;
+ }
+
/** Configure the vibrator quality factor. */
@NonNull
public Builder setQFactor(float qFactor) {
@@ -645,7 +748,7 @@
@NonNull
public VibratorInfo build() {
return new VibratorInfo(mId, mCapabilities, mSupportedEffects, mSupportedBraking,
- mSupportedPrimitives, mQFactor, mFrequencyMapping);
+ mSupportedPrimitives, mPrimitiveDurations, mQFactor, mFrequencyMapping);
}
}
diff --git a/core/tests/coretests/src/android/os/VibratorInfoTest.java b/core/tests/coretests/src/android/os/VibratorInfoTest.java
index 3a80464..9880f8c 100644
--- a/core/tests/coretests/src/android/os/VibratorInfoTest.java
+++ b/core/tests/coretests/src/android/os/VibratorInfoTest.java
@@ -100,6 +100,17 @@
}
@Test
+ public void testGetPrimitiveDuration() {
+ VibratorInfo info = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
+ .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+ .setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK)
+ .setPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
+ .build();
+ assertEquals(20, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
+ assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_TICK));
+ }
+
+ @Test
public void testGetDefaultBraking_returnsFirstSupportedBraking() {
assertEquals(Braking.NONE, new VibratorInfo.Builder(
TEST_VIBRATOR_ID).build().getDefaultBraking());
@@ -251,12 +262,14 @@
.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
.setSupportedEffects(VibrationEffect.EFFECT_CLICK)
.setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK)
+ .setPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
.setQFactor(2f)
.setFrequencyMapping(TEST_FREQUENCY_MAPPING);
VibratorInfo complete = completeBuilder.build();
assertEquals(complete, complete);
assertEquals(complete, completeBuilder.build());
+ assertEquals(complete.hashCode(), completeBuilder.build().hashCode());
VibratorInfo completeWithComposeControl = completeBuilder
.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
@@ -279,6 +292,11 @@
.build();
assertNotEquals(complete, completeWithUnknownPrimitives);
+ VibratorInfo completeWithDifferentPrimitiveDuration = completeBuilder
+ .setPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+ .build();
+ assertNotEquals(complete, completeWithDifferentPrimitiveDuration);
+
VibratorInfo completeWithDifferentFrequencyMapping = completeBuilder
.setFrequencyMapping(new VibratorInfo.FrequencyMapping(TEST_MIN_FREQUENCY + 10,
TEST_RESONANT_FREQUENCY + 20, TEST_FREQUENCY_RESOLUTION + 5,
@@ -314,7 +332,8 @@
VibratorInfo original = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
.setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
.setSupportedEffects(VibrationEffect.EFFECT_CLICK)
- .setSupportedPrimitives(null)
+ .setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK)
+ .setPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
.setQFactor(Float.NaN)
.setFrequencyMapping(TEST_FREQUENCY_MAPPING)
.build();
@@ -324,5 +343,6 @@
parcel.setDataPosition(0);
VibratorInfo restored = VibratorInfo.CREATOR.createFromParcel(parcel);
assertEquals(original, restored);
+ assertEquals(original.hashCode(), restored.hashCode());
}
}
diff --git a/core/tests/coretests/src/android/os/VibratorTest.java b/core/tests/coretests/src/android/os/VibratorTest.java
index 6213285..8f9168b 100644
--- a/core/tests/coretests/src/android/os/VibratorTest.java
+++ b/core/tests/coretests/src/android/os/VibratorTest.java
@@ -80,6 +80,16 @@
}
@Test
+ public void getPrimitivesDurations_returnsArrayOfSameSize() {
+ assertEquals(0, mVibratorSpy.getPrimitiveDurations(new int[0]).length);
+ assertEquals(1, mVibratorSpy.getPrimitiveDurations(
+ new int[]{VibrationEffect.Composition.PRIMITIVE_CLICK}).length);
+ assertEquals(2, mVibratorSpy.getPrimitiveDurations(
+ new int[]{VibrationEffect.Composition.PRIMITIVE_CLICK,
+ VibrationEffect.Composition.PRIMITIVE_QUICK_RISE}).length);
+ }
+
+ @Test
public void vibrate_withAudioAttributes_createsVibrationAttributesWithSameUsage() {
VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
AudioAttributes audioAttributes = new AudioAttributes.Builder().setUsage(
diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
index b7fa796..698e3f7 100644
--- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp
+++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
@@ -370,6 +370,7 @@
jintArray supportedEffects = nullptr;
jintArray supportedBraking = nullptr;
jintArray supportedPrimitives = nullptr;
+ jintArray primitiveDurations = nullptr;
jfloatArray maxAmplitudes = nullptr;
if (info.supportedEffects.isOk()) {
@@ -390,6 +391,15 @@
env->SetIntArrayRegion(supportedPrimitives, 0, primitives.size(),
reinterpret_cast<jint*>(primitives.data()));
}
+ if (info.primitiveDurations.isOk()) {
+ std::vector<int32_t> durations;
+ for (auto duration : info.primitiveDurations.value()) {
+ durations.push_back(duration.count());
+ }
+ primitiveDurations = env->NewIntArray(durations.size());
+ env->SetIntArrayRegion(primitiveDurations, 0, durations.size(),
+ reinterpret_cast<jint*>(durations.data()));
+ }
if (info.maxAmplitudes.isOk()) {
std::vector<float> amplitudes = info.maxAmplitudes.value();
maxAmplitudes = env->NewFloatArray(amplitudes.size());
@@ -403,7 +413,7 @@
return env->NewObject(sVibratorInfoClass, sVibratorInfoCtor, wrapper->getVibratorId(),
capabilities, supportedEffects, supportedBraking, supportedPrimitives,
- qFactor, frequencyMapping);
+ primitiveDurations, qFactor, frequencyMapping);
}
static const JNINativeMethod method_table[] = {
@@ -450,9 +460,10 @@
sFrequencyMappingCtor = GetMethodIDOrDie(env, sFrequencyMappingClass, "<init>", "(FFFF[F)V");
jclass vibratorInfoClass = FindClassOrDie(env, "android/os/VibratorInfo");
- sVibratorInfoClass = static_cast<jclass>(env->NewGlobalRef(vibratorInfoClass));
- sVibratorInfoCtor = GetMethodIDOrDie(env, sVibratorInfoClass, "<init>",
- "(IJ[I[I[IFLandroid/os/VibratorInfo$FrequencyMapping;)V");
+ sVibratorInfoClass = (jclass)env->NewGlobalRef(vibratorInfoClass);
+ sVibratorInfoCtor =
+ GetMethodIDOrDie(env, sVibratorInfoClass, "<init>",
+ "(IJ[I[I[I[IFLandroid/os/VibratorInfo$FrequencyMapping;)V");
return jniRegisterNativeMethods(env,
"com/android/server/vibrator/VibratorController$NativeWrapper",
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
index 1d715c8..0585758 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -156,7 +156,7 @@
mMinFrequency, mResonantFrequency, mFrequencyResolution,
suggestedFrequencyRange, mMaxAmplitudes);
return new VibratorInfo(vibratorId, mCapabilities, mSupportedEffects, mSupportedBraking,
- mSupportedPrimitives, mQFactor, frequencyMapping);
+ mSupportedPrimitives, null, mQFactor, frequencyMapping);
}
private void applyLatency() {