Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b6de494d authored by Ahmad Khalil's avatar Ahmad Khalil
Browse files

Removing FrequencyProfile dependency on resonance

The FrequencyProfile validation should only depend on resonant frequency, if it is available.

The segment validation should fail immediately if the FrequencyProfile is invalid.

Bug: 430503750
Flag: android.os.vibrator.decouple_frequency_profile_from_resonance
Test: N/A
Change-Id: Ib8353878b00adbcaf29fe847f7f00c24e19d061a
parent 7a1044ec
Loading
Loading
Loading
Loading
+14 −44
Original line number Diff line number Diff line
@@ -23,8 +23,8 @@ 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.Slog;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;

@@ -32,7 +32,6 @@ 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;
@@ -691,13 +690,23 @@ public class VibratorInfo implements Parcelable {

            mResonantFrequencyHz = resonantFrequencyHz;

            boolean isValid = !Float.isNaN(resonantFrequencyHz)
                    && (resonantFrequencyHz > 0)
                    && (frequenciesHz != null && outputAccelerationsGs != null)
            boolean isValid = (frequenciesHz != null && outputAccelerationsGs != null)
                    && (frequenciesHz.length == outputAccelerationsGs.length)
                    && (frequenciesHz.length > 0);

            if (Flags.decoupleFrequencyProfileFromResonance()) {
                isValid = isValid
                        && (Float.isNaN(resonantFrequencyHz) || (resonantFrequencyHz > 0));
            } else {
                isValid = isValid && !Float.isNaN(resonantFrequencyHz) && (resonantFrequencyHz > 0);
            }

            if (!isValid) {
                Slog.e(TAG, "Invalid frequency profile received from HAL."
                        + " resonantFrequencyHz=" + resonantFrequencyHz
                        + ", frequenciesHz=" + Arrays.toString(frequenciesHz)
                        + ", outputAccelerationsGs=" + Arrays.toString(outputAccelerationsGs));

                mFrequenciesHz = null;
                mOutputAccelerationsGs = null;
                mMinFrequencyHz = Float.NaN;
@@ -916,45 +925,6 @@ public class VibratorInfo implements Parcelable {
                        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;
        }
    }

    /**
+10 −0
Original line number Diff line number Diff line
@@ -176,3 +176,13 @@ flag {
      purpose: PURPOSE_FEATURE
    }
}

flag {
    namespace: "haptics"
    name: "decouple_frequency_profile_from_resonance"
    description: "Removes frequency profiles dependency on resonant frequency when not present"
    bug: "430503750"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}
+34 −1
Original line number Diff line number Diff line
@@ -26,14 +26,23 @@ import static org.junit.Assert.assertTrue;

import android.hardware.vibrator.Braking;
import android.hardware.vibrator.IVibrator;
import android.os.vibrator.Flags;
import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.Range;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class VibratorInfoTest {
    @Rule
    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();

    private static final float TEST_TOLERANCE = 1e-5f;

    private static final int TEST_VIBRATOR_ID = 1;
@@ -195,7 +204,8 @@ public class VibratorInfoTest {
    }

    @Test
    public void testFrequencyProfile_invalidValuesCreatesEmptyProfile() {
    @RequiresFlagsDisabled(Flags.FLAG_DECOUPLE_FREQUENCY_PROFILE_FROM_RESONANCE)
    public void testFrequencyProfile_flagDisabled_invalidValuesCreatesEmptyProfile() {
        // Invalid resonant frequency.
        assertThat(new VibratorInfo.FrequencyProfile(Float.NaN,
                TEST_FREQUENCIES, TEST_OUTPUT_ACCELERATIONS).isEmpty()).isTrue();
@@ -210,6 +220,29 @@ public class VibratorInfoTest {
                /*outputAccelerationsGs=*/ new float[]{0.8f, 1.0f, 2.0f}).isEmpty()).isTrue();
    }

    @Test
    @RequiresFlagsEnabled(Flags.FLAG_DECOUPLE_FREQUENCY_PROFILE_FROM_RESONANCE)
    public void testFrequencyProfile_invalidValuesCreatesEmptyProfile() {
        // Negative resonant frequency.
        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
    @RequiresFlagsEnabled(Flags.FLAG_DECOUPLE_FREQUENCY_PROFILE_FROM_RESONANCE)
    public void testFrequencyProfile_creationWithoutResonantFrequency_isValid() {
        // Frequency profile is not dependent on resonant frequency.
        assertThat(new VibratorInfo.FrequencyProfile(Float.NaN,
                TEST_FREQUENCIES, TEST_OUTPUT_ACCELERATIONS).isEmpty()).isFalse();
    }

    @Test
    public void testGetFrequenciesAndOutputAccelerations_noFrequencyAccelerationData_returnNull() {
        VibratorInfo.FrequencyProfile emptyFrequencyProfile =
+46 −16
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.server.vibrator;
import android.hardware.vibrator.IVibrator;
import android.os.VibratorInfo;
import android.os.vibrator.BasicPwleSegment;
import android.os.vibrator.Flags;
import android.os.vibrator.PwleSegment;
import android.os.vibrator.VibrationEffectSegment;

@@ -28,17 +29,15 @@ import java.util.List;
 * Validates {@link PwleSegment} and {@link BasicPwleSegment} instances to ensure they are
 * compatible with the device's capabilities.
 *
 * <p>This validator performs the following checks:
 * <p>This validator enforced the following rules:
 * <ul>
 *   <li>For {@link PwleSegment}:
 *   <li>A {@link BasicPwleSegment} is always considered <b>invalid</b>. Its presence indicates
 *   a failure in the steps that should have converted it to a {@link PwleSegment}.</li>
 *   <li>For a {@link PwleSegment} to be valid, all of the following must be true:
 *     <ul>
 *       <li>Verifies that the device supports {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
 *       <li>Verifies that each segment's start and end frequencies fall within the supported range.
 *     </ul>
 *   </li>
 *   <li>For {@link BasicPwleSegment}:
 *     <ul>
 *       <li>Verifies that the device supports {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
 *       <li>The device must support {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.</li>
 *       <li>The device has a valid {@link VibratorInfo.FrequencyProfile}.</li>
 *       <li>The segment's start and end frequencies must fall within the supported range.</li>
 *     </ul>
 *   </li>
 * </ul>
@@ -49,8 +48,11 @@ final class PwleSegmentsValidator implements VibrationSegmentsValidator {
    public boolean hasValidSegments(VibratorInfo info, List<VibrationEffectSegment> segments) {

        boolean hasPwleCapability = info.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
        float minFrequency = info.getFrequencyProfile().getMinFrequencyHz();
        float maxFrequency = info.getFrequencyProfile().getMaxFrequencyHz();
        VibratorInfo.FrequencyProfile frequencyProfile = info.getFrequencyProfile();

        if (!Flags.decoupleFrequencyProfileFromResonance()) {
            float minFrequency = frequencyProfile.getMinFrequencyHz();
            float maxFrequency = frequencyProfile.getMaxFrequencyHz();

            for (VibrationEffectSegment segment : segments) {
                if (segment instanceof BasicPwleSegment && !hasPwleCapability) {
@@ -68,4 +70,32 @@ final class PwleSegmentsValidator implements VibrationSegmentsValidator {

            return true;
        }


        for (VibrationEffectSegment segment : segments) {
            if (segment instanceof BasicPwleSegment) {
                return false;
            }
            if (segment instanceof PwleSegment pwleSegment) {
                if (!hasPwleCapability || isFrequencyOutOfRange(pwleSegment.getStartFrequencyHz(),
                        frequencyProfile) || isFrequencyOutOfRange(pwleSegment.getEndFrequencyHz(),
                        frequencyProfile)) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Checks if the given frequency is unsupported.
     *
     * <p>A frequency is considered unsupported if it's outside the supported range or if the
     * frequency profile is empty.
     */
    private boolean isFrequencyOutOfRange(float frequency, VibratorInfo.FrequencyProfile profile) {
        return profile.isEmpty() || frequency < profile.getMinFrequencyHz()
                || frequency > profile.getMaxFrequencyHz();
    }
}
+65 −1
Original line number Diff line number Diff line
@@ -77,7 +77,8 @@ public class DeviceAdapterTest {
    private static final int PWLE_WITHOUT_FREQUENCIES_VIBRATOR_ID = 3;
    private static final int PWLE_V2_VIBRATOR_ID = 4;
    private static final int PWLE_V2_BASIC_VIBRATOR_ID = 5;
    private static final int BASIC_VIBRATOR_ID = 6;
    private static final int PWLE_V2_EMPTY_PROFILE_VIBRATOR_ID = 6;
    private static final int BASIC_VIBRATOR_ID = 7;
    private static final float TEST_MIN_FREQUENCY = 50;
    private static final float TEST_RESONANT_FREQUENCY = 150;
    private static final float TEST_FREQUENCY_RESOLUTION = 25;
@@ -133,6 +134,8 @@ public class DeviceAdapterTest {
        vibrators.put(PWLE_V2_BASIC_VIBRATOR_ID,
                createPwleV2Vibrator(PWLE_V2_VIBRATOR_ID, TEST_BASIC_FREQUENCIES_HZ,
                        TEST_BASIC_OUTPUT_ACCELERATIONS_GS));
        vibrators.put(PWLE_V2_EMPTY_PROFILE_VIBRATOR_ID,
                createPwleV2VibratorWithEmptyProfile(PWLE_V2_EMPTY_PROFILE_VIBRATOR_ID));
        vibrators.put(BASIC_VIBRATOR_ID, createBasicVibrator(BASIC_VIBRATOR_ID));
        for (int i = 0; i < vibrators.size(); i++) {
            vibrators.valueAt(i).init((vibratorId, vibrationId, stepId)  -> {});
@@ -322,6 +325,12 @@ public class DeviceAdapterTest {
                        new StepSegment(1, 175, 10),
                        new StepSegment(1, 0, 50)),
                        /* repeatIndex= */ 1))
                .addVibrator(PWLE_V2_EMPTY_PROFILE_VIBRATOR_ID,
                        new VibrationEffect.Composed(Arrays.asList(
                                // Step(amplitude, frequencyHz, duration)
                                new StepSegment(1, 175, 10),
                                new StepSegment(1, 0, 50)),
                                /* repeatIndex= */ 1))
                .combine();

        assertThat(CombinedVibration.createParallel(effect).adapt(mAdapter)).isEqualTo(expected);
@@ -415,6 +424,39 @@ public class DeviceAdapterTest {
        assertThat(mAdapter.adaptToVibrator(PWLE_V2_VIBRATOR_ID, effect)).isNull();
    }

    @Test
    @EnableFlags(value = {Flags.FLAG_NORMALIZED_PWLE_EFFECTS,
            Flags.FLAG_DECOUPLE_FREQUENCY_PROFILE_FROM_RESONANCE})
    public void testPwleSegment_withEmptyProfile_returnsNull() {
        VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
                new PwleSegment(1, 0.2f, 30, 60, 20),
                new PwleSegment(0.8f, 0.2f, 60, 100, 100),
                new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
                /* repeatIndex= */ 1);

        assertThat(mAdapter.adaptToVibrator(PWLE_V2_EMPTY_PROFILE_VIBRATOR_ID, effect)).isNull();
    }

    @Test
    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    @DisableFlags(Flags.FLAG_DECOUPLE_FREQUENCY_PROFILE_FROM_RESONANCE)
    public void testPwleSegment_withEmptyProfile_flagDisabled_returnsAdaptedSegments() {
        VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
                new PwleSegment(1, 0.2f, 30, 60, 20),
                new PwleSegment(0.8f, 0.2f, 60, 100, 100),
                new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
                /* repeatIndex= */ 1);

        VibrationEffect.Composed expected = new VibrationEffect.Composed(Arrays.asList(
                new PwleSegment(1, 0.2f, 30, 60, 20),
                new PwleSegment(0.8f, 0.2f, 60, 100, 100),
                new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
                /* repeatIndex= */ 1);

        assertThat(mAdapter.adaptToVibrator(PWLE_V2_EMPTY_PROFILE_VIBRATOR_ID, effect)).isEqualTo(
                expected);
    }

    @Test
    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
    public void testBasicPwleSegment_withoutPwleV2Capability_returnsNull() {
@@ -448,6 +490,19 @@ public class DeviceAdapterTest {
        assertThat(mAdapter.adaptToVibrator(PWLE_V2_BASIC_VIBRATOR_ID, effect)).isEqualTo(expected);
    }

    @Test
    @EnableFlags({Flags.FLAG_NORMALIZED_PWLE_EFFECTS,
            Flags.FLAG_DECOUPLE_FREQUENCY_PROFILE_FROM_RESONANCE})
    public void testBasicPwleSegment_withEmptyProfile_returnsNull() {
        VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
                new BasicPwleSegment(0.0f, 0.5f, 0.0f, 0.5f, 20),
                new BasicPwleSegment(0.5f, 1.0f, 0.5f, 1.0f, 100),
                new BasicPwleSegment(1.0f, 0.0f, 1.0f, 0.5f, 100)),
                /* repeatIndex= */ 1);

        assertThat(mAdapter.adaptToVibrator(PWLE_V2_EMPTY_PROFILE_VIBRATOR_ID, effect)).isNull();
    }

    @Test
    @DisableFlags(Flags.FLAG_PRIMITIVE_COMPOSITION_ABSOLUTE_DELAY)
    public void testPrimitiveWithRelativeDelay_withoutFlag_returnsNull() {
@@ -615,6 +670,15 @@ public class DeviceAdapterTest {
        return helper.newInitializedHalVibrator(vibratorId, mHalCallbacks);
    }

    private HalVibrator createPwleV2VibratorWithEmptyProfile(int vibratorId) {
        HalVibratorHelper helper = createVibratorHelperWithEffects(
                IVibrator.CAP_GET_RESONANT_FREQUENCY, IVibrator.CAP_FREQUENCY_CONTROL,
                IVibrator.CAP_COMPOSE_EFFECTS, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
        helper.setFrequenciesHz(null);
        helper.setOutputAccelerationsGs(null);
        return helper.newInitializedHalVibrator(vibratorId, mHalCallbacks);
    }

    private HalVibratorHelper createVibratorHelperWithEffects(int... capabilities) {
        HalVibratorHelper helper = new HalVibratorHelper(mTestLooper.getLooper());
        helper.setCapabilities(capabilities);