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() {