Add new vibrator frequency profile for PWLE v2

This new vibrator frequency profile provides developers with richer information about the vibrators capabilities, including:
1. The minimum and maximum frequencies supported by the vibrator.
2. The maximum output acceleration the vibrator can achieve.
3. Retrieve frequency range for a specified minimum output acceleration

This profile will only be available if the device supports frequency control.

Bug: 347034419
Flag: android.os.vibrator.normalized_pwle_effects
Test: atest FrameworksVibratorCoreTests
Change-Id: I9a16b9b4932d95e8eb8c89bfa43f94920ed9e1ee
diff --git a/core/api/current.txt b/core/api/current.txt
index 44b3c62..ca45d1b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -34334,6 +34334,7 @@
     method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public boolean areEnvelopeEffectsSupported();
     method @NonNull public boolean[] arePrimitivesSupported(@NonNull int...);
     method @RequiresPermission(android.Manifest.permission.VIBRATE) public abstract void cancel();
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @Nullable public android.os.vibrator.VibratorFrequencyProfile getFrequencyProfile();
     method public int getId();
     method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public int getMaxEnvelopeEffectControlPointDurationMillis();
     method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public int getMaxEnvelopeEffectDurationMillis();
@@ -34687,6 +34688,19 @@
 
 }
 
+package android.os.vibrator {
+
+  @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class VibratorFrequencyProfile {
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.util.SparseArray<java.lang.Float> getFrequenciesOutputAcceleration();
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @Nullable public android.util.Range<java.lang.Float> getFrequencyRange(float);
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public float getMaxFrequencyHz();
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public float getMaxOutputAccelerationGs();
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public float getMinFrequencyHz();
+    method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public float getOutputAccelerationGs(float);
+  }
+
+}
+
 package android.preference {
 
   @Deprecated public class CheckBoxPreference extends android.preference.TwoStatePreference {
diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java
index 7327630..c4c4580 100644
--- a/core/java/android/os/Vibrator.java
+++ b/core/java/android/os/Vibrator.java
@@ -34,6 +34,7 @@
 import android.media.AudioAttributes;
 import android.os.vibrator.Flags;
 import android.os.vibrator.VibrationConfig;
+import android.os.vibrator.VibratorFrequencyProfile;
 import android.os.vibrator.VibratorFrequencyProfileLegacy;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
@@ -303,6 +304,28 @@
     }
 
     /**
+     * Gets the profile that describes the vibrator output across the supported frequency range.
+     *
+     * <p>The profile describes the output acceleration that the device can reach when it
+     * vibrates at different frequencies.
+     *
+     * @return The frequency profile for this vibrator, or null if the vibrator does not have
+     * frequency control. If this vibrator is a composite of multiple physical devices then this
+     * will return a profile supported in all devices, or null if the intersection is empty or not
+     * available.
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    @Nullable
+    public VibratorFrequencyProfile getFrequencyProfile() {
+        VibratorInfo.FrequencyProfile frequencyProfile = getInfo().getFrequencyProfile();
+        if (frequencyProfile.isEmpty()) {
+            return null;
+        }
+
+        return new VibratorFrequencyProfile(frequencyProfile);
+    }
+
+    /**
      * Return the maximum amplitude the vibrator can play using the audio haptic channels.
      *
      * <p>This is a positive value, or {@link Float#NaN NaN} if it's unknown. If this returns a
diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java
index f7fff39..9419032 100644
--- a/core/java/android/os/VibratorInfo.java
+++ b/core/java/android/os/VibratorInfo.java
@@ -20,8 +20,10 @@
 import android.annotation.Nullable;
 import android.hardware.vibrator.Braking;
 import android.hardware.vibrator.IVibrator;
+import android.os.vibrator.Flags;
 import android.util.IndentingPrintWriter;
 import android.util.MathUtils;
+import android.util.Pair;
 import android.util.Range;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
@@ -30,8 +32,11 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.TreeMap;
 
 /**
  * A VibratorInfo describes the capabilities of a {@link Vibrator}.
@@ -60,6 +65,7 @@
     private final int mPwleSizeMax;
     private final float mQFactor;
     private final FrequencyProfileLegacy mFrequencyProfileLegacy;
+    private final FrequencyProfile mFrequencyProfile;
     private final int mMaxEnvelopeEffectSize;
     private final int mMinEnvelopeEffectControlPointDurationMillis;
     private final int mMaxEnvelopeEffectControlPointDurationMillis;
@@ -76,6 +82,7 @@
         mPwleSizeMax = in.readInt();
         mQFactor = in.readFloat();
         mFrequencyProfileLegacy = FrequencyProfileLegacy.CREATOR.createFromParcel(in);
+        mFrequencyProfile = FrequencyProfile.CREATOR.createFromParcel(in);
         mMaxEnvelopeEffectSize = in.readInt();
         mMinEnvelopeEffectControlPointDurationMillis = in.readInt();
         mMaxEnvelopeEffectControlPointDurationMillis = in.readInt();
@@ -87,7 +94,7 @@
                 baseVibratorInfo.mPrimitiveDelayMax, baseVibratorInfo.mCompositionSizeMax,
                 baseVibratorInfo.mPwlePrimitiveDurationMax, baseVibratorInfo.mPwleSizeMax,
                 baseVibratorInfo.mQFactor, baseVibratorInfo.mFrequencyProfileLegacy,
-                baseVibratorInfo.mMaxEnvelopeEffectSize,
+                baseVibratorInfo.mFrequencyProfile, baseVibratorInfo.mMaxEnvelopeEffectSize,
                 baseVibratorInfo.mMinEnvelopeEffectControlPointDurationMillis,
                 baseVibratorInfo.mMaxEnvelopeEffectControlPointDurationMillis);
     }
@@ -114,6 +121,17 @@
      * @param qFactor                  The vibrator quality factor.
      * @param frequencyProfileLegacy   The description of the vibrator supported frequencies and max
      *                                 amplitude mappings.
+     * @param frequencyProfile       The description of the vibrator supported frequencies and
+     *                                 output acceleration mappings.
+     * @param maxEnvelopeEffectSize    The maximum number of control points supported for an
+     *                                 envelope effect.
+     * @param minEnvelopeEffectControlPointDurationMillis   The minimum duration supported
+     *                                                      between two control points within an
+     *                                                      envelope effect.
+     * @param maxEnvelopeEffectControlPointDurationMillis   The maximum duration supported
+     *                                                      between two control points within an
+     *                                                      envelope effect.
+     *
      * @hide
      */
     public VibratorInfo(int id, long capabilities, @Nullable SparseBooleanArray supportedEffects,
@@ -121,10 +139,12 @@
             @NonNull SparseIntArray supportedPrimitives, int primitiveDelayMax,
             int compositionSizeMax, int pwlePrimitiveDurationMax, int pwleSizeMax,
             float qFactor, @NonNull FrequencyProfileLegacy frequencyProfileLegacy,
-            int maxEnvelopeEffectSize, int minEnvelopeEffectControlPointDurationMillis,
+            @NonNull FrequencyProfile frequencyProfile, int maxEnvelopeEffectSize,
+            int minEnvelopeEffectControlPointDurationMillis,
             int maxEnvelopeEffectControlPointDurationMillis) {
         Preconditions.checkNotNull(supportedPrimitives);
         Preconditions.checkNotNull(frequencyProfileLegacy);
+        Preconditions.checkNotNull(frequencyProfile);
         mId = id;
         mCapabilities = capabilities;
         mSupportedEffects = supportedEffects == null ? null : supportedEffects.clone();
@@ -136,6 +156,7 @@
         mPwleSizeMax = pwleSizeMax;
         mQFactor = qFactor;
         mFrequencyProfileLegacy = frequencyProfileLegacy;
+        mFrequencyProfile = frequencyProfile;
         mMaxEnvelopeEffectSize = maxEnvelopeEffectSize;
         mMinEnvelopeEffectControlPointDurationMillis =
                 minEnvelopeEffectControlPointDurationMillis;
@@ -156,6 +177,7 @@
         dest.writeInt(mPwleSizeMax);
         dest.writeFloat(mQFactor);
         mFrequencyProfileLegacy.writeToParcel(dest, flags);
+        mFrequencyProfile.writeToParcel(dest, flags);
         dest.writeInt(mMaxEnvelopeEffectSize);
         dest.writeInt(mMinEnvelopeEffectControlPointDurationMillis);
         dest.writeInt(mMaxEnvelopeEffectControlPointDurationMillis);
@@ -206,6 +228,7 @@
                 && Objects.equals(mSupportedBraking, that.mSupportedBraking)
                 && Objects.equals(mQFactor, that.mQFactor)
                 && Objects.equals(mFrequencyProfileLegacy, that.mFrequencyProfileLegacy)
+                && Objects.equals(mFrequencyProfile, that.mFrequencyProfile)
                 && mMaxEnvelopeEffectSize == that.mMaxEnvelopeEffectSize
                 && mMinEnvelopeEffectControlPointDurationMillis
                 == that.mMinEnvelopeEffectControlPointDurationMillis
@@ -216,7 +239,7 @@
     @Override
     public int hashCode() {
         int hashCode = Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedBraking,
-                mQFactor, mFrequencyProfileLegacy);
+                mQFactor, mFrequencyProfileLegacy, mFrequencyProfile);
         for (int i = 0; i < mSupportedPrimitives.size(); i++) {
             hashCode = 31 * hashCode + mSupportedPrimitives.keyAt(i);
             hashCode = 31 * hashCode + mSupportedPrimitives.valueAt(i);
@@ -239,6 +262,7 @@
                 + ", mPwleSizeMax=" + mPwleSizeMax
                 + ", mQFactor=" + mQFactor
                 + ", mFrequencyProfileLegacy=" + mFrequencyProfileLegacy
+                + ", mFrequencyProfile=" + mFrequencyProfile
                 + ", mMaxEnvelopeEffectSize=" + mMaxEnvelopeEffectSize
                 + ", mMinEnvelopeEffectControlPointDurationMillis="
                 + mMinEnvelopeEffectControlPointDurationMillis
@@ -263,6 +287,7 @@
         pw.println("pwleSizeMax = " + mPwleSizeMax);
         pw.println("q-factor = " + mQFactor);
         pw.println("frequencyProfileLegacy = " + mFrequencyProfileLegacy);
+        pw.println("frequencyProfile = " + mFrequencyProfile);
         pw.println("mMaxEnvelopeEffectSize = " + mMaxEnvelopeEffectSize);
         pw.println("mMinEnvelopeEffectControlPointDurationMillis = "
                 + mMinEnvelopeEffectControlPointDurationMillis);
@@ -517,6 +542,9 @@
      * this vibrator is a composite of multiple physical devices.
      */
     public float getResonantFrequencyHz() {
+        if (Flags.normalizedPwleEffects()) {
+            return mFrequencyProfile.mResonantFrequencyHz;
+        }
         return mFrequencyProfileLegacy.mResonantFrequencyHz;
     }
 
@@ -541,6 +569,17 @@
         return mFrequencyProfileLegacy;
     }
 
+    /**
+     * Gets the profile of supported frequencies, including the measurements of maximum
+     * output acceleration for supported vibration frequencies.
+     *
+     * <p>If the devices does not have frequency control then the profile should be empty.
+     */
+    @NonNull
+    public FrequencyProfile getFrequencyProfile() {
+        return mFrequencyProfile;
+    }
+
     /** Returns a single int representing all the capabilities of the vibrator. */
     public long getCapabilities() {
         return mCapabilities;
@@ -623,6 +662,304 @@
     }
 
     /**
+     * Describes the maximum output acceleration that can be achieved for each supported
+     * frequency in a specific vibrator.
+     *
+     * @hide
+     */
+    public static final class FrequencyProfile implements Parcelable {
+
+        private final float[] mFrequenciesHz;
+        private final float[] mOutputAccelerationsGs;
+        private final float mResonantFrequencyHz;
+        private final float mMaxOutputAccelerationGs;
+        private final float mMinFrequencyHz;
+        private final float mMaxFrequencyHz;
+
+        public FrequencyProfile(Parcel in) {
+            this(in.readFloat(), in.createFloatArray(), in.createFloatArray());
+        }
+
+        /**
+         * Default constructor.
+         *
+         * @param resonantFrequencyHz   The vibrator resonant frequency, in hertz.
+         * @param frequenciesHz         The supported vibration frequencies, in hertz.
+         * @param outputAccelerationsGs The maximum achievable output acceleration (in Gs) the
+         *                              device can reach at the supported frequencies.
+         */
+        public FrequencyProfile(float resonantFrequencyHz, float[] frequenciesHz,
+                float[] outputAccelerationsGs) {
+
+            mResonantFrequencyHz = resonantFrequencyHz;
+
+            boolean isValid = !Float.isNaN(resonantFrequencyHz)
+                    && (resonantFrequencyHz > 0)
+                    && (frequenciesHz != null && outputAccelerationsGs != null)
+                    && (frequenciesHz.length == outputAccelerationsGs.length)
+                    && (frequenciesHz.length > 0);
+
+            if (!isValid) {
+                mFrequenciesHz = null;
+                mOutputAccelerationsGs = null;
+                mMinFrequencyHz = Float.NaN;
+                mMaxFrequencyHz = Float.NaN;
+                mMaxOutputAccelerationGs = Float.NaN;
+                return;
+            }
+
+            TreeMap<Float, Float> frequencyToOutputAccelerationMap = new TreeMap<>();
+
+            for (int i = 0; i < frequenciesHz.length; i++) {
+                frequencyToOutputAccelerationMap.putIfAbsent(frequenciesHz[i],
+                        outputAccelerationsGs[i]);
+            }
+
+            float[] frequencies = new float[frequencyToOutputAccelerationMap.size()];
+            float[] accelerations = new float[frequencyToOutputAccelerationMap.size()];
+            float maxOutputAccelerationGs = 0;
+            int i = 0;
+            for (Map.Entry<Float, Float> entry : frequencyToOutputAccelerationMap.entrySet()) {
+                frequencies[i] = entry.getKey();
+                accelerations[i] = entry.getValue();
+                maxOutputAccelerationGs = Math.max(maxOutputAccelerationGs, entry.getValue());
+                i++;
+            }
+
+            mFrequenciesHz = frequencies;
+            mOutputAccelerationsGs = accelerations;
+            mMinFrequencyHz = mFrequenciesHz[0];
+            mMaxFrequencyHz = mFrequenciesHz[mFrequenciesHz.length - 1];
+            mMaxOutputAccelerationGs = maxOutputAccelerationGs;
+        }
+
+        /** Returns true if the supported frequency range is null. */
+        public boolean isEmpty() {
+            return mFrequenciesHz == null;
+        }
+
+        /**
+         * Returns a list of available frequencies.
+         */
+        @Nullable
+        public float[] getFrequenciesHz() {
+            return mFrequenciesHz;
+        }
+
+        /** Returns the list of available output accelerations */
+        @Nullable
+        public float[] getOutputAccelerationsGs() {
+            return mOutputAccelerationsGs;
+        }
+
+        /** Maximum output acceleration reachable in Gs when amplitude is 1.0f. */
+        public float getMaxOutputAccelerationGs() {
+            return mMaxOutputAccelerationGs;
+        }
+
+        /**
+         * Calculates the maximum output acceleration for a given frequency using linear
+         * interpolation.
+         *
+         * @param frequencyHz frequency, in hertz, for query.
+         * @return the maximum output acceleration for the given frequency.
+         */
+        public float getOutputAccelerationGs(float frequencyHz) {
+            if (mFrequenciesHz == null) {
+                return Float.NaN;
+            }
+
+            if (frequencyHz < mMinFrequencyHz || frequencyHz > mMaxFrequencyHz) {
+                // Outside supported frequency range, not able to vibrate at this frequency.
+                return 0;
+            }
+
+            int idx = Arrays.binarySearch(mFrequenciesHz, frequencyHz);
+            if (idx >= 0) {
+                return mOutputAccelerationsGs[idx];
+            }
+
+            // This indicates that the value was not found in the list. Adjust index of the
+            // insertion point to be at the lower bound.
+            idx = -idx - 2;
+
+            // Linearly interpolate the output acceleration based on the frequency.
+            return MathUtils.constrainedMap(
+                    mOutputAccelerationsGs[idx], mOutputAccelerationsGs[idx + 1],
+                    mFrequenciesHz[idx], mFrequenciesHz[idx + 1],
+                    frequencyHz);
+        }
+
+        /** The minimum frequency supported, in hertz. */
+        public float getMinFrequencyHz() {
+            return mMinFrequencyHz;
+        }
+
+        /** The maximum frequency supported, in hertz. */
+        public float getMaxFrequencyHz() {
+            return mMaxFrequencyHz;
+        }
+
+        /**
+         * Returns the frequency range that supports the specified minimum output
+         * acceleration.
+         *
+         * @return The frequency range, or null if the specified acceleration
+         *         is not achievable on the device.
+         */
+        @Nullable
+        public Range<Float> getFrequencyRangeHz(float minOutputAcceleration) {
+            if (mFrequenciesHz == null || mOutputAccelerationsGs == null
+                    || minOutputAcceleration > mMaxOutputAccelerationGs) {
+                return null; // No frequency range available
+            }
+
+            if (minOutputAcceleration <= 0) {
+                return new Range<>(mMinFrequencyHz, mMaxFrequencyHz);
+            }
+
+            float minFrequency = Float.NaN;
+            float maxFrequency = Float.NaN;
+            int lowerFrequencyBoundIndex = 0;
+            // Find the lower frequency bound
+            for (int i = 0; i < mOutputAccelerationsGs.length; i++) {
+                if (mOutputAccelerationsGs[i] >= minOutputAcceleration) {
+                    if (i == 0) {
+                        minFrequency = mMinFrequencyHz;
+                    } else {
+                        minFrequency = MathUtils.constrainedMap(
+                                mFrequenciesHz[i - 1], mFrequenciesHz[i],
+                                mOutputAccelerationsGs[i - 1], mOutputAccelerationsGs[i],
+                                minOutputAcceleration);
+                    }
+                    lowerFrequencyBoundIndex = i;
+                    break; // Found the lower bound
+                }
+            }
+
+            if (Float.isNaN(minFrequency)) {
+                // Lower bound was not found
+                return null;
+            }
+
+            // Find the upper frequency bound
+            for (int i = lowerFrequencyBoundIndex; i < mOutputAccelerationsGs.length; i++) {
+                if (mOutputAccelerationsGs[i] <= minOutputAcceleration) {
+                    maxFrequency = MathUtils.constrainedMap(
+                            mFrequenciesHz[i - 1], mFrequenciesHz[i],
+                            mOutputAccelerationsGs[i - 1], mOutputAccelerationsGs[i],
+                            minOutputAcceleration);
+                    break; // Found the upper bound
+                }
+            }
+
+            if (Float.isNaN(maxFrequency)) {
+                // If the upper bound was not found, the specified output acceleration is
+                // achievable at all remaining frequencies.
+                maxFrequency = mMaxFrequencyHz;
+            }
+
+            return new Range<>(minFrequency, maxFrequency);
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeFloat(mResonantFrequencyHz);
+            dest.writeFloatArray(mFrequenciesHz);
+            dest.writeFloatArray(mOutputAccelerationsGs);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof FrequencyProfile that)) {
+                return false;
+            }
+            return Float.compare(mResonantFrequencyHz, that.mResonantFrequencyHz) == 0
+                    && Arrays.equals(mFrequenciesHz, that.mFrequenciesHz)
+                    && Arrays.equals(mOutputAccelerationsGs, that.mOutputAccelerationsGs);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mResonantFrequencyHz, Arrays.hashCode(mFrequenciesHz),
+                    Arrays.hashCode(mOutputAccelerationsGs));
+        }
+
+        @Override
+        public String toString() {
+            return "FrequencyProfile{"
+                    + "mResonantFrequency=" + mResonantFrequencyHz
+                    + ", mFrequenciesHz=" + Arrays.toString(mFrequenciesHz)
+                    + ", mOutputAccelerationsGs=" + Arrays.toString(mOutputAccelerationsGs)
+                    + ", mMinFrequencyHz=" + mMinFrequencyHz
+                    + ", mMaxFrequencyHz=" + mMaxFrequencyHz
+                    + ", mMaxOutputAccelerationGs=" + mMaxOutputAccelerationGs
+                    + '}';
+        }
+
+        @NonNull
+        public static final Creator<FrequencyProfile> CREATOR =
+                new Creator<FrequencyProfile>() {
+                    @Override
+                    public FrequencyProfile createFromParcel(Parcel in) {
+                        return new FrequencyProfile(in);
+                    }
+
+                    @Override
+                    public FrequencyProfile[] newArray(int size) {
+                        return new FrequencyProfile[size];
+                    }
+                };
+
+        private static void deduplicateAndSortList(List<Pair<Float, Float>> list) {
+            if (list == null || list.size() < 2) {
+                return; // Nothing to dedupe
+            }
+
+            list.sort(Comparator.comparing(pair -> pair.first));
+
+            // Remove duplicates from the list
+            int writeIndex = 1;
+            for (int i = 1; i < list.size(); i++) {
+                Pair<Float, Float> currentPair = list.get(i);
+                Pair<Float, Float> previousPair = list.get(writeIndex - 1);
+
+                if (currentPair.first.compareTo(previousPair.first) != 0) {
+                    list.set(writeIndex++, currentPair);
+                }
+            }
+            list.subList(writeIndex, list.size()).clear();
+        }
+
+        private static ArrayList<Pair<Float, Float>> extractFrequencyToOutputAccelerationData(
+                float[] frequencies, float[] outputAccelerations) {
+
+            if (frequencies == null || outputAccelerations == null
+                    || frequencies.length == 0
+                    || frequencies.length != outputAccelerations.length) {
+                return new ArrayList<>(); // Return empty list for invalid or mismatched data
+            }
+
+            ArrayList<Pair<Float, Float>> frequencyToOutputAccelerationList = new ArrayList<>(
+                    frequencies.length);
+            for (int i = 0; i < frequencies.length; i++) {
+                frequencyToOutputAccelerationList.add(
+                        new Pair<>(frequencies[i], outputAccelerations[i]));
+            }
+
+            return frequencyToOutputAccelerationList;
+        }
+    }
+
+    /**
      * Describes the maximum relative output acceleration that can be achieved for each supported
      * frequency in a specific vibrator.
      *
@@ -834,6 +1171,8 @@
         private float mQFactor = Float.NaN;
         private FrequencyProfileLegacy mFrequencyProfileLegacy =
                 new FrequencyProfileLegacy(Float.NaN, Float.NaN, Float.NaN, null);
+        private FrequencyProfile mFrequencyProfile = new FrequencyProfile(Float.NaN, null,
+                null);
         private int mMaxEnvelopeEffectSize;
         private int mMinEnvelopeEffectControlPointDurationMillis;
         private int mMaxEnvelopeEffectControlPointDurationMillis;
@@ -914,6 +1253,16 @@
         }
 
         /**
+         * Configure the vibrator frequency information like resonant frequency and frequency to
+         * output acceleration data.
+         */
+        @NonNull
+        public Builder setFrequencyProfile(@NonNull FrequencyProfile frequencyProfile) {
+            mFrequencyProfile = frequencyProfile;
+            return this;
+        }
+
+        /**
          * Configure the maximum number of control points supported for envelope effects on this
          * device.
          */
