Extract VibratorInfo aggregation logic to a factory

The aggregation logic used to live in SystemVibrator.
To allow the server side code to do VibratorInfo aggregation, we're
extracting this logic into a factory class in android.os that can be
used by both the client and server side codes. SystemVibrator now
uses the new class to combine its VibratorInfos. The factory will
later be used within VibratorManagerService.

Unit tests for the aggregation logic used to live in VibratorTest.
They have now been moved to the factory class's test, along with more
tests checking the factory method (ID assignment, handling of empty
vibrator info list, and handling of a size-1 list).

Bug: 296358077
Test: atest VibratorInfoFactoryTest
Test: atest VibratorTest
Test: atest VibratorInfoTest
Change-Id: I27a8245e3e9f322ccd046b7d748141ec05293b99
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));
+    }
+}