diff options
14 files changed, 1006 insertions, 13 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index c91992c29630..7a07d582ed32 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -34338,6 +34338,7 @@ package android.os { 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(); @@ -34691,6 +34692,19 @@ package android.os.strictmode { } +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 7327630bca39..c4c4580bf0a8 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -34,6 +34,7 @@ import android.hardware.vibrator.IVibrator; 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 @@ public abstract class Vibrator { } /** + * 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 f7fff391147b..9419032c46f8 100644 --- a/core/java/android/os/VibratorInfo.java +++ b/core/java/android/os/VibratorInfo.java @@ -20,8 +20,10 @@ import android.annotation.NonNull; 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 com.android.internal.util.Preconditions; 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 @@ public class VibratorInfo implements Parcelable { 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 @@ public class VibratorInfo implements Parcelable { 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 @@ public class VibratorInfo implements Parcelable { 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 @@ public class VibratorInfo implements Parcelable { * @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 @@ public class VibratorInfo implements Parcelable { @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 @@ public class VibratorInfo implements Parcelable { mPwleSizeMax = pwleSizeMax; mQFactor = qFactor; mFrequencyProfileLegacy = frequencyProfileLegacy; + mFrequencyProfile = frequencyProfile; mMaxEnvelopeEffectSize = maxEnvelopeEffectSize; mMinEnvelopeEffectControlPointDurationMillis = minEnvelopeEffectControlPointDurationMillis; @@ -156,6 +177,7 @@ public class VibratorInfo implements Parcelable { 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 @@ public class VibratorInfo implements Parcelable { && 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 @@ public class VibratorInfo implements Parcelable { @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 @@ public class VibratorInfo implements Parcelable { + ", mPwleSizeMax=" + mPwleSizeMax + ", mQFactor=" + mQFactor + ", mFrequencyProfileLegacy=" + mFrequencyProfileLegacy + + ", mFrequencyProfile=" + mFrequencyProfile + ", mMaxEnvelopeEffectSize=" + mMaxEnvelopeEffectSize + ", mMinEnvelopeEffectControlPointDurationMillis=" + mMinEnvelopeEffectControlPointDurationMillis @@ -263,6 +287,7 @@ public class VibratorInfo implements Parcelable { 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 @@ public class VibratorInfo implements Parcelable { * 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 @@ public class VibratorInfo implements Parcelable { 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 @@ public class VibratorInfo implements Parcelable { } /** + * 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 @@ public class VibratorInfo implements Parcelable { 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 @@ public class VibratorInfo implements Parcelable { } /** + * 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 @@ public class VibratorInfo implements Parcelable { 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 37dae56abc6d..1ba8d9973d2b 100644 --- a/core/java/android/os/vibrator/MultiVibratorInfo.java +++ b/core/java/android/os/vibrator/MultiVibratorInfo.java @@ -27,6 +27,9 @@ import android.util.SparseBooleanArray; 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 @@ public final class MultiVibratorInfo extends VibratorInfo { 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 @@ public final class MultiVibratorInfo extends VibratorInfo { integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax), integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax), floatPropertyIntersection(vibrators, VibratorInfo::getQFactor), + mergedLegacyProfile, mergedProfile, integerLimitIntersection(vibrators, VibratorInfo::getMaxEnvelopeEffectSize), @@ -209,7 +218,82 @@ public final class MultiVibratorInfo extends VibratorInfo { } @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 000000000000..2b5f9bf2a22e --- /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 47d01c4452f6..04945f38e319 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 static org.junit.Assert.assertTrue; 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 @@ public class VibratorInfoTest { 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 @@ public class VibratorInfoTest { } @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 @@ public class VibratorInfoTest { } @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 @@ public class VibratorInfoTest { } @Test - public void testGetFrequencyRangeHz_validProfileReturnsMappedValues() { + public void testLegacyGetFrequencyRangeHz_validProfileReturnsMappedValues() { VibratorInfo.FrequencyProfileLegacy profile = new VibratorInfo.FrequencyProfileLegacy( /* resonantFrequencyHz= */ 150, /* minFrequencyHz= */ 50, @@ -306,6 +426,7 @@ public class VibratorInfoTest { .setPwleSizeMax(20) .setQFactor(2f) .setFrequencyProfileLegacy(TEST_FREQUENCY_PROFILE_LEGACY) + .setFrequencyProfile(TEST_FREQUENCY_PROFILE) .setMaxEnvelopeEffectSize(16) .setMinEnvelopeEffectControlPointDurationMillis(20) .setMaxEnvelopeEffectControlPointDurationMillis(1_000); @@ -347,18 +468,33 @@ public class VibratorInfoTest { 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 @@ public class VibratorInfoTest { .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 f192b896e1db..c9ab29722011 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.hardware.vibrator.IVibrator; 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 @@ import org.junit.runners.JUnit4; 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 @@ public class MultiVibratorInfoTest { } @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 @@ public class MultiVibratorInfoTest { } @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 @@ public class MultiVibratorInfoTest { } @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 @@ public class MultiVibratorInfoTest { } @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 @@ public class MultiVibratorInfoTest { } @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 @@ public class MultiVibratorInfoTest { } @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 @@ public class MultiVibratorInfoTest { 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 bcd0b94b6877..903d892515b7 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 JavaVM* sJvm = nullptr; 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 @@ static struct { jmethodID setCompositionSizeMax; jmethodID setQFactor; jmethodID setFrequencyProfileLegacy; + jmethodID setFrequencyProfile; jmethodID setMaxEnvelopeEffectSize; jmethodID setMinEnvelopeEffectControlPointDurationMillis; jmethodID setMaxEnvelopeEffectControlPointDurationMillis; @@ -524,6 +527,40 @@ static jboolean vibratorGetInfo(JNIEnv* env, jclass /* clazz */, jlong ptr, 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 @@ int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env 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 @@ int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env 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 1493253a50d4..d7ae04614442 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.RampSegment; 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 @@ public class DeviceAdapterTest { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private PackageManagerInternal mPackageManagerInternalMock; @@ -186,6 +190,7 @@ public class DeviceAdapterTest { } @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 81036824f92b..96f0fda263f1 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.PrimitiveSegment; 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 @@ public class RampToStepAdapterTest { 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 @@ public class RampToStepAdapterTest { } @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 f2c3726375e6..53e49e0c711f 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.PrimitiveSegment; 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 @@ public class SplitSegmentsAdapterTest { private SplitSegmentsAdapter mAdapter; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setUp() throws Exception { mAdapter = new SplitSegmentsAdapter(); @@ -97,6 +103,7 @@ public class SplitSegmentsAdapterTest { } @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 d501dba13ac3..fae634d0bd39 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.PrimitiveSegment; 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 @@ public class StepToRampAdapterTest { private StepToRampAdapter mAdapter; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setUp() throws Exception { mAdapter = new StepToRampAdapter(); @@ -134,6 +140,7 @@ public class StepToRampAdapterTest { } @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 7536f5f54ba2..58a1e84e127d 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.RampSegment; 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 @@ public class VibrationThreadTest { @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 @@ public class VibrationThreadTest { } @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 @@ public class VibrationThreadTest { } @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 @@ public class VibrationThreadTest { } @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 f96177d98052..6dc1b10ec930 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 @@ public final class FakeVibratorControllerProvider { 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 @@ public final class FakeVibratorControllerProvider { 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 @@ public final class FakeVibratorControllerProvider { 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; |