Merge "Extract VibratorInfo aggregation logic to a factory" into main
diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java
index bf72b1d..2cda787 100644
--- a/core/java/android/os/SystemVibrator.java
+++ b/core/java/android/os/SystemVibrator.java
@@ -18,26 +18,19 @@
 
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
-import android.hardware.vibrator.IVibrator;
+import android.os.vibrator.VibratorInfoFactory;
 import android.util.ArrayMap;
 import android.util.Log;
-import android.util.Range;
-import android.util.Slog;
 import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Objects;
 import java.util.concurrent.Executor;
-import java.util.function.Function;
 
 /**
  * Vibrator implementation that controls the main system vibrator.
@@ -82,7 +75,7 @@
             if (vibratorIds.length == 0) {
                 // It is known that the device has no vibrator, so cache and return info that
                 // reflects the lack of support for effects/primitives.
-                return mVibratorInfo = new NoVibratorInfo();
+                return mVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO;
             }
             VibratorInfo[] vibratorInfos = new VibratorInfo[vibratorIds.length];
             for (int i = 0; i < vibratorIds.length; i++) {
@@ -96,12 +89,7 @@
                 }
                 vibratorInfos[i] = vibrator.getInfo();
             }
-            if (vibratorInfos.length == 1) {
-                // Device has a single vibrator info, cache and return successfully loaded info.
-                return mVibratorInfo = new VibratorInfo(/* id= */ -1, vibratorInfos[0]);
-            }
-            // Device has multiple vibrators, generate a single info representing all of them.
-            return mVibratorInfo = new MultiVibratorInfo(vibratorInfos);
+            return mVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, vibratorInfos);
         }
     }
 
@@ -275,296 +263,6 @@
     }
 
     /**
-     * Represents a device with no vibrator as a single {@link VibratorInfo}.
-     *
-     * @hide
-     */
-    @VisibleForTesting
-    public static class NoVibratorInfo extends VibratorInfo {
-        public NoVibratorInfo() {
-            // Use empty arrays to indicate no support, while null would indicate support unknown.
-            super(/* id= */ -1,
-                    /* capabilities= */ 0,
-                    /* supportedEffects= */ new SparseBooleanArray(),
-                    /* supportedBraking= */ new SparseBooleanArray(),
-                    /* supportedPrimitives= */ new SparseIntArray(),
-                    /* primitiveDelayMax= */ 0,
-                    /* compositionSizeMax= */ 0,
-                    /* pwlePrimitiveDurationMax= */ 0,
-                    /* pwleSizeMax= */ 0,
-                    /* qFactor= */ Float.NaN,
-                    new FrequencyProfile(/* resonantFrequencyHz= */ Float.NaN,
-                            /* minFrequencyHz= */ Float.NaN,
-                            /* frequencyResolutionHz= */ Float.NaN,
-                            /* maxAmplitudes= */ null));
-        }
-    }
-
-    /**
-     * Represents multiple vibrator information as a single {@link VibratorInfo}.
-     *
-     * <p>This uses an intersection of all vibrators to decide the capabilities and effect/primitive
-     * support.
-     *
-     * @hide
-     */
-    @VisibleForTesting
-    public static class MultiVibratorInfo extends VibratorInfo {
-        // Epsilon used for float comparison applied in calculations for the merged info.
-        private static final float EPSILON = 1e-5f;
-
-        public MultiVibratorInfo(VibratorInfo[] vibrators) {
-            // Need to use an extra constructor to share the computation in super initialization.
-            this(vibrators, frequencyProfileIntersection(vibrators));
-        }
-
-        private MultiVibratorInfo(VibratorInfo[] vibrators,
-                VibratorInfo.FrequencyProfile mergedProfile) {
-            super(/* id= */ -1,
-                    capabilitiesIntersection(vibrators, mergedProfile.isEmpty()),
-                    supportedEffectsIntersection(vibrators),
-                    supportedBrakingIntersection(vibrators),
-                    supportedPrimitivesAndDurationsIntersection(vibrators),
-                    integerLimitIntersection(vibrators, VibratorInfo::getPrimitiveDelayMax),
-                    integerLimitIntersection(vibrators, VibratorInfo::getCompositionSizeMax),
-                    integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
-                    integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
-                    floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
-                    mergedProfile);
-        }
-
-        private static int capabilitiesIntersection(VibratorInfo[] infos,
-                boolean frequencyProfileIsEmpty) {
-            int intersection = ~0;
-            for (VibratorInfo info : infos) {
-                intersection &= info.getCapabilities();
-            }
-            if (frequencyProfileIsEmpty) {
-                // Revoke frequency control if the merged frequency profile ended up empty.
-                intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL;
-            }
-            return intersection;
-        }
-
-        @Nullable
-        private static SparseBooleanArray supportedBrakingIntersection(VibratorInfo[] infos) {
-            for (VibratorInfo info : infos) {
-                if (!info.isBrakingSupportKnown()) {
-                    // If one vibrator support is unknown, then the intersection is also unknown.
-                    return null;
-                }
-            }
-
-            SparseBooleanArray intersection = new SparseBooleanArray();
-            SparseBooleanArray firstVibratorBraking = infos[0].getSupportedBraking();
-
-            brakingIdLoop:
-            for (int i = 0; i < firstVibratorBraking.size(); i++) {
-                int brakingId = firstVibratorBraking.keyAt(i);
-                if (!firstVibratorBraking.valueAt(i)) {
-                    // The first vibrator already doesn't support this braking, so skip it.
-                    continue brakingIdLoop;
-                }
-
-                for (int j = 1; j < infos.length; j++) {
-                    if (!infos[j].hasBrakingSupport(brakingId)) {
-                        // One vibrator doesn't support this braking, so the intersection doesn't.
-                        continue brakingIdLoop;
-                    }
-                }
-
-                intersection.put(brakingId, true);
-            }
-
-            return intersection;
-        }
-
-        @Nullable
-        private static SparseBooleanArray supportedEffectsIntersection(VibratorInfo[] infos) {
-            for (VibratorInfo info : infos) {
-                if (!info.isEffectSupportKnown()) {
-                    // If one vibrator support is unknown, then the intersection is also unknown.
-                    return null;
-                }
-            }
-
-            SparseBooleanArray intersection = new SparseBooleanArray();
-            SparseBooleanArray firstVibratorEffects = infos[0].getSupportedEffects();
-
-            effectIdLoop:
-            for (int i = 0; i < firstVibratorEffects.size(); i++) {
-                int effectId = firstVibratorEffects.keyAt(i);
-                if (!firstVibratorEffects.valueAt(i)) {
-                    // The first vibrator already doesn't support this effect, so skip it.
-                    continue effectIdLoop;
-                }
-
-                for (int j = 1; j < infos.length; j++) {
-                    if (infos[j].isEffectSupported(effectId) != VIBRATION_EFFECT_SUPPORT_YES) {
-                        // One vibrator doesn't support this effect, so the intersection doesn't.
-                        continue effectIdLoop;
-                    }
-                }
-
-                intersection.put(effectId, true);
-            }
-
-            return intersection;
-        }
-
-        @NonNull
-        private static SparseIntArray supportedPrimitivesAndDurationsIntersection(
-                VibratorInfo[] infos) {
-            SparseIntArray intersection = new SparseIntArray();
-            SparseIntArray firstVibratorPrimitives = infos[0].getSupportedPrimitives();
-
-            primitiveIdLoop:
-            for (int i = 0; i < firstVibratorPrimitives.size(); i++) {
-                int primitiveId = firstVibratorPrimitives.keyAt(i);
-                int primitiveDuration = firstVibratorPrimitives.valueAt(i);
-                if (primitiveDuration == 0) {
-                    // The first vibrator already doesn't support this primitive, so skip it.
-                    continue primitiveIdLoop;
-                }
-
-                for (int j = 1; j < infos.length; j++) {
-                    int vibratorPrimitiveDuration = infos[j].getPrimitiveDuration(primitiveId);
-                    if (vibratorPrimitiveDuration == 0) {
-                        // One vibrator doesn't support this primitive, so the intersection doesn't.
-                        continue primitiveIdLoop;
-                    } else {
-                        // The primitive vibration duration is the maximum among all vibrators.
-                        primitiveDuration = Math.max(primitiveDuration, vibratorPrimitiveDuration);
-                    }
-                }
-
-                intersection.put(primitiveId, primitiveDuration);
-            }
-            return intersection;
-        }
-
-        private static int integerLimitIntersection(VibratorInfo[] infos,
-                Function<VibratorInfo, Integer> propertyGetter) {
-            int limit = 0; // Limit 0 means unlimited
-            for (VibratorInfo info : infos) {
-                int vibratorLimit = propertyGetter.apply(info);
-                if ((limit == 0) || (vibratorLimit > 0 && vibratorLimit < limit)) {
-                    // This vibrator is limited and intersection is unlimited or has a larger limit:
-                    // use smaller limit here for the intersection.
-                    limit = vibratorLimit;
-                }
-            }
-            return limit;
-        }
-
-        private static float floatPropertyIntersection(VibratorInfo[] infos,
-                Function<VibratorInfo, Float> propertyGetter) {
-            float property = propertyGetter.apply(infos[0]);
-            if (Float.isNaN(property)) {
-                // If one vibrator is undefined then the intersection is undefined.
-                return Float.NaN;
-            }
-            for (int i = 1; i < infos.length; i++) {
-                if (Float.compare(property, propertyGetter.apply(infos[i])) != 0) {
-                    // If one vibrator has a different value then the intersection is undefined.
-                    return Float.NaN;
-                }
-            }
-            return property;
-        }
-
-        @NonNull
-        private static FrequencyProfile frequencyProfileIntersection(VibratorInfo[] infos) {
-            float freqResolution = floatPropertyIntersection(infos,
-                    info -> info.getFrequencyProfile().getFrequencyResolutionHz());
-            float resonantFreq = floatPropertyIntersection(infos,
-                    VibratorInfo::getResonantFrequencyHz);
-            Range<Float> freqRange = frequencyRangeIntersection(infos, freqResolution);
-
-            if ((freqRange == null) || Float.isNaN(freqResolution)) {
-                return new FrequencyProfile(resonantFreq, Float.NaN, freqResolution, null);
-            }
-
-            int amplitudeCount =
-                    Math.round(1 + (freqRange.getUpper() - freqRange.getLower()) / freqResolution);
-            float[] maxAmplitudes = new float[amplitudeCount];
-
-            // Use MAX_VALUE here to ensure that the FrequencyProfile constructor called with this
-            // will fail if the loop below is broken and do not replace filled values with actual
-            // vibrator measurements.
-            Arrays.fill(maxAmplitudes, Float.MAX_VALUE);
-
-            for (VibratorInfo info : infos) {
-                Range<Float> vibratorFreqRange = info.getFrequencyProfile().getFrequencyRangeHz();
-                float[] vibratorMaxAmplitudes = info.getFrequencyProfile().getMaxAmplitudes();
-                int vibratorStartIdx = Math.round(
-                        (freqRange.getLower() - vibratorFreqRange.getLower()) / freqResolution);
-                int vibratorEndIdx = vibratorStartIdx + maxAmplitudes.length - 1;
-
-                if ((vibratorStartIdx < 0) || (vibratorEndIdx >= vibratorMaxAmplitudes.length)) {
-                    Slog.w(TAG, "Error calculating the intersection of vibrator frequency"
-                            + " profiles: attempted to fetch from vibrator "
-                            + info.getId() + " max amplitude with bad index " + vibratorStartIdx);
-                    return new FrequencyProfile(resonantFreq, Float.NaN, Float.NaN, null);
-                }
-
-                for (int i = 0; i < maxAmplitudes.length; i++) {
-                    maxAmplitudes[i] = Math.min(maxAmplitudes[i],
-                            vibratorMaxAmplitudes[vibratorStartIdx + i]);
-                }
-            }
-
-            return new FrequencyProfile(resonantFreq, freqRange.getLower(),
-                    freqResolution, maxAmplitudes);
-        }
-
-        @Nullable
-        private static Range<Float> frequencyRangeIntersection(VibratorInfo[] infos,
-                float frequencyResolution) {
-            Range<Float> firstRange = infos[0].getFrequencyProfile().getFrequencyRangeHz();
-            if (firstRange == null) {
-                // If one vibrator is undefined then the intersection is undefined.
-                return null;
-            }
-            float intersectionLower = firstRange.getLower();
-            float intersectionUpper = firstRange.getUpper();
-
-            // Generate the intersection of all vibrator supported ranges, making sure that both
-            // min supported frequencies are aligned w.r.t. the frequency resolution.
-
-            for (int i = 1; i < infos.length; i++) {
-                Range<Float> vibratorRange = infos[i].getFrequencyProfile().getFrequencyRangeHz();
-                if (vibratorRange == null) {
-                    // If one vibrator is undefined then the intersection is undefined.
-                    return null;
-                }
-
-                if ((vibratorRange.getLower() >= intersectionUpper)
-                        || (vibratorRange.getUpper() <= intersectionLower)) {
-                    // If the range and intersection are disjoint then the intersection is undefined
-                    return null;
-                }
-
-                float frequencyDelta = Math.abs(intersectionLower - vibratorRange.getLower());
-                if ((frequencyDelta % frequencyResolution) > EPSILON) {
-                    // If the intersection is not aligned with one vibrator then it's undefined
-                    return null;
-                }
-
-                intersectionLower = Math.max(intersectionLower, vibratorRange.getLower());
-                intersectionUpper = Math.min(intersectionUpper, vibratorRange.getUpper());
-            }
-
-            if ((intersectionUpper - intersectionLower) < frequencyResolution) {
-                // If the intersection is empty then it's undefined.
-                return null;
-            }
-
-            return Range.create(intersectionLower, intersectionUpper);
-        }
-    }
-
-    /**
      * Listener for all vibrators state change.
      *
      * <p>This registers a listener to all vibrators to merge the callbacks into a single state
diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java
index 0b7d7c3..4f8c24d 100644
--- a/core/java/android/os/VibratorInfo.java
+++ b/core/java/android/os/VibratorInfo.java
@@ -156,6 +156,16 @@
             return false;
         }
         VibratorInfo that = (VibratorInfo) o;
+        return mId == that.mId && equalContent(that);
+    }
+
+    /**
+     * Returns {@code true} only if the properties and capabilities of the provided info, except for
+     * the ID, equals to this info. Returns {@code false} otherwise.
+     *
+     * @hide
+     */
+    public boolean equalContent(VibratorInfo that) {
         int supportedPrimitivesCount = mSupportedPrimitives.size();
         if (supportedPrimitivesCount != that.mSupportedPrimitives.size()) {
             return false;
@@ -168,7 +178,7 @@
                 return false;
             }
         }