@@ -951,7 +1300,8 @@
             return new VibratorInfo(mId, mCapabilities, mSupportedEffects, mSupportedBraking,
                     mSupportedPrimitives, mPrimitiveDelayMax, mCompositionSizeMax,
                     mPwlePrimitiveDurationMax, mPwleSizeMax, mQFactor, mFrequencyProfileLegacy,
-                    mMaxEnvelopeEffectSize, mMinEnvelopeEffectControlPointDurationMillis,
+                    mFrequencyProfile, mMaxEnvelopeEffectSize,
+                    mMinEnvelopeEffectControlPointDurationMillis,
                     mMaxEnvelopeEffectControlPointDurationMillis);
         }
 
diff --git a/core/java/android/os/vibrator/MultiVibratorInfo.java b/core/java/android/os/vibrator/MultiVibratorInfo.java
index 37dae56..1ba8d99 100644
--- a/core/java/android/os/vibrator/MultiVibratorInfo.java
+++ b/core/java/android/os/vibrator/MultiVibratorInfo.java
@@ -27,6 +27,9 @@
 import android.util.SparseIntArray;
 
 import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.function.Function;
 
 /**
@@ -44,13 +47,18 @@
     private static final float EPSILON = 1e-5f;
 
     public MultiVibratorInfo(int id, VibratorInfo[] vibrators) {
-        this(id, vibrators, frequencyProfileIntersection(vibrators));
+        this(id, vibrators, frequencyProfileLegacyIntersection(vibrators),
+                frequencyProfileIntersection(vibrators));
     }
 
     private MultiVibratorInfo(
-            int id, VibratorInfo[] vibrators, FrequencyProfileLegacy mergedProfile) {
+            int id, VibratorInfo[] vibrators,
+            VibratorInfo.FrequencyProfileLegacy mergedLegacyProfile,
+            FrequencyProfile mergedProfile) {
         super(id,
-                capabilitiesIntersection(vibrators, mergedProfile.isEmpty()),
+                capabilitiesIntersection(vibrators,
+                        Flags.normalizedPwleEffects() ? mergedProfile.isEmpty()
+                                : mergedLegacyProfile.isEmpty()),
                 supportedEffectsIntersection(vibrators),
                 supportedBrakingIntersection(vibrators),
                 supportedPrimitivesAndDurationsIntersection(vibrators),
@@ -59,6 +67,7 @@
                 integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
                 integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
                 floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
+                mergedLegacyProfile,
                 mergedProfile,
                 integerLimitIntersection(vibrators,
                         VibratorInfo::getMaxEnvelopeEffectSize),
@@ -209,7 +218,82 @@
     }
 
     @NonNull
-    private static FrequencyProfileLegacy frequencyProfileIntersection(VibratorInfo[] infos) {
+    private static FrequencyProfile frequencyProfileIntersection(VibratorInfo[] infos) {
+        if (infos == null || infos.length == 0) {
+            return new FrequencyProfile(Float.NaN,
+                    /*frequenciesHz=*/ null, /*outputAccelerationsGs=*/ null);
+        }
+
+        float resonantFreq = floatPropertyIntersection(infos, VibratorInfo::getResonantFrequencyHz);
+
+        if (Float.isNaN(resonantFreq)) {
+            return new FrequencyProfile(Float.NaN,
+                    /*frequenciesHz=*/ null, /*outputAccelerationsGs=*/ null);
+        }
+
+        float minFrequency = 0.0f;
+        float maxFrequency = Float.MAX_VALUE;
+        Set<Float> allFrequencies = new TreeSet<>(); // Using TreeSet for automatic sorting
+
+        for (VibratorInfo info : infos) {
+            float newMinFrequency = info.getFrequencyProfile().getMinFrequencyHz();
+            float newMaxFrequency = info.getFrequencyProfile().getMaxFrequencyHz();
+
+            if (Float.isNaN(newMinFrequency) || Float.isNaN(newMaxFrequency)) {
+                // If one vibrator is undefined then the intersection is undefined.
+                return new FrequencyProfile(Float.NaN,
+                        /*frequenciesHz=*/ null, /*outputAccelerationsGs=*/ null);
+            }
+
+            minFrequency = Math.max(minFrequency, newMinFrequency);
+            maxFrequency = Math.min(maxFrequency, newMaxFrequency);
+
+            if (info.getFrequencyProfile().getFrequenciesHz() == null) {
+                return new FrequencyProfile(Float.NaN,
+                        /*frequenciesHz=*/ null, /*outputAccelerationsGs=*/ null);
+            }
+
+            for (float frequency : info.getFrequencyProfile().getFrequenciesHz()) {
+                allFrequencies.add(frequency);
+            }
+        }
+
+        if (minFrequency > maxFrequency) {
+            // If the range and intersection are disjoint then the intersection is undefined
+            return new FrequencyProfile(Float.NaN,
+                    /*frequenciesHz=*/ null, /*outputAccelerationsGs=*/ null);
+        }
+
+        // Trim frequencies to the min/max range
+        Iterator<Float> iterator = allFrequencies.iterator();
+        while (iterator.hasNext()) {
+            float frequency = iterator.next();
+            if (frequency < minFrequency || frequency > maxFrequency) {
+                iterator.remove();
+            }
+        }
+
+        float[] frequencies = new float[allFrequencies.size()];
+        float[] accelerations = new float[allFrequencies.size()];
+        int idx = 0;
+
+        for (Float frequency : allFrequencies) {
+            float outputAcceleration = Float.MAX_VALUE;
+            for (VibratorInfo info : infos) {
+                // This will find the mapped value or interpolate it if needed.
+                outputAcceleration = Math.min(outputAcceleration,
+                        info.getFrequencyProfile().getOutputAccelerationGs(frequency));
+            }
+            frequencies[idx] = frequency;
+            accelerations[idx] = outputAcceleration;
+            idx++;
+        }
+
+        return new FrequencyProfile(resonantFreq, frequencies, accelerations);
+    }
+
+    @NonNull
+    private static FrequencyProfileLegacy frequencyProfileLegacyIntersection(VibratorInfo[] infos) {
         float freqResolution = floatPropertyIntersection(infos,
                 info -> info.getFrequencyProfileLegacy().getFrequencyResolutionHz());
         float resonantFreq = floatPropertyIntersection(infos,
diff --git a/core/java/android/os/vibrator/VibratorFrequencyProfile.java b/core/java/android/os/vibrator/VibratorFrequencyProfile.java
new file mode 100644
index 0000000..2b5f9bf
--- /dev/null
+++ b/core/java/android/os/vibrator/VibratorFrequencyProfile.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2024 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.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.VibratorInfo;
+import android.util.Range;
+import android.util.SparseArray;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Describes the output of a {@link android.os.Vibrator} for different vibration frequencies.
+ *
+ * <p>The profile contains the vibrator's frequency range (minimum/maximum) and maximum
+ * acceleration, enabling retrieval of supported acceleration levels for specific frequencies, if
+ * the device supports independent frequency control.
+ *
+ * <p>It also describes the max output acceleration (Gs), of a vibration at different supported
+ * frequencies (Hz).
+ *
+ * <p>Vibrators without independent frequency control do not have a frequency profile.
+ */
+@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+public final class VibratorFrequencyProfile {
+
+    private final VibratorInfo.FrequencyProfile mFrequencyProfile;
+    private final SparseArray<Float> mFrequenciesOutputAcceleration;
+
+    /** @hide */
+    public VibratorFrequencyProfile(@NonNull VibratorInfo.FrequencyProfile frequencyProfile) {
+        Objects.requireNonNull(frequencyProfile);
+        Preconditions.checkArgument(!frequencyProfile.isEmpty(),
+                "Frequency profile must not be empty");
+        mFrequencyProfile = frequencyProfile;
+        mFrequenciesOutputAcceleration = generateFrequencyToAccelerationMap(
+                frequencyProfile.getFrequenciesHz(), frequencyProfile.getOutputAccelerationsGs());
+    }
+
+    /**
+     * Returns a {@link SparseArray} representing the vibrator's output acceleration capabilities
+     * across different frequencies. This map defines the maximum acceleration
+     * the vibrator can achieve at each supported frequency.
+     * <p>The map's keys are frequencies in Hz, and the corresponding values
+     * are the maximum achievable output accelerations in Gs.
+     *
+     * @return A map of frequencies (Hz) to maximum accelerations (Gs).
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    @NonNull
+    public SparseArray<Float> getFrequenciesOutputAcceleration() {
+        return mFrequenciesOutputAcceleration;
+    }
+
+    /**
+     * Returns the maximum output acceleration (in Gs) supported by the vibrator.
+     * This value represents the highest acceleration the vibrator can achieve
+     * across its entire frequency range.
+     *
+     * @return The maximum output acceleration in Gs.
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public float getMaxOutputAccelerationGs() {
+        return mFrequencyProfile.getMaxOutputAccelerationGs();
+    }
+
+    /**
+     * Returns the frequency range (in Hz) where the vibrator can sustain at least
+     * the given minimum output acceleration (Gs).
+     *
+     * @param minOutputAccelerationGs The minimum desired output acceleration in Gs.
+     * @return A {@link Range} object representing the frequency range where the
+     *         vibrator can sustain at least the given minimum acceleration, or null if
+     *         the minimum output acceleration cannot be achieved.
+     *
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    @Nullable
+    public Range<Float> getFrequencyRange(float minOutputAccelerationGs) {
+        return mFrequencyProfile.getFrequencyRangeHz(minOutputAccelerationGs);
+    }
+
+    /**
+     * Returns the output acceleration (in Gs) for the given frequency (Hz).
+     * This method provides the actual acceleration the vibrator will produce
+     * when operating at the specified frequency, using linear interpolation over
+     * the {@link #getFrequenciesOutputAcceleration()}.
+     *
+     * @param frequencyHz The frequency in Hz.
+     * @return The output acceleration in Gs for the given frequency.
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public float getOutputAccelerationGs(float frequencyHz) {
+        return mFrequencyProfile.getOutputAccelerationGs(frequencyHz);
+    }
+
+    /**
+     * Gets the minimum frequency supported by the vibrator.
+     *
+     * @return the minimum frequency supported by the vibrator, in hertz.
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public float getMinFrequencyHz() {
+        return mFrequencyProfile.getMinFrequencyHz();
+    }
+
+    /**
+     * Gets the maximum frequency supported by the vibrator.
+     *
+     * @return the maximum frequency supported by the vibrator, in hertz.
+     */
+    @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public float getMaxFrequencyHz() {
+        return mFrequencyProfile.getMaxFrequencyHz();
+    }
+
+    private static SparseArray<Float> generateFrequencyToAccelerationMap(
+            float[] frequencies, float[] accelerations) {
+        SparseArray<Float> sparseArray = new SparseArray<>(frequencies.length);
+
+        for (int i = 0; i < frequencies.length; i++) {
+            int frequency = (int) frequencies[i];
+            float acceleration = accelerations[i];
+
+            sparseArray.put(frequency,
+                    Math.min(acceleration, sparseArray.get(frequency, Float.MAX_VALUE)));
+
+        }
+
+        return sparseArray;
+    }
+}
diff --git a/core/tests/vibrator/src/android/os/VibratorInfoTest.java b/core/tests/vibrator/src/android/os/VibratorInfoTest.java
index 47d01c4..04945f3 100644
--- a/core/tests/vibrator/src/android/os/VibratorInfoTest.java
+++ b/core/tests/vibrator/src/android/os/VibratorInfoTest.java
@@ -16,6 +16,8 @@
 
 package android.os;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -24,6 +26,7 @@
 
 import android.hardware.vibrator.Braking;
 import android.hardware.vibrator.IVibrator;
+import android.util.Range;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,12 +42,19 @@
     private static final float TEST_FREQUENCY_RESOLUTION = 25;
     private static final float[] TEST_AMPLITUDE_MAP = new float[]{
             /* 50Hz= */ 0.1f, 0.2f, 0.4f, 0.8f, /* 150Hz= */ 1f, 0.9f, /* 200Hz= */ 0.8f};
+    private static final float[] TEST_FREQUENCIES =
+            new float[]{90f, 120f, 150f, 60f, 30f, 210f, 270f, 300f, 240f, 180f};
+    private static final float[] TEST_OUTPUT_ACCELERATIONS =
+            new float[]{1.2f, 1.8f, 2.4f, 0.6f, 0.1f, 2.2f, 1.0f, 0.5f, 1.9f, 3.0f};
 
     private static final VibratorInfo.FrequencyProfileLegacy EMPTY_FREQUENCY_PROFILE =
             new VibratorInfo.FrequencyProfileLegacy(Float.NaN, Float.NaN, Float.NaN, null);
     private static final VibratorInfo.FrequencyProfileLegacy TEST_FREQUENCY_PROFILE_LEGACY =
             new VibratorInfo.FrequencyProfileLegacy(TEST_RESONANT_FREQUENCY, TEST_MIN_FREQUENCY,
                     TEST_FREQUENCY_RESOLUTION, TEST_AMPLITUDE_MAP);
+    private static final VibratorInfo.FrequencyProfile TEST_FREQUENCY_PROFILE =
+            new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, TEST_FREQUENCIES,
+                    TEST_OUTPUT_ACCELERATIONS);
 
     @Test
     public void testHasAmplitudeControl() {
@@ -179,13 +189,123 @@
     }
 
     @Test
+    public void testGetFrequencyProfile_unsetProfileIsEmpty() {
+        assertTrue(new VibratorInfo.Builder(
+                TEST_VIBRATOR_ID).build().getFrequencyProfile().isEmpty());
+    }
+
+    @Test
+    public void testFrequencyProfile_invalidValuesCreatesEmptyProfile() {
+        // Invalid resonant frequency.
+        assertThat(new VibratorInfo.FrequencyProfile(Float.NaN,
+                TEST_FREQUENCIES, TEST_OUTPUT_ACCELERATIONS).isEmpty()).isTrue();
+        assertThat(new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/-1f,
+                TEST_FREQUENCIES, TEST_OUTPUT_ACCELERATIONS).isEmpty()).isTrue();
+        // No frequency-acceleration data
+        assertThat(new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/150f,
+                /*frequenciesHz=*/ null, /*outputAccelerationsGs=*/ null).isEmpty()).isTrue();
+        // Mismatching frequency and output acceleration lists
+        assertThat(new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/150f,
+                /*frequenciesHz=*/ new float[]{30f, 40f, 50f, 100f},
+                /*outputAccelerationsGs=*/ new float[]{0.8f, 1.0f, 2.0f}).isEmpty()).isTrue();
+    }
+
+    @Test
+    public void testGetFrequenciesAndOutputAccelerations_noFrequencyAccelerationData_returnNull() {
+        VibratorInfo.FrequencyProfile emptyFrequencyProfile =
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/150f,
+                        /*frequenciesHz=*/ null, /*outputAccelerationsGs=*/ null);
+        assertThat(emptyFrequencyProfile.getFrequenciesHz()).isNull();
+        assertThat(emptyFrequencyProfile.getOutputAccelerationsGs()).isNull();
+    }
+
+    @Test
+    public void testGetFrequenciesAndOutputAccelerations_mismatchingDataLength_returnNull() {
+        VibratorInfo.FrequencyProfile emptyFrequencyProfile =
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/150f,
+                        /*frequenciesHz=*/ new float[]{150f, 200f},
+                        /*outputAccelerationsGs=*/ new float[]{1.2f, 2.2f, 3.0f});
+        assertThat(emptyFrequencyProfile.getFrequenciesHz()).isNull();
+        assertThat(emptyFrequencyProfile.getOutputAccelerationsGs()).isNull();
+    }
+
+    @Test
+    public void testGetFrequenciesAndOutputAccelerations_dataIsDedupedAndSorted() {
+        VibratorInfo.FrequencyProfile frequencyProfile =
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/150f,
+                        /*frequenciesHz=*/ new float[]{150f, 150f, 150f, 130f, 200f, 160f},
+                        /*outputAccelerationsGs=*/ new float[]{1.2f, 1.5f, 1.9f, 1.0f, 2.2f, 3.0f});
+        float[] frequencies = frequencyProfile.getFrequenciesHz();
+        assertThat(frequencies).isEqualTo(
+                new float[]{130f, 150f, 160f, 200f});
+        assertThat(frequencyProfile.getOutputAccelerationsGs()).isEqualTo(
+                new float[]{1.0f, 1.2f, 3.0f, 2.2f});
+    }
+
+    @Test
+    public void testGetFrequencyRangeHz_emptyProfileReturnsNull() {
+        VibratorInfo.FrequencyProfile emptyFrequencyProfile =
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/150f,
+                        /*frequenciesHz=*/ null, /*outputAccelerationsGs=*/ null);
+        assertThat(
+                emptyFrequencyProfile.getFrequencyRangeHz(/*minOutputAcceleration=*/0.2f)).isNull();
+    }
+
+    @Test
+    public void testGetFrequencyRangeHz_validProfileReturnsMappedValues() {
+        VibratorInfo.FrequencyProfile frequencyProfile =
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/150f,
+                /*frequenciesHz=*/new float[]{90f, 120f, 150f, 60f, 30f, 210f, 180f},
+                /*outputAccelerationsGs=*/ new float[]{1.2f, 1.8f, 2.4f, 0.6f, 0.4f, 2.2f, 3.0f});
+
+        // lower and upper bounds are min and max frequencies
+        assertThat(frequencyProfile.getFrequencyRangeHz(/*minOutputAcceleration=*/0.33f)).isEqualTo(
+                new Range<>(frequencyProfile.getMinFrequencyHz(),
+                        frequencyProfile.getMaxFrequencyHz()));
+
+        // lower and upper bounds are within frequency range and use interpolation
+        assertThat(frequencyProfile.getFrequencyRangeHz(/*minOutputAcceleration=*/2.6f))
+                .isEqualTo(new Range<>(160f, 195f));
+
+        // upper bound is max frequency
+        assertThat(frequencyProfile.getFrequencyRangeHz(/*minOutputAcceleration=*/2.0f))
+                .isEqualTo(new Range<>(130f, frequencyProfile.getMaxFrequencyHz()));
+    }
+
+    @Test
+    public void testFrequencyProfile_emptyProfileReturnsNanValues() {
+        VibratorInfo.FrequencyProfile frequencyProfile = new VibratorInfo.FrequencyProfile(
+                /*resonantFrequencyHz=*/150f, /*frequenciesHz=*/ null,
+                /*outputAccelerationsGs=*/ null);
+
+        assertThat(frequencyProfile.getMaxOutputAccelerationGs()).isNaN();
+        assertThat(frequencyProfile.getMinFrequencyHz()).isNaN();
+        assertThat(frequencyProfile.getMaxFrequencyHz()).isNaN();
+        assertThat(frequencyProfile.getOutputAccelerationGs(/*frequencyHz=*/150f)).isNaN();
+    }
+
+    @Test
+    public void testFrequencyProfile_validProfileReturnsAppropriateValues() {
+        VibratorInfo.FrequencyProfile frequencyProfile = new VibratorInfo.FrequencyProfile(
+                /*resonantFrequencyHz=*/150f, TEST_FREQUENCIES, TEST_OUTPUT_ACCELERATIONS);
+
+        assertThat(frequencyProfile.getMaxOutputAccelerationGs()).isEqualTo(3f);
+        assertThat(frequencyProfile.getMinFrequencyHz()).isEqualTo(30f);
+        assertThat(frequencyProfile.getMaxFrequencyHz()).isEqualTo(300f);
+        assertThat(frequencyProfile.getOutputAccelerationGs(/*frequencyHz=*/150f)).isEqualTo(2.4f);
+        // Test getting output acceleration using linear interpolation
+        assertThat(frequencyProfile.getOutputAccelerationGs(/*frequencyHz=*/166f)).isEqualTo(
+                2.72f);
+    }
+
+    @Test
     public void testGetFrequencyProfileLegacy_unsetProfileIsEmpty() {
         assertTrue(new VibratorInfo.Builder(
                 TEST_VIBRATOR_ID).build().getFrequencyProfileLegacy().isEmpty());
     }
 
     @Test
-    public void testFrequencyProfile_invalidValuesCreatesEmptyProfile() {
+    public void testFrequencyProfileLegacy_invalidValuesCreatesEmptyProfile() {
         // Invalid, contains NaN values or empty array.
         assertTrue(new VibratorInfo.FrequencyProfileLegacy(
                 Float.NaN, 50, 25, TEST_AMPLITUDE_MAP).isEmpty());
@@ -216,7 +336,7 @@
     }
 
     @Test
-    public void testGetFrequencyRangeHz_emptyProfileReturnsNull() {
+    public void testLegacyGetFrequencyRangeHz_emptyProfileReturnsNull() {
         assertNull(new VibratorInfo.FrequencyProfileLegacy(
                 Float.NaN, 50, 25, TEST_AMPLITUDE_MAP).getFrequencyRangeHz());
         assertNull(new VibratorInfo.FrequencyProfileLegacy(
@@ -228,7 +348,7 @@
     }
 
     @Test
-    public void testGetFrequencyRangeHz_validProfileReturnsMappedValues() {
+    public void testLegacyGetFrequencyRangeHz_validProfileReturnsMappedValues() {
         VibratorInfo.FrequencyProfileLegacy profile = new VibratorInfo.FrequencyProfileLegacy(
                 /* resonantFrequencyHz= */ 150,
                 /* minFrequencyHz= */ 50,
@@ -306,6 +426,7 @@
                     .setPwleSizeMax(20)
                     .setQFactor(2f)
                     .setFrequencyProfileLegacy(TEST_FREQUENCY_PROFILE_LEGACY)
+                    .setFrequencyProfile(TEST_FREQUENCY_PROFILE)
                     .setMaxEnvelopeEffectSize(16)
                     .setMinEnvelopeEffectControlPointDurationMillis(20)
                     .setMaxEnvelopeEffectControlPointDurationMillis(1_000);
@@ -347,18 +468,33 @@
         assertNotEquals(complete, completeWithDifferentPrimitiveDuration);
         assertFalse(complete.equalContent(completeWithDifferentPrimitiveDuration));
 
-        VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder
+        VibratorInfo completeWithDifferentFrequencyProfileLegacy = completeBuilder
                 .setFrequencyProfileLegacy(new VibratorInfo.FrequencyProfileLegacy(
                         TEST_RESONANT_FREQUENCY + 20,
                         TEST_MIN_FREQUENCY + 10,
                         TEST_FREQUENCY_RESOLUTION + 5,
                         TEST_AMPLITUDE_MAP))
                 .build();
+        assertNotEquals(complete, completeWithDifferentFrequencyProfileLegacy);
+        assertFalse(complete.equalContent(completeWithDifferentFrequencyProfileLegacy));
+
+        VibratorInfo completeWithEmptyFrequencyProfileLegacy = completeBuilder
+                .setFrequencyProfileLegacy(EMPTY_FREQUENCY_PROFILE)
+                .build();
+        assertNotEquals(complete, completeWithEmptyFrequencyProfileLegacy);
+        assertFalse(complete.equalContent(completeWithEmptyFrequencyProfileLegacy));
+
+        VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder
+                .setFrequencyProfile(
+                        new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY + 20,
+                                new float[]{90f, 150f}, new float[]{1.2f, 2.2f}))
+                .build();
         assertNotEquals(complete, completeWithDifferentFrequencyProfile);
         assertFalse(complete.equalContent(completeWithDifferentFrequencyProfile));
 
         VibratorInfo completeWithEmptyFrequencyProfile = completeBuilder
-                .setFrequencyProfileLegacy(EMPTY_FREQUENCY_PROFILE)
+                .setFrequencyProfile(
+                        new VibratorInfo.FrequencyProfile(Float.NaN, null, null))
                 .build();
         assertNotEquals(complete, completeWithEmptyFrequencyProfile);
         assertFalse(complete.equalContent(completeWithEmptyFrequencyProfile));
@@ -396,6 +532,7 @@
                 .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
                 .setQFactor(Float.NaN)
                 .setFrequencyProfileLegacy(TEST_FREQUENCY_PROFILE_LEGACY)
+                .setFrequencyProfile(TEST_FREQUENCY_PROFILE)
                 .build();
 
         Parcel parcel = Parcel.obtain();
diff --git a/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java b/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java
index f192b89..c9ab297 100644
--- a/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/MultiVibratorInfoTest.java
@@ -16,6 +16,8 @@
 
 package android.os.vibrator;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.TestCase.assertEquals;
@@ -24,7 +26,11 @@
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.VibratorInfo;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -33,6 +39,9 @@
 public class MultiVibratorInfoTest {
     private static final float TEST_TOLERANCE = 1e-5f;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Test
     public void testGetId() {
         VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
@@ -157,6 +166,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testGetQFactorAndResonantFrequency_differentValues_returnsNaN() {
         VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
                 .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
@@ -187,6 +197,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testGetQFactorAndResonantFrequency_sameValues_returnsValue() {
         VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
                 .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
@@ -212,6 +223,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testGetFrequencyProfileLegacy_differentResonantFreqOrResolutions_returnsEmpty() {
         VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
                 .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
@@ -240,6 +252,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testGetFrequencyProfileLegacy_missingValues_returnsEmpty() {
         VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
                 .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
@@ -288,6 +301,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testGetFrequencyProfileLegacy_unalignedMaxAmplitudes_returnsEmpty() {
         VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
                 .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
@@ -312,6 +326,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testGetFrequencyProfileLegacy_alignedProfiles_returnsIntersection() {
         VibratorInfo firstInfo = new VibratorInfo.Builder(/* id= */ 1)
                 .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL)
@@ -353,6 +368,132 @@
         assertEquals(false, info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL));
     }
 
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testGetFrequencyProfile_alignedProfiles_returnsIntersection() {
+        VibratorInfo firstInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 1,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/ 180f,
+                /*frequencies=*/new float[]{30f, 60f, 120f, 150f, 180f, 210f, 270f, 300f},
+                /*accelerations=*/new float[]{0.1f, 0.6f, 1.8f, 2.4f, 3.0f, 2.2f, 1.0f, 0.5f});
+
+        VibratorInfo secondInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 2,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/ 180f,
+                /*frequencies=*/new float[]{120f, 150f, 180f, 210f},
+                /*accelerations=*/new float[]{1.5f, 2.6f, 2.7f, 2.1f});
+
+        VibratorInfo.FrequencyProfile expectedFrequencyProfile =
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/
+                        180f, /*frequenciesHz=*/new float[]{120.0f, 150.0f, 180.0f, 210.0f},
+                        /*outputAccelerationsGs=*/new float[]{1.5f, 2.4f, 2.7f, 2.1f});
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertThat(info.getFrequencyProfile()).isEqualTo(expectedFrequencyProfile);
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testGetFrequencyProfile_alignedProfilesUsingInterpolation_returnsIntersection() {
+        VibratorInfo firstInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 1,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/ 180f,
+                /*frequencies=*/new float[]{30f, 60f, 120f},
+                /*accelerations=*/new float[]{0.25f, 1.0f, 4.0f});
+
+        VibratorInfo secondInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 2,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/ 180f,
+                /*frequencies=*/new float[]{40f, 70f, 110f},
+                /*accelerations=*/new float[]{1.0f, 2.5f, 4.0f});
+
+        VibratorInfo.FrequencyProfile expectedFrequencyProfile =
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/
+                        180f, /*frequenciesHz=*/new float[]{40f, 60f, 70f, 110f},
+                        /*outputAccelerationsGs=*/new float[]{0.5f, 1.0f, 1.5f, 3.5f});
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertThat(info.getFrequencyProfile()).isEqualTo(expectedFrequencyProfile);
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testGetFrequencyProfile_disjointFrequencyRange_returnsEmpty() {
+
+        VibratorInfo firstInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 1,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/ 180f,
+                /*frequencies=*/new float[]{30f, 60f, 120f, 150f, 180f, 210f, 270f, 300f},
+                /*accelerations=*/new float[]{0.1f, 0.6f, 1.8f, 2.4f, 3.0f, 2.2f, 1.0f, 0.5f});
+
+        VibratorInfo secondInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 2,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/ 180f,
+                /*frequencies=*/new float[]{310f, 320f, 350f, 380f, 410f, 440f},
+                /*accelerations=*/new float[]{0.3f, 0.75f, 1.82f, 2.11f, 2.8f, 2.12f, 1.4f, 0.42f});
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertThat(info.getFrequencyProfile()).isEqualTo(
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/ Float.NaN,
+                        /*frequenciesHz=*/null, /*outputAccelerationsGs=*/null));
+        assertThat(info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testGetFrequencyProfile_emptyFrequencyRange_returnsEmpty() {
+        VibratorInfo firstInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 1,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/180f,
+                /*frequencies=*/null, /*accelerations=*/null);
+
+        VibratorInfo secondInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 2,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/180f,
+                /*frequencies=*/new float[]{30f, 60f, 150f, 180f, 210f, 240f, 300f},
+                /*accelerations=*/new float[]{0.1f, 0.6f, 2.4f, 3.0f, 2.2f, 1.9f, 0.5f});
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertThat(info.getFrequencyProfile()).isEqualTo(
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/ Float.NaN,
+                        /*frequenciesHz=*/null,
+                        /*outputAccelerationsGs=*/null));
+        assertThat(info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testGetFrequencyProfile_differentResonantFrequency_returnsEmpty() {
+        VibratorInfo firstInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 1,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/ 160f,
+                /*frequencies=*/new float[]{30f, 60f, 120f, 150f, 180f, 210f, 270f, 300f},
+                /*accelerations=*/new float[]{0.1f, 0.6f, 1.8f, 2.4f, 3.0f, 2.2f, 1.0f, 0.5f});
+
+        VibratorInfo secondInfo = createVibratorInfoWithFrequencyProfile(/*id=*/ 2,
+                IVibrator.CAP_FREQUENCY_CONTROL, /*resonantFrequencyHz=*/ 180f,
+                /*frequencies=*/new float[]{30f, 60f, 120f, 150f, 180f, 210f, 270f, 300f},
+                /*accelerations=*/new float[]{0.1f, 0.6f, 1.8f, 2.4f, 3.0f, 2.2f, 1.0f, 0.5f});
+
+        VibratorInfo info = new MultiVibratorInfo(/* id= */ 1,
+                new VibratorInfo[]{firstInfo, secondInfo});
+
+        assertThat(info.getFrequencyProfile()).isEqualTo(
+                new VibratorInfo.FrequencyProfile(/*resonantFrequencyHz=*/ Float.NaN,
+                        /*frequenciesHz=*/null,
+                        /*outputAccelerationsGs=*/null));
+        assertThat(info.hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)).isFalse();
+    }
+
+    private VibratorInfo createVibratorInfoWithFrequencyProfile(int id, long capabilities,
+            float resonantFrequencyHz, float[] frequencies, float[] accelerations) {
+        return new VibratorInfo.Builder(id)
+                .setCapabilities(capabilities)
+                .setFrequencyProfile(
+                        new VibratorInfo.FrequencyProfile(resonantFrequencyHz, frequencies,
+                                accelerations))
+                .build();
+    }
+
     /**
      * Asserts that the frequency profile is empty, and therefore frequency control isn't supported.
      */
diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
index bcd0b94..903d892 100644
--- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp
+++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
@@ -43,6 +43,8 @@
 static jmethodID sMethodIdOnComplete;
 static jclass sFrequencyProfileLegacyClass;
 static jmethodID sFrequencyProfileLegacyCtor;
+static jclass sFrequencyProfileClass;
+static jmethodID sFrequencyProfileCtor;
 static struct {
     jmethodID setCapabilities;
     jmethodID setSupportedEffects;
@@ -54,6 +56,7 @@
     jmethodID setCompositionSizeMax;
     jmethodID setQFactor;
     jmethodID setFrequencyProfileLegacy;
+    jmethodID setFrequencyProfile;
     jmethodID setMaxEnvelopeEffectSize;
     jmethodID setMinEnvelopeEffectControlPointDurationMillis;
     jmethodID setMaxEnvelopeEffectControlPointDurationMillis;
@@ -524,6 +527,40 @@
                           sVibratorInfoBuilderClassInfo.setFrequencyProfileLegacy,
                           frequencyProfileLegacy);
 
+    if (info.frequencyToOutputAccelerationMap.isOk()) {
+        size_t mapSize = info.frequencyToOutputAccelerationMap.value().size();
+
+        jfloatArray frequenciesHz = env->NewFloatArray(mapSize);
+        jfloatArray outputAccelerationsGs = env->NewFloatArray(mapSize);
+
+        jfloat* frequenciesHzPtr = env->GetFloatArrayElements(frequenciesHz, nullptr);
+        jfloat* outputAccelerationsGsPtr =
+                env->GetFloatArrayElements(outputAccelerationsGs, nullptr);
+
+        size_t i = 0;
+        for (auto const& dataEntry : info.frequencyToOutputAccelerationMap.value()) {
+            frequenciesHzPtr[i] = static_cast<jfloat>(dataEntry.frequencyHz);
+            outputAccelerationsGsPtr[i] = static_cast<jfloat>(dataEntry.maxOutputAccelerationGs);
+            i++;
+        }
+
+        // Release the float pointers
+        env->ReleaseFloatArrayElements(frequenciesHz, frequenciesHzPtr, 0);
+        env->ReleaseFloatArrayElements(outputAccelerationsGs, outputAccelerationsGsPtr, 0);
+
+        jobject frequencyProfile =
+                env->NewObject(sFrequencyProfileClass, sFrequencyProfileCtor, resonantFrequency,
+                               frequenciesHz, outputAccelerationsGs);
+
+        env->CallObjectMethod(vibratorInfoBuilder,
+                              sVibratorInfoBuilderClassInfo.setFrequencyProfile, frequencyProfile);
+
+        // Delete local references to avoid memory leaks
+        env->DeleteLocalRef(frequenciesHz);
+        env->DeleteLocalRef(outputAccelerationsGs);
+        env->DeleteLocalRef(frequencyProfile);
+    }
+
     return info.shouldRetry() ? JNI_FALSE : JNI_TRUE;
 }
 
@@ -574,6 +611,10 @@
     sFrequencyProfileLegacyCtor =
             GetMethodIDOrDie(env, sFrequencyProfileLegacyClass, "<init>", "(FFF[F)V");
 
+    jclass frequencyProfileClass = FindClassOrDie(env, "android/os/VibratorInfo$FrequencyProfile");
+    sFrequencyProfileClass = static_cast<jclass>(env->NewGlobalRef(frequencyProfileClass));
+    sFrequencyProfileCtor = GetMethodIDOrDie(env, sFrequencyProfileClass, "<init>", "(F[F[F)V");
+
     jclass vibratorInfoBuilderClass = FindClassOrDie(env, "android/os/VibratorInfo$Builder");
     sVibratorInfoBuilderClassInfo.setCapabilities =
             GetMethodIDOrDie(env, vibratorInfoBuilderClass, "setCapabilities",
@@ -606,6 +647,10 @@
             GetMethodIDOrDie(env, vibratorInfoBuilderClass, "setFrequencyProfileLegacy",
                              "(Landroid/os/VibratorInfo$FrequencyProfileLegacy;)"
                              "Landroid/os/VibratorInfo$Builder;");
+    sVibratorInfoBuilderClassInfo.setFrequencyProfile =
+            GetMethodIDOrDie(env, vibratorInfoBuilderClass, "setFrequencyProfile",
+                             "(Landroid/os/VibratorInfo$FrequencyProfile;)"
+                             "Landroid/os/VibratorInfo$Builder;");
     sVibratorInfoBuilderClassInfo.setMaxEnvelopeEffectSize =
             GetMethodIDOrDie(env, vibratorInfoBuilderClass, "setMaxEnvelopeEffectSize",
                              "(I)Landroid/os/VibratorInfo$Builder;");
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
index 1493253..d7ae046 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
@@ -35,7 +35,9 @@
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.SparseArray;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -63,6 +65,8 @@
 
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Mock
     private PackageManagerInternal mPackageManagerInternalMock;
@@ -186,6 +190,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testStepAndRampSegments_withValidFreqMapping_returnsClippedValuesOnlyInRamps() {
         VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
                 // Individual step without frequency control, will not use PWLE composition
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/RampToStepAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/RampToStepAdapterTest.java
index 8103682..96f0fda2 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/RampToStepAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/RampToStepAdapterTest.java
@@ -26,8 +26,11 @@
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -49,6 +52,9 @@
 
     private RampToStepAdapter mAdapter;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Before
     public void setUp() throws Exception {
         mAdapter = new RampToStepAdapter(TEST_STEP_DURATION);
@@ -87,6 +93,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testRampSegments_withoutPwleCapability_convertsRampsToSteps() {
         List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
                 new StepSegment(/* amplitude= */ 0, /* frequencyHz= */ 1, /* duration= */ 10),
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/SplitSegmentsAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/SplitSegmentsAdapterTest.java
index f2c3726..53e49e0 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/SplitSegmentsAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/SplitSegmentsAdapterTest.java
@@ -26,8 +26,11 @@
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -52,6 +55,9 @@
 
     private SplitSegmentsAdapter mAdapter;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Before
     public void setUp() throws Exception {
         mAdapter = new SplitSegmentsAdapter();
@@ -97,6 +103,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testRampSegments_withPwleDurationLimit_splitsLongRampsAndPreserveOtherSegments() {
         List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
                 new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100),
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/StepToRampAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/StepToRampAdapterTest.java
index d501dba..fae634d 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/StepToRampAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/StepToRampAdapterTest.java
@@ -26,8 +26,11 @@
 import android.os.vibrator.RampSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -48,6 +51,9 @@
 
     private StepToRampAdapter mAdapter;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Before
     public void setUp() throws Exception {
         mAdapter = new StepToRampAdapter();
@@ -134,6 +140,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void testStepSegments_withPwleCapabilityAndFrequency_convertsStepsToRamps() {
         List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
                 new StepSegment(/* amplitude= */ 0, /* frequencyHz= */ 100, /* duration= */ 10),
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 7536f5f..58a1e84 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -63,9 +63,11 @@
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationConfig;
 import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.util.SparseArray;
 
@@ -113,6 +115,8 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
     @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Rule
     public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
 
     @Mock private PackageManagerInternal mPackageManagerInternalMock;
@@ -780,6 +784,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void vibrate_singleVibratorComposedEffects_runsDifferentVibrations() {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
         fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
@@ -870,6 +875,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void vibrate_singleVibratorPwle_runsComposePwle() {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
         fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
@@ -1724,6 +1730,7 @@
     }
 
     @Test
+    @DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
     public void vibrate_pwleWithRampDown_doesNotAddRampDown() {
         when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(15);
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
index f96177d..6dc1b10 100644
--- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -76,6 +76,9 @@
     private float mFrequencyResolution = Float.NaN;
     private float mQFactor = Float.NaN;
     private float[] mMaxAmplitudes;
+
+    private float[] mFrequenciesHz;
+    private float[] mOutputAccelerationsGs;
     private long mVendorEffectDuration = EFFECT_DURATION;
 
     void recordEffectSegment(long vibrationId, VibrationEffectSegment segment) {
@@ -220,6 +223,9 @@
             infoBuilder.setQFactor(mQFactor);
             infoBuilder.setFrequencyProfileLegacy(new VibratorInfo.FrequencyProfileLegacy(
                     mResonantFrequency, mMinFrequency, mFrequencyResolution, mMaxAmplitudes));
+            infoBuilder.setFrequencyProfile(
+                    new VibratorInfo.FrequencyProfile(mResonantFrequency, mFrequenciesHz,
+                            mOutputAccelerationsGs));
             infoBuilder.setMaxEnvelopeEffectSize(mMaxEnvelopeEffectSize);
             infoBuilder.setMinEnvelopeEffectControlPointDurationMillis(
                     mMinEnvelopeEffectControlPointDurationMillis);
@@ -360,6 +366,16 @@
         mMaxAmplitudes = maxAmplitudes;
     }
 
+    /** Set the list of available frequencies. */
+    public void setFrequenciesHz(float[] frequenciesHz) {
+        mFrequenciesHz = frequenciesHz;
+    }
+
+    /** Set the max output acceleration achievable by the supported frequencies. */
+    public void setOutputAccelerationsGs(float[] outputAccelerationsGs) {
+        mOutputAccelerationsGs = outputAccelerationsGs;
+    }
+
     /** Set the duration of vendor effects in fake vibrator hardware. */
     public void setVendorEffectDuration(long durationMs) {
         mVendorEffectDuration = durationMs;