-        return mId == that.mId && mCapabilities == that.mCapabilities
+        return mCapabilities == that.mCapabilities
                 && mPrimitiveDelayMax == that.mPrimitiveDelayMax
                 && mCompositionSizeMax == that.mCompositionSizeMax
                 && mPwlePrimitiveDurationMax == that.mPwlePrimitiveDurationMax
@@ -445,7 +455,8 @@
         return mFrequencyProfile;
     }
 
-    protected long getCapabilities() {
+    /** Returns a single int representing all the capabilities of the vibrator. */
+    public long getCapabilities() {
         return mCapabilities;
     }
 
diff --git a/core/java/android/os/vibrator/MultiVibratorInfo.java b/core/java/android/os/vibrator/MultiVibratorInfo.java
new file mode 100644
index 0000000..5f32731
--- /dev/null
+++ b/core/java/android/os/vibrator/MultiVibratorInfo.java
@@ -0,0 +1,294 @@
+/*
+ * 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 android.os.vibrator;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.vibrator.IVibrator;
+import android.os.Vibrator;
+import android.os.VibratorInfo;
+import android.util.Range;
+import android.util.Slog;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import java.util.Arrays;
+import java.util.function.Function;
+
+/**
+ * Represents multiple vibrator information as a single {@link VibratorInfo}.
+ *
+ * <p>This uses an intersection of all vibrators to decide the capabilities and effect/primitive
+ * support.
+ *
+ * @hide
+ */
+public final class MultiVibratorInfo extends VibratorInfo {
+    private static final String TAG = "MultiVibratorInfo";
+
+    // Epsilon used for float comparison applied in calculations for the merged info.
+    private static final float EPSILON = 1e-5f;
+
+    public MultiVibratorInfo(int id, VibratorInfo[] vibrators) {
+        this(id, vibrators, frequencyProfileIntersection(vibrators));
+    }
+
+    private MultiVibratorInfo(
+            int id, VibratorInfo[] vibrators, VibratorInfo.FrequencyProfile mergedProfile) {
+        super(id,
+                capabilitiesIntersection(vibrators, mergedProfile.isEmpty()),
+                supportedEffectsIntersection(vibrators),
+                supportedBrakingIntersection(vibrators),
+                supportedPrimitivesAndDurationsIntersection(vibrators),
+                integerLimitIntersection(vibrators, VibratorInfo::getPrimitiveDelayMax),
+                integerLimitIntersection(vibrators, VibratorInfo::getCompositionSizeMax),
+                integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
+                integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
+                floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
+                mergedProfile);
+    }
+
+    private static int capabilitiesIntersection(VibratorInfo[] infos,
+            boolean frequencyProfileIsEmpty) {
+        int intersection = ~0;
+        for (VibratorInfo info : infos) {
+            intersection &= info.getCapabilities();
+        }
+        if (frequencyProfileIsEmpty) {
+            // Revoke frequency control if the merged frequency profile ended up empty.
+            intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL;
+        }
+        return intersection;
+    }
+
+    @Nullable
+    private static SparseBooleanArray supportedBrakingIntersection(VibratorInfo[] infos) {
+        for (VibratorInfo info : infos) {
+            if (!info.isBrakingSupportKnown()) {
+                // If one vibrator support is unknown, then the intersection is also unknown.
+                return null;
+            }
+        }
+
+        SparseBooleanArray intersection = new SparseBooleanArray();
+        SparseBooleanArray firstVibratorBraking = infos[0].getSupportedBraking();
+
+        brakingIdLoop:
+        for (int i = 0; i < firstVibratorBraking.size(); i++) {
+            int brakingId = firstVibratorBraking.keyAt(i);
+            if (!firstVibratorBraking.valueAt(i)) {
+                // The first vibrator already doesn't support this braking, so skip it.
+                continue brakingIdLoop;
+            }
+
+            for (int j = 1; j < infos.length; j++) {
+                if (!infos[j].hasBrakingSupport(brakingId)) {
+                    // One vibrator doesn't support this braking, so the intersection doesn't.
+                    continue brakingIdLoop;
+                }
+            }
+
+            intersection.put(brakingId, true);
+        }
+
+        return intersection;
+    }
+
+    @Nullable
+    private static SparseBooleanArray supportedEffectsIntersection(VibratorInfo[] infos) {
+        for (VibratorInfo info : infos) {
+            if (!info.isEffectSupportKnown()) {
+                // If one vibrator support is unknown, then the intersection is also unknown.
+                return null;
+            }
+        }
+
+        SparseBooleanArray intersection = new SparseBooleanArray();
+        SparseBooleanArray firstVibratorEffects = infos[0].getSupportedEffects();
+
+        effectIdLoop:
+        for (int i = 0; i < firstVibratorEffects.size(); i++) {
+            int effectId = firstVibratorEffects.keyAt(i);
+            if (!firstVibratorEffects.valueAt(i)) {
+                // The first vibrator already doesn't support this effect, so skip it.
+                continue effectIdLoop;
+            }
+
+            for (int j = 1; j < infos.length; j++) {
+                if (infos[j].isEffectSupported(effectId) != Vibrator.VIBRATION_EFFECT_SUPPORT_YES) {
+                    // One vibrator doesn't support this effect, so the intersection doesn't.
+                    continue effectIdLoop;
+                }
+            }
+
+            intersection.put(effectId, true);
+        }
+
+        return intersection;
+    }
+
+    @NonNull
+    private static SparseIntArray supportedPrimitivesAndDurationsIntersection(
+            VibratorInfo[] infos) {
+        SparseIntArray intersection = new SparseIntArray();
+        SparseIntArray firstVibratorPrimitives = infos[0].getSupportedPrimitives();
+
+        primitiveIdLoop:
+        for (int i = 0; i < firstVibratorPrimitives.size(); i++) {
+            int primitiveId = firstVibratorPrimitives.keyAt(i);
+            int primitiveDuration = firstVibratorPrimitives.valueAt(i);
+            if (primitiveDuration == 0) {
+                // The first vibrator already doesn't support this primitive, so skip it.
+                continue primitiveIdLoop;
+            }
+
+            for (int j = 1; j < infos.length; j++) {
+                int vibratorPrimitiveDuration = infos[j].getPrimitiveDuration(primitiveId);
+                if (vibratorPrimitiveDuration == 0) {
+                    // One vibrator doesn't support this primitive, so the intersection doesn't.
+                    continue primitiveIdLoop;
+                } else {
+                    // The primitive vibration duration is the maximum among all vibrators.
+                    primitiveDuration = Math.max(primitiveDuration, vibratorPrimitiveDuration);
+                }
+            }
+
+            intersection.put(primitiveId, primitiveDuration);
+        }
+        return intersection;
+    }
+
+    private static int integerLimitIntersection(VibratorInfo[] infos,
+            Function<VibratorInfo, Integer> propertyGetter) {
+        int limit = 0; // Limit 0 means unlimited
+        for (VibratorInfo info : infos) {
+            int vibratorLimit = propertyGetter.apply(info);
+            if ((limit == 0) || (vibratorLimit > 0 && vibratorLimit < limit)) {
+                // This vibrator is limited and intersection is unlimited or has a larger limit:
+                // use smaller limit here for the intersection.
+                limit = vibratorLimit;
+            }
+        }
+        return limit;
+    }
+
+    private static float floatPropertyIntersection(VibratorInfo[] infos,
+            Function<VibratorInfo, Float> propertyGetter) {
+        float property = propertyGetter.apply(infos[0]);
+        if (Float.isNaN(property)) {
+            // If one vibrator is undefined then the intersection is undefined.
+            return Float.NaN;
+        }
+        for (int i = 1; i < infos.length; i++) {
+            if (Float.compare(property, propertyGetter.apply(infos[i])) != 0) {
+                // If one vibrator has a different value then the intersection is undefined.
+                return Float.NaN;
+            }
+        }
+        return property;
+    }
+
+    @NonNull
+    private static FrequencyProfile frequencyProfileIntersection(VibratorInfo[] infos) {
+        float freqResolution = floatPropertyIntersection(infos,
+                info -> info.getFrequencyProfile().getFrequencyResolutionHz());
+        float resonantFreq = floatPropertyIntersection(infos,
+                VibratorInfo::getResonantFrequencyHz);
+        Range<Float> freqRange = frequencyRangeIntersection(infos, freqResolution);
+
+        if ((freqRange == null) || Float.isNaN(freqResolution)) {
+            return new FrequencyProfile(resonantFreq, Float.NaN, freqResolution, null);
+        }
+
+        int amplitudeCount =
+                Math.round(1 + (freqRange.getUpper() - freqRange.getLower()) / freqResolution);
+        float[] maxAmplitudes = new float[amplitudeCount];
+
+        // Use MAX_VALUE here to ensure that the FrequencyProfile constructor called with this
+        // will fail if the loop below is broken and do not replace filled values with actual
+        // vibrator measurements.
+        Arrays.fill(maxAmplitudes, Float.MAX_VALUE);
+
+        for (VibratorInfo info : infos) {
+            Range<Float> vibratorFreqRange = info.getFrequencyProfile().getFrequencyRangeHz();
+            float[] vibratorMaxAmplitudes = info.getFrequencyProfile().getMaxAmplitudes();
+            int vibratorStartIdx = Math.round(
+                    (freqRange.getLower() - vibratorFreqRange.getLower()) / freqResolution);
+            int vibratorEndIdx = vibratorStartIdx + maxAmplitudes.length - 1;
+
+            if ((vibratorStartIdx < 0) || (vibratorEndIdx >= vibratorMaxAmplitudes.length)) {
+                Slog.w(TAG, "Error calculating the intersection of vibrator frequency"
+                        + " profiles: attempted to fetch from vibrator "
+                        + info.getId() + " max amplitude with bad index " + vibratorStartIdx);
+                return new FrequencyProfile(resonantFreq, Float.NaN, Float.NaN, null);
+            }
+
+            for (int i = 0; i < maxAmplitudes.length; i++) {
+                maxAmplitudes[i] = Math.min(maxAmplitudes[i],
+                        vibratorMaxAmplitudes[vibratorStartIdx + i]);
+            }
+        }
+
+        return new FrequencyProfile(resonantFreq, freqRange.getLower(),
+                freqResolution, maxAmplitudes);
+    }
+
+    @Nullable
+    private static Range<Float> frequencyRangeIntersection(VibratorInfo[] infos,
+            float frequencyResolution) {
+        Range<Float> firstRange = infos[0].getFrequencyProfile().getFrequencyRangeHz();
+        if (firstRange == null) {
+            // If one vibrator is undefined then the intersection is undefined.
+            return null;
+        }
+        float intersectionLower = firstRange.getLower();
+        float intersectionUpper = firstRange.getUpper();
+
+        // Generate the intersection of all vibrator supported ranges, making sure that both
+        // min supported frequencies are aligned w.r.t. the frequency resolution.
+
+        for (int i = 1; i < infos.length; i++) {
+            Range<Float> vibratorRange = infos[i].getFrequencyProfile().getFrequencyRangeHz();
+            if (vibratorRange == null) {
+                // If one vibrator is undefined then the intersection is undefined.
+                return null;
+            }
+
+            if ((vibratorRange.getLower() >= intersectionUpper)
+                    || (vibratorRange.getUpper() <= intersectionLower)) {
+                // If the range and intersection are disjoint then the intersection is undefined
+                return null;
+            }
+
+            float frequencyDelta = Math.abs(intersectionLower - vibratorRange.getLower());
+            if ((frequencyDelta % frequencyResolution) > EPSILON) {
+                // If the intersection is not aligned with one vibrator then it's undefined
+                return null;
+            }
+
+            intersectionLower = Math.max(intersectionLower, vibratorRange.getLower());
+            intersectionUpper = Math.min(intersectionUpper, vibratorRange.getUpper());
+        }
+
+        if ((intersectionUpper - intersectionLower) < frequencyResolution) {
+            // If the intersection is empty then it's undefined.
+            return null;
+        }
+
+        return Range.create(intersectionLower, intersectionUpper);
+    }
+}
diff --git a/core/java/android/os/vibrator/VibratorInfoFactory.java b/core/java/android/os/vibrator/VibratorInfoFactory.java
new file mode 100644
index 0000000..d10d7ec
--- /dev/null
+++ b/core/java/android/os/vibrator/VibratorInfoFactory.java
@@ -0,0 +1,52 @@
+/*
+ * 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 android.os.vibrator;
+
+import android.annotation.NonNull;
+import android.os.VibratorInfo;
+
+/**
+ * Factory for creating {@link VibratorInfo}s.
+ *
+ * @hide
+ */
+public final class VibratorInfoFactory {
+    /**
+     * Creates a single {@link VibratorInfo} that is an intersection of a given collection of
+     * {@link VibratorInfo}s. That is, the capabilities of the returned info will be an
+     * intersection of that of the provided infos.
+     *
+     * @param id the ID for the new {@link VibratorInfo}.
+     * @param vibratorInfos the {@link VibratorInfo}s from which to create a single
+     *      {@link VibratorInfo}.
+     * @return a {@link VibratorInfo} that represents the intersection of {@code vibratorInfos}.
+     */
+    @NonNull
+    public static VibratorInfo create(int id, @NonNull VibratorInfo[] vibratorInfos) {
+        if (vibratorInfos.length == 0) {
+            return new VibratorInfo.Builder(id).build();
+        }
+        if (vibratorInfos.length == 1) {
+            // Create an equivalent info with the requested ID.
+            return new VibratorInfo(id, vibratorInfos[0]);
+        }
+        // Create a MultiVibratorInfo that intersects all the given infos and has the requested ID.
+        return new MultiVibratorInfo(id, vibratorInfos);
+    }
+
+    private VibratorInfoFactory() {}
+}
diff --git a/core/tests/vibrator/src/android/os/VibratorInfoTest.java b/core/tests/vibrator/src/android/os/VibratorInfoTest.java
index 808c4ec..73cd464 100644
--- a/core/tests/vibrator/src/android/os/VibratorInfoTest.java
+++ b/core/tests/vibrator/src/android/os/VibratorInfoTest.java
@@ -257,8 +257,13 @@
 
     @Test
     public void testEquals() {
-        VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
-                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+        VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID);
+        // Create a builder with a different ID, but same properties the same as the first one.
+        VibratorInfo.Builder completeBuilder2 = new VibratorInfo.Builder(TEST_VIBRATOR_ID + 2);
+
+        for (VibratorInfo.Builder builder :
+                new VibratorInfo.Builder[] {completeBuilder, completeBuilder2}) {
+            builder.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
                 .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
                 .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
                 .setPrimitiveDelayMax(100)
@@ -268,31 +273,43 @@
                 .setPwleSizeMax(20)
                 .setQFactor(2f)
                 .setFrequencyProfile(TEST_FREQUENCY_PROFILE);
+        }
         VibratorInfo complete = completeBuilder.build();
 
         assertEquals(complete, complete);
+        assertTrue(complete.equalContent(complete));
         assertEquals(complete, completeBuilder.build());
+        assertTrue(complete.equalContent(completeBuilder.build()));
         assertEquals(complete.hashCode(), completeBuilder.build().hashCode());
 
+        // The infos from the two builders should have equal content, but should not be equal due to
+        // their different IDs.
+        assertNotEquals(complete, completeBuilder2.build());
+        assertTrue(complete.equalContent(completeBuilder2.build()));
+
         VibratorInfo completeWithComposeControl = completeBuilder
                 .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
                 .build();
         assertNotEquals(complete, completeWithComposeControl);
+        assertFalse(complete.equalContent(completeWithComposeControl));
 
         VibratorInfo completeWithNoEffects = completeBuilder
                 .setSupportedEffects(new int[0])
                 .build();
         assertNotEquals(complete, completeWithNoEffects);
+        assertFalse(complete.equalContent(completeWithNoEffects));
 
         VibratorInfo completeWithUnknownEffects = completeBuilder
                 .setSupportedEffects(null)
                 .build();
         assertNotEquals(complete, completeWithUnknownEffects);
+        assertFalse(complete.equalContent(completeWithUnknownEffects));
 
         VibratorInfo completeWithDifferentPrimitiveDuration = completeBuilder
                 .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
                 .build();
         assertNotEquals(complete, completeWithDifferentPrimitiveDuration);
+        assertFalse(complete.equalContent(completeWithDifferentPrimitiveDuration));
 
         VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder
                 .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
@@ -302,31 +319,37 @@
                         TEST_AMPLITUDE_MAP))
                 .build();
         assertNotEquals(complete, completeWithDifferentFrequencyProfile);
+        assertFalse(complete.equalContent(completeWithDifferentFrequencyProfile));
 
         VibratorInfo completeWithEmptyFrequencyProfile = completeBuilder
                 .setFrequencyProfile(EMPTY_FREQUENCY_PROFILE)
                 .build();
         assertNotEquals(complete, completeWithEmptyFrequencyProfile);
+        assertFalse(complete.equalContent(completeWithEmptyFrequencyProfile));
 
         VibratorInfo completeWithUnknownQFactor = completeBuilder.setQFactor(Float.NaN).build();
         assertNotEquals(complete, completeWithUnknownQFactor);
+        assertFalse(complete.equalContent(completeWithUnknownQFactor));
 
         VibratorInfo completeWithDifferentQFactor = completeBuilder
                 .setQFactor(complete.getQFactor() + 3f)
                 .build();
         assertNotEquals(complete, completeWithDifferentQFactor);
+        assertFalse(complete.equalContent(completeWithDifferentQFactor));
 
         VibratorInfo unknownEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build();
         VibratorInfo knownEmptyEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
                 .setSupportedEffects(new int[0])
                 .build();
         assertNotEquals(unknownEffectSupport, knownEmptyEffectSupport);
+        assertFalse(unknownEffectSupport.equalContent(knownEmptyEffectSupport));
 
         VibratorInfo unknownBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build();
         VibratorInfo knownEmptyBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
                 .setSupportedBraking(new int[0])
                 .build();
         assertNotEquals(unknownBrakingSupport, knownEmptyBrakingSupport);
+        assertFalse(unknownBrakingSupport.equalContent(knownEmptyBrakingSupport));
     }
 
     @Test
diff --git a/core/tests/vibrator/src/android/os/VibratorTest.java b/core/tests/vibrator/src/android/os/VibratorTest.java
index 8141ca4..cfa12bb 100644
--- a/core/tests/vibrator/src/android/os/VibratorTest.java
+++ b/core/tests/vibrator/src/android/os/VibratorTest.java
@@ -37,7 +37,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.hardware.vibrator.IVibrator;
 import android.media.AudioAttributes;
 import android.os.test.TestLooper;
 
@@ -60,8 +59,6 @@
     @Rule
     public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
 
-    private static final float TEST_TOLERANCE = 1e-5f;
-
     private Context mContextSpy;
     private Vibrator mVibratorSpy;
     private TestLooper mTestLooper;
@@ -79,9 +76,6 @@
     @Test
     public void getId_returnsDefaultId() {
         assertEquals(-1, mVibratorSpy.getId());
-        assertEquals(-1, new SystemVibrator.NoVibratorInfo().getId());
-        assertEquals(-1, new SystemVibrator.MultiVibratorInfo(new VibratorInfo[] {
-                VibratorInfo.EMPTY_VIBRATOR_INFO, VibratorInfo.EMPTY_VIBRATOR_INFO }).getId());
     }
 
     @Test
@@ -95,53 +89,6 @@
     }
 
     @Test
-    public void areEffectsSupported_noVibrator_returnsAlwaysNo() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_NO,
-                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
-    }
-
-    @Test
-    public void areEffectsSupported_unsupportedInOneVibrator_returnsNo() {
-        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
-                .build();
-        VibratorInfo unsupportedVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setSupportedEffects(new int[0])
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
-        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_NO,
-                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
-    }
-
-    @Test
-    public void areEffectsSupported_unknownInOneVibrator_returnsUnknown() {
-        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
-                .build();
-        VibratorInfo unknownSupportVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{supportedVibrator, unknownSupportVibrator});
-        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN,
-                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
-    }
-
-    @Test
-    public void arePrimitivesSupported_supportedInAllVibrators_returnsYes() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_YES,
-                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
-    }
-
-    @Test
     public void arePrimitivesSupported_returnsArrayOfSameSize() {
         assertEquals(0, mVibratorSpy.arePrimitivesSupported(new int[0]).length);
         assertEquals(1, mVibratorSpy.arePrimitivesSupported(
@@ -152,39 +99,6 @@
     }
 
     @Test
-    public void arePrimitivesSupported_noVibrator_returnsAlwaysFalse() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-        assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void arePrimitivesSupported_unsupportedInOneVibrator_returnsFalse() {
-        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .build();
-        VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
-        assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void arePrimitivesSupported_supportedInAllVibrators_returnsTrue() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5)
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 15)
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-        assertTrue(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
     public void getPrimitivesDurations_returnsArrayOfSameSize() {
         assertEquals(0, mVibratorSpy.getPrimitiveDurations(new int[0]).length);
         assertEquals(1, mVibratorSpy.getPrimitiveDurations(
@@ -195,245 +109,6 @@
     }
 
     @Test
-    public void getPrimitivesDurations_noVibrator_returnsAlwaysZero() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-        assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void getPrimitivesDurations_unsupportedInOneVibrator_returnsZero() {
-        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .build();
-        VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
-        assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void getPrimitivesDurations_supportedInAllVibrators_returnsMaxDuration() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
-                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-        assertEquals(20, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
-    }
-
-    @Test
-    public void getQFactorAndResonantFrequency_noVibrator_returnsNaN() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-
-        assertTrue(Float.isNaN(info.getQFactor()));
-        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
-    }
-
-    @Test
-    public void getQFactorAndResonantFrequency_differentValues_returnsNaN() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setQFactor(1f)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setQFactor(2f)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-
-        assertTrue(Float.isNaN(info.getQFactor()));
-        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
-        assertEmptyFrequencyProfileAndControl(info);
-
-        // One vibrator with values undefined.
-        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, thirdVibrator});
-
-        assertTrue(Float.isNaN(info.getQFactor()));
-        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getQFactorAndResonantFrequency_sameValues_returnsValue() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setQFactor(10f)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
-                        /* resonantFrequencyHz= */ 11, 10, 0.5f, null))
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setQFactor(10f)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
-                        /* resonantFrequencyHz= */ 11, 5, 1, null))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator});
-
-        assertEquals(10f, info.getQFactor(), TEST_TOLERANCE);
-        assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE);
-
-        // No frequency range defined.
-        assertTrue(info.getFrequencyProfile().isEmpty());
-        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
-    }
-
-    @Test
-    public void getFrequencyProfile_noVibrator_returnsEmpty() {
-        VibratorInfo info = new SystemVibrator.NoVibratorInfo();
-
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getFrequencyProfile_differentResonantFrequencyOrResolutionValues_returnsEmpty() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, differentResonantFrequency});
-
-        assertEmptyFrequencyProfileAndControl(info);
-
-        VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2,
-                        new float[] { 0, 1 }))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, differentFrequencyResolution});
-
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getFrequencyProfile_missingValues_returnsEmpty() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, missingResonantFrequency});
-
-        assertEmptyFrequencyProfileAndControl(info);
-
-        VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1,
-                        new float[] { 0, 1 }))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, missingMinFrequency});
-
-        assertEmptyFrequencyProfileAndControl(info);
-
-        VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN,
-                        new float[] { 0, 1 }))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, missingFrequencyResolution});
-
-        assertEmptyFrequencyProfileAndControl(info);
-
-        VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, missingMaxAmplitudes});
-
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
-                        new float[] { 0, 1, 1, 0 }))
-                .build();
-        VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f,
-                        new float[] { 0, 1, 1, 0 }))
-                .build();
-        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
-                        new float[] { 0, 1, 1, 0 }))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, unalignedMinFrequency, thirdVibrator});
-
-        assertEmptyFrequencyProfileAndControl(info);
-    }
-
-    @Test
-    public void getFrequencyProfile_alignedProfiles_returnsIntersection() {
-        VibratorInfo firstVibrator = new VibratorInfo.Builder(/* id= */ 1)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
-                        new float[] { 0.5f, 1, 1, 0.5f }))
-                .build();
-        VibratorInfo secondVibrator = new VibratorInfo.Builder(/* id= */ 2)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
-                        new float[] { 1, 1, 1 }))
-                .build();
-        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
-                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
-                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
-                .build();
-        VibratorInfo info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator});
-
-        assertEquals(
-                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
-                info.getFrequencyProfile());
-        assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
-
-        // Third vibrator without frequency control capability.
-        thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
-                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
-                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
-                .build();
-        info = new SystemVibrator.MultiVibratorInfo(
-                new VibratorInfo[]{firstVibrator, secondVibrator, thirdVibrator});
-
-        assertEquals(
-                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
-                info.getFrequencyProfile());
-        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
-    }
-
-    @Test
     public void onVibratorStateChanged_noVibrator_registersNoListenerToVibratorManager() {
         VibratorManager mockVibratorManager = mock(VibratorManager.class);
         when(mockVibratorManager.getVibratorIds()).thenReturn(new int[0]);
@@ -577,12 +252,4 @@
         VibrationAttributes vibrationAttributes = captor.getValue();
         assertEquals(new VibrationAttributes.Builder().build(), vibrationAttributes);
     }
-
-    /**
-     * Asserts that the frequency profile is empty, and therefore frequency control isn't supported.
-     */
-    void assertEmptyFrequencyProfileAndControl(VibratorInfo info) {
-        assertTrue(info.getFrequencyProfile().isEmpty());
-        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
-    }
 }
diff --git a/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java b/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java
new file mode 100644
index 0000000..fc31ac4
--- /dev/null
+++ b/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java
@@ -0,0 +1,361 @@
+/*
+ * 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 android.os.vibrator;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.TestCase.assertEquals;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.os.VibratorInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MultiVibratorInfoTest {
+    private static final float TEST_TOLERANCE = 1e-5f;
+
+    @Test
+    public void testGetId() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setSupportedEffects(new int[0])
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 3,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertEquals(3, info.getId());
+    }
+
+    @Test
+    public void testIsEffectSupported_supportedInAllVibrators_returnsYes() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_TICK)
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_YES,
+                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
+    }
+
+    @Test
+    public void testIsEffectSupported_unsupportedInOneVibrator_returnsNo() {
+        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo unsupportedVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setSupportedEffects(new int[0])
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
+
+        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_NO,
+                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
+    }
+
+    @Test
+    public void testIsEffectSupported_unknownInOneVibrator_returnsUnknown() {
+        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo unknownSupportVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{supportedVibrator, unknownSupportVibrator});
+        assertEquals(Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN,
+                info.isEffectSupported(VibrationEffect.EFFECT_CLICK));
+    }
+
+    @Test
+    public void testIsPrimitiveSupported_unsupportedInOneVibrator_returnsFalse() {
+        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+        VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
+
+        assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
+    }
+
+    @Test
+    public void testIsPrimitiveSupported_supportedInAllVibrators_returnsTrue() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 5)
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 15)
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertTrue(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
+    }
+
+    @Test
+    public void testGetPrimitiveDuration_unsupportedInOneVibrator_returnsZero() {
+        VibratorInfo supportedVibrator = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+        VibratorInfo unsupportedVibrator = VibratorInfo.EMPTY_VIBRATOR_INFO;
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{supportedVibrator, unsupportedVibrator});
+
+        assertEquals(0, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
+    }
+
+    @Test
+    public void testGetPrimitiveDuration_supportedInAllVibrators_returnsMaxDuration() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertEquals(20, info.getPrimitiveDuration(VibrationEffect.Composition.PRIMITIVE_CLICK));
+    }
+
+    @Test
+    public void testGetQFactorAndResonantFrequency_differentValues_returnsNaN() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setQFactor(1f)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setQFactor(2f)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 2, 2, null))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertTrue(Float.isNaN(info.getQFactor()));
+        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+        assertEmptyFrequencyProfileAndControl(info);
+
+        // One vibrator with values undefined.
+        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3).build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, thirdVibrator});
+
+        assertTrue(Float.isNaN(info.getQFactor()));
+        assertTrue(Float.isNaN(info.getResonantFrequencyHz()));
+        assertEmptyFrequencyProfileAndControl(info);
+    }
+
+    @Test
+    public void testGetQFactorAndResonantFrequency_sameValues_returnsValue() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setQFactor(10f)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
+                        /* resonantFrequencyHz= */ 11, 10, 0.5f, null))
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setQFactor(10f)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
+                        /* resonantFrequencyHz= */ 11, 5, 1, null))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertEquals(10f, info.getQFactor(), TEST_TOLERANCE);
+        assertEquals(11f, info.getResonantFrequencyHz(), TEST_TOLERANCE);
+        // No frequency range defined.
+        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+    }
+
+    @Test
+    public void testGetFrequencyProfile_differentResonantFrequencyOrResolutions_returnsEmpty() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
+                        new float[] { 0, 1 }))
+                .build();
+        VibratorInfo differentResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(2, 1, 1,
+                        new float[] { 0, 1 }))
+                .build();
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, differentResonantFrequency});
+
+        assertEmptyFrequencyProfileAndControl(info);
+
+        VibratorInfo differentFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 2,
+                        new float[] { 0, 1 }))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, differentFrequencyResolution});
+
+        assertEmptyFrequencyProfileAndControl(info);
+    }
+
+    @Test
+    public void testGetFrequencyProfile_missingValues_returnsEmpty() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1,
+                        new float[] { 0, 1 }))
+                .build();
+        VibratorInfo missingResonantFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(Float.NaN, 1, 1,
+                        new float[] { 0, 1 }))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, missingResonantFrequency});
+
+        assertEmptyFrequencyProfileAndControl(info);
+
+        VibratorInfo missingMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, Float.NaN, 1,
+                        new float[] { 0, 1 }))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, missingMinFrequency});
+
+        assertEmptyFrequencyProfileAndControl(info);
+
+        VibratorInfo missingFrequencyResolution = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, Float.NaN,
+                        new float[] { 0, 1 }))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, missingFrequencyResolution});
+
+        assertEmptyFrequencyProfileAndControl(info);
+
+        VibratorInfo missingMaxAmplitudes = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(1, 1, 1, null))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, missingMaxAmplitudes});
+
+        assertEmptyFrequencyProfileAndControl(info);
+    }
+
+    @Test
+    public void testGetFrequencyProfile_unalignedMaxAmplitudes_returnsEmpty() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
+                        new float[] { 0, 1, 1, 0 }))
+                .build();
+        VibratorInfo unalignedMinFrequency = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.1f, 0.5f,
+                        new float[] { 0, 1, 1, 0 }))
+                .build();
+        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 0, 1, 1, 0 }))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, unalignedMinFrequency, thirdVibrator});
+
+        assertEmptyFrequencyProfileAndControl(info);
+    }
+
+    @Test
+    public void testGetFrequencyProfile_alignedProfiles_returnsIntersection() {
+        VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10, 0.5f,
+                        new float[] { 0.5f, 1, 1, 0.5f }))
+                .build();
+        VibratorInfo secondInfo = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 1, 1, 1 }))
+                .build();
+        VibratorInfo thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
+                .build();
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo, thirdVibrator});
+
+        assertEquals(
+                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
+                info.getFrequencyProfile());
+        assertEquals(true, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+
+        // Third vibrator without frequency control capability.
+        thirdVibrator = new VibratorInfo.Builder(/* id= */ 3)
+                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f,
+                        new float[] { 0.8f, 1, 0.8f, 0.5f }))
+                .build();
+        info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo, thirdVibrator});
+
+        assertEquals(
+                new VibratorInfo.FrequencyProfile(11, 10.5f, 0.5f, new float[] { 0.8f, 1, 0.5f }),
+                info.getFrequencyProfile());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+    }
+
+    /**
+     * Asserts that the frequency profile is empty, and therefore frequency control isn't supported.
+     */
+    private void assertEmptyFrequencyProfileAndControl(VibratorInfo info) {
+        assertTrue(info.getFrequencyProfile().isEmpty());
+        assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
+    }
+}
diff --git a/core/tests/vibrator/src/android/os/vibrator/VibratorInfoFactoryTest.java b/core/tests/vibrator/src/android/os/vibrator/VibratorInfoFactoryTest.java
new file mode 100644
index 0000000..df4822f
--- /dev/null
+++ b/core/tests/vibrator/src/android/os/vibrator/VibratorInfoFactoryTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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 android.os.vibrator;
+
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.TestCase.assertEquals;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibrationEffect;
+import android.os.VibratorInfo;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class VibratorInfoFactoryTest {
+
+    @Test
+    public void testCreatedInfo_hasTheRequestedId() {
+        // Empty info list.
+        VibratorInfo infoFromEmptyInfos =
+                VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {});
+        VibratorInfo info1 = new VibratorInfo.Builder(/* id= */ 1)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo info2 = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+        VibratorInfo infoFromOneInfo =
+                VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info1});
+        VibratorInfo infoFromTwoInfos =
+                VibratorInfoFactory.create(/* id= */ -3, new VibratorInfo[] {info1, info2});
+
+        assertEquals(3, infoFromEmptyInfos.getId());
+        assertEquals(-1, infoFromOneInfo.getId());
+        assertEquals(-3, infoFromTwoInfos.getId());
+    }
+
+    @Test
+    public void testCreatedInfo_fromEmptyVibratorInfos_returnsEmptyVibratorInfo() {
+        VibratorInfo info = VibratorInfoFactory.create(/* id= */ 2, new VibratorInfo[] {});
+
+        assertEqualContent(VibratorInfo.EMPTY_VIBRATOR_INFO, info);
+    }
+
+    @Test
+    public void testCreatedInfo_fromSingleVibratorInfo_hasEqualContent() {
+        VibratorInfo info = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_FREQUENCY_CONTROL)
+                .setSupportedEffects(VibrationEffect.EFFECT_TICK, VibrationEffect.EFFECT_THUD)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 30)
+                .build();
+
+        VibratorInfo createdInfo =
+                VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info});
+
+        assertEqualContent(info, createdInfo);
+    }
+
+    @Test
+    public void testCreatedInfo_hasEqualContentRegardlessOfSourceInfoOrder() {
+        VibratorInfo info1 = new VibratorInfo.Builder(/* id= */ 1)
+                .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK)
+                .build();
+        VibratorInfo info2 = new VibratorInfo.Builder(/* id= */ 2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .build();
+
+        assertEqualContent(
+                VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info1, info2}),
+                VibratorInfoFactory.create(/* id= */ -1, new VibratorInfo[] {info2, info1}));
+    }
+
+    @Test
+    public void testCreatedInfoContents() {
+        VibratorInfo info1 = new VibratorInfo.Builder(/* id= */ -1)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_FREQUENCY_CONTROL)
+                .setSupportedEffects(VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_POP)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 5)
+                .build();
+        VibratorInfo info2 = new VibratorInfo.Builder(/* id= */ -2)
+                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS | IVibrator.CAP_AMPLITUDE_CONTROL)
+                .setSupportedEffects(VibrationEffect.EFFECT_POP, VibrationEffect.EFFECT_THUD)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
+                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 20)
+                .build();
+        VibratorInfo info3 = new VibratorInfo.Builder(/* id= */ -3)
+                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                .build();
+
+        assertEquals(
+                new VibratorInfo.Builder(/* id= */ 3)
+                        .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
+                        .setSupportedEffects(VibrationEffect.EFFECT_POP)
+                        .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 20)
+                        .build(),
+                VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {info1, info2}));
+        assertEquals(
+                new VibratorInfo.Builder(/* id= */ 3)
+                        .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
+                        .build(),
+                VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {info2, info3}));
+        assertEquals(
+                new VibratorInfo.Builder(/* id= */ 3).build(),
+                VibratorInfoFactory.create(/* id= */ 3, new VibratorInfo[] {info1, info3}));
+    }
+
+    private static void assertEqualContent(VibratorInfo info1, VibratorInfo info2) {
+        assertTrue(info1.equalContent(info2));
+    }
+}