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

Commit cb3718bf authored by Lais Andrade's avatar Lais Andrade
Browse files

Create VibratorFrequencyProfile in Vibrator API.

Create public class to represent the vibrator response for the
supported frequency range to the Vibrator public API, with basic
implementation that extracts these values from FrequencyMapping.

Integrate this data with the result of Vibrator.hasFrequencyControl
method, which also checks the HAL capabilities.

Also make public the methods to provide resonant frequency and Q-factor
of the device. Update the SystemVibrator to handle multi-vibrator
scenario and return values when all vibrators have the same
configurations. This includes returning the intersection of all vibrator
max amplitude measurements in the new profile object.

Add validation to HAL values and CTS to cover expected ranges based on
the device support for frequency control.

Bug: 203785430
Test: android.os.cts.VibratorTest & VibratorInfoTest
Change-Id: I0b7cf599ae6039355a0a198f4bdaafb85fb7422c
parent 6ae72448
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -32955,9 +32955,13 @@ package android.os {
    method @NonNull public int[] areEffectsSupported(@NonNull int...);
    method @NonNull public boolean[] arePrimitivesSupported(@NonNull int...);
    method @RequiresPermission(android.Manifest.permission.VIBRATE) public abstract void cancel();
    method @Nullable public android.os.vibrator.VibratorFrequencyProfile getFrequencyProfile();
    method public int getId();
    method @NonNull public int[] getPrimitiveDurations(@NonNull int...);
    method public float getQFactor();
    method public float getResonantFrequency();
    method public abstract boolean hasAmplitudeControl();
    method public boolean hasFrequencyControl();
    method public abstract boolean hasVibrator();
    method @Deprecated @RequiresPermission(android.Manifest.permission.VIBRATE) public void vibrate(long);
    method @Deprecated @RequiresPermission(android.Manifest.permission.VIBRATE) public void vibrate(long, android.media.AudioAttributes);
@@ -33283,6 +33287,17 @@ package android.os.strictmode {
}
package android.os.vibrator {
  public final class VibratorFrequencyProfile {
    method public float getMaxAmplitudeMeasurementInterval();
    method @FloatRange(from=0, to=1) @NonNull public float[] getMaxAmplitudeMeasurements();
    method public float getMaxFrequency();
    method public float getMinFrequency();
  }
}
package android.preference {
  @Deprecated public class CheckBoxPreference extends android.preference.TwoStatePreference {
+268 −46
Original line number Diff line number Diff line
@@ -18,18 +18,25 @@ package android.os;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Range;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Function;

/**
 * Vibrator implementation that controls the main system vibrator.
@@ -51,7 +58,7 @@ public class SystemVibrator extends Vibrator {

    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private AllVibratorsInfo mVibratorInfo;
    private VibratorInfo mVibratorInfo;

    @UnsupportedAppUsage
    public SystemVibrator(Context context) {
@@ -71,6 +78,11 @@ public class SystemVibrator extends Vibrator {
                return VibratorInfo.EMPTY_VIBRATOR_INFO;
            }
            int[] vibratorIds = mVibratorManager.getVibratorIds();
            if (vibratorIds.length == 0) {
                // It is known that the device has no vibrator, so cache and return info that
                // reflects the lack of support for effects/primitives.
                return mVibratorInfo = new NoVibratorInfo();
            }
            VibratorInfo[] vibratorInfos = new VibratorInfo[vibratorIds.length];
            for (int i = 0; i < vibratorIds.length; i++) {
                Vibrator vibrator = mVibratorManager.getVibrator(vibratorIds[i]);
@@ -83,7 +95,12 @@ public class SystemVibrator extends Vibrator {
                }
                vibratorInfos[i] = vibrator.getInfo();
            }
            return mVibratorInfo = new AllVibratorsInfo(vibratorInfos);
            if (vibratorInfos.length == 1) {
                // Device has a single vibrator info, cache and return successfully loaded info.
                return mVibratorInfo = new VibratorInfo(/* id= */ -1, vibratorInfos[0]);
            }
            // Device has multiple vibrators, generate a single info representing all of them.
            return mVibratorInfo = new MultiVibratorInfo(vibratorInfos);
        }
    }

@@ -257,77 +274,282 @@ public class SystemVibrator extends Vibrator {
    }

    /**
     * Represents all the vibrators information as a single {@link VibratorInfo}.
     * Represents a device with no vibrator as a single {@link VibratorInfo}.
     *
     * <p>This uses the first vibrator on the list as the default one for all hardware spec, but
     * uses an intersection of all vibrators to decide the capabilities and effect/primitive
     * @hide
     */
    @VisibleForTesting
    public static class NoVibratorInfo extends VibratorInfo {
        public NoVibratorInfo() {
            // Use empty arrays to indicate no support, while null would indicate support unknown.
            super(/* id= */ -1,
                    /* capabilities= */ 0,
                    /* supportedEffects= */ new SparseBooleanArray(),
                    /* supportedBraking= */ new SparseBooleanArray(),
                    /* supportedPrimitives= */ new SparseIntArray(),
                    /* primitiveDelayMax= */ 0,
                    /* compositionSizeMax= */ 0,
                    /* pwlePrimitiveDurationMax= */ 0,
                    /* pwleSizeMax= */ 0,
                    /* qFactor= */ Float.NaN,
                    new FrequencyProfile(/* resonantFrequencyHz= */ Float.NaN,
                            /* minFrequencyHz= */ Float.NaN,
                            /* frequencyResolutionHz= */ Float.NaN,
                            /* maxAmplitudes= */ null));
        }
    }

    /**
     * Represents multiple vibrator information as a single {@link VibratorInfo}.
     *
     * <p>This uses an intersection of all vibrators to decide the capabilities and effect/primitive
     * support.
     *
     * @hide
     */
    @VisibleForTesting
    public static class AllVibratorsInfo extends VibratorInfo {
        private final VibratorInfo[] mVibratorInfos;
    public static class MultiVibratorInfo extends VibratorInfo {
        // Epsilon used for float comparison applied in calculations for the merged info.
        private static final float EPSILON = 1e-5f;

        public MultiVibratorInfo(VibratorInfo[] vibrators) {
            super(/* id= */ -1,
                    capabilitiesIntersection(vibrators),
                    supportedEffectsIntersection(vibrators),
                    supportedBrakingIntersection(vibrators),
                    supportedPrimitivesAndDurationsIntersection(vibrators),
                    integerLimitIntersection(vibrators, VibratorInfo::getPrimitiveDelayMax),
                    integerLimitIntersection(vibrators, VibratorInfo::getCompositionSizeMax),
                    integerLimitIntersection(vibrators, VibratorInfo::getPwlePrimitiveDurationMax),
                    integerLimitIntersection(vibrators, VibratorInfo::getPwleSizeMax),
                    floatPropertyIntersection(vibrators, VibratorInfo::getQFactor),
                    frequencyProfileIntersection(vibrators));
        }

        public AllVibratorsInfo(VibratorInfo[] vibrators) {
            super(/* id= */ -1, capabilitiesIntersection(vibrators),
                    vibrators.length > 0 ? vibrators[0] : VibratorInfo.EMPTY_VIBRATOR_INFO);
            mVibratorInfos = vibrators;
        private static int capabilitiesIntersection(VibratorInfo[] infos) {
            int intersection = ~0;
            for (VibratorInfo info : infos) {
                intersection &= info.getCapabilities();
            }
            return intersection;
        }

        @Override
        public int isEffectSupported(int effectId) {
            if (mVibratorInfos.length == 0) {
                return Vibrator.VIBRATION_EFFECT_SUPPORT_NO;
        @Nullable
        private static SparseBooleanArray supportedBrakingIntersection(VibratorInfo[] infos) {
            for (VibratorInfo info : infos) {
                if (!info.isBrakingSupportKnown()) {
                    // If one vibrator support is unknown, then the intersection is also unknown.
                    return null;
                }
            }

            SparseBooleanArray intersection = new SparseBooleanArray();
            SparseBooleanArray firstVibratorBraking = infos[0].getSupportedBraking();

            brakingIdLoop:
            for (int i = 0; i < firstVibratorBraking.size(); i++) {
                int brakingId = firstVibratorBraking.keyAt(i);
                if (!firstVibratorBraking.valueAt(i)) {
                    // The first vibrator already doesn't support this braking, so skip it.
                    continue brakingIdLoop;
                }
            int supported = Vibrator.VIBRATION_EFFECT_SUPPORT_YES;
            for (VibratorInfo info : mVibratorInfos) {
                int effectSupported = info.isEffectSupported(effectId);
                if (effectSupported == Vibrator.VIBRATION_EFFECT_SUPPORT_NO) {
                    return effectSupported;
                } else if (effectSupported == Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN) {
                    supported = effectSupported;

                for (int j = 1; j < infos.length; j++) {
                    if (!infos[j].hasBrakingSupport(brakingId)) {
                        // One vibrator doesn't support this braking, so the intersection doesn't.
                        continue brakingIdLoop;
                    }
                }
            return supported;

                intersection.put(brakingId, true);
            }

        @Override
        public boolean isPrimitiveSupported(int primitiveId) {
            if (mVibratorInfos.length == 0) {
                return false;
            return intersection;
        }
            for (VibratorInfo info : mVibratorInfos) {
                if (!info.isPrimitiveSupported(primitiveId)) {
                    return false;

        @Nullable
        private static SparseBooleanArray supportedEffectsIntersection(VibratorInfo[] infos) {
            for (VibratorInfo info : infos) {
                if (!info.isEffectSupportKnown()) {
                    // If one vibrator support is unknown, then the intersection is also unknown.
                    return null;
                }
            }
            return true;

            SparseBooleanArray intersection = new SparseBooleanArray();
            SparseBooleanArray firstVibratorEffects = infos[0].getSupportedEffects();

            effectIdLoop:
            for (int i = 0; i < firstVibratorEffects.size(); i++) {
                int effectId = firstVibratorEffects.keyAt(i);
                if (!firstVibratorEffects.valueAt(i)) {
                    // The first vibrator already doesn't support this effect, so skip it.
                    continue effectIdLoop;
                }

        @Override
        public int getPrimitiveDuration(int primitiveId) {
            int maxDuration = 0;
            for (VibratorInfo info : mVibratorInfos) {
                int duration = info.getPrimitiveDuration(primitiveId);
                if (duration == 0) {
                    return 0;
                for (int j = 1; j < infos.length; j++) {
                    if (infos[j].isEffectSupported(effectId) != VIBRATION_EFFECT_SUPPORT_YES) {
                        // One vibrator doesn't support this effect, so the intersection doesn't.
                        continue effectIdLoop;
                    }
                maxDuration = Math.max(maxDuration, duration);
                }
            return maxDuration;

                intersection.put(effectId, true);
            }

        private static int capabilitiesIntersection(VibratorInfo[] infos) {
            if (infos.length == 0) {
                return 0;
            return intersection;
        }

        @NonNull
        private static SparseIntArray supportedPrimitivesAndDurationsIntersection(
                VibratorInfo[] infos) {
            SparseIntArray intersection = new SparseIntArray();
            SparseIntArray firstVibratorPrimitives = infos[0].getSupportedPrimitives();

            primitiveIdLoop:
            for (int i = 0; i < firstVibratorPrimitives.size(); i++) {
                int primitiveId = firstVibratorPrimitives.keyAt(i);
                int primitiveDuration = firstVibratorPrimitives.valueAt(i);
                if (primitiveDuration == 0) {
                    // The first vibrator already doesn't support this primitive, so skip it.
                    continue primitiveIdLoop;
                }

                for (int j = 1; j < infos.length; j++) {
                    int vibratorPrimitiveDuration = infos[j].getPrimitiveDuration(primitiveId);
                    if (vibratorPrimitiveDuration == 0) {
                        // One vibrator doesn't support this primitive, so the intersection doesn't.
                        continue primitiveIdLoop;
                    } else {
                        // The primitive vibration duration is the maximum among all vibrators.
                        primitiveDuration = Math.max(primitiveDuration, vibratorPrimitiveDuration);
                    }
            int intersection = ~0;
            for (VibratorInfo info : infos) {
                intersection &= info.getCapabilities();
                }

                intersection.put(primitiveId, primitiveDuration);
            }
            return intersection;
        }

        private static int integerLimitIntersection(VibratorInfo[] infos,
                Function<VibratorInfo, Integer> propertyGetter) {
            int limit = 0; // Limit 0 means unlimited
            for (VibratorInfo info : infos) {
                int vibratorLimit = propertyGetter.apply(info);
                if ((limit == 0) || (vibratorLimit > 0 && vibratorLimit < limit)) {
                    // This vibrator is limited and intersection is unlimited or has a larger limit:
                    // use smaller limit here for the intersection.
                    limit = vibratorLimit;
                }
            }
            return limit;
        }

        private static float floatPropertyIntersection(VibratorInfo[] infos,
                Function<VibratorInfo, Float> propertyGetter) {
            float property = propertyGetter.apply(infos[0]);
            if (Float.isNaN(property)) {
                // If one vibrator is undefined then the intersection is undefined.
                return Float.NaN;
            }
            for (int i = 1; i < infos.length; i++) {
                if (Float.compare(property, propertyGetter.apply(infos[i])) != 0) {
                    // If one vibrator has a different value then the intersection is undefined.
                    return Float.NaN;
                }
            }
            return property;
        }

        @NonNull
        private static FrequencyProfile frequencyProfileIntersection(VibratorInfo[] infos) {
            float freqResolution = floatPropertyIntersection(infos,
                    info -> info.getFrequencyProfile().getFrequencyResolutionHz());
            float resonantFreq = floatPropertyIntersection(infos,
                    VibratorInfo::getResonantFrequencyHz);
            Range<Float> freqRange = frequencyRangeIntersection(infos, freqResolution);

            if ((freqRange == null) || Float.isNaN(freqResolution)) {
                return new FrequencyProfile(resonantFreq, Float.NaN, freqResolution, null);
            }

            int amplitudeCount =
                    Math.round(1 + (freqRange.getUpper() - freqRange.getLower()) / freqResolution);
            float[] maxAmplitudes = new float[amplitudeCount];

            // Use MAX_VALUE here to ensure that the FrequencyProfile constructor called with this
            // will fail if the loop below is broken and do not replace filled values with actual
            // vibrator measurements.
            Arrays.fill(maxAmplitudes, Float.MAX_VALUE);

            for (VibratorInfo info : infos) {
                Range<Float> vibratorFreqRange = info.getFrequencyProfile().getFrequencyRangeHz();
                float[] vibratorMaxAmplitudes = info.getFrequencyProfile().getMaxAmplitudes();
                int vibratorStartIdx = Math.round(
                        (freqRange.getLower() - vibratorFreqRange.getLower()) / freqResolution);
                int vibratorEndIdx = vibratorStartIdx + maxAmplitudes.length - 1;

                if ((vibratorStartIdx < 0) || (vibratorEndIdx >= vibratorMaxAmplitudes.length)) {
                    Slog.w(TAG, "Error calculating the intersection of vibrator frequency"
                            + " profiles: attempted to fetch from vibrator "
                            + info.getId() + " max amplitude with bad index " + vibratorStartIdx);
                    return new FrequencyProfile(resonantFreq, Float.NaN, Float.NaN, null);
                }

                for (int i = 0; i < maxAmplitudes.length; i++) {
                    maxAmplitudes[i] = Math.min(maxAmplitudes[i],
                            vibratorMaxAmplitudes[vibratorStartIdx + i]);
                }
            }

            return new FrequencyProfile(resonantFreq, freqRange.getLower(),
                    freqResolution, maxAmplitudes);
        }

        @Nullable
        private static Range<Float> frequencyRangeIntersection(VibratorInfo[] infos,
                float frequencyResolution) {
            Range<Float> firstRange = infos[0].getFrequencyProfile().getFrequencyRangeHz();
            if (firstRange == null) {
                // If one vibrator is undefined then the intersection is undefined.
                return null;
            }
            float intersectionLower = firstRange.getLower();
            float intersectionUpper = firstRange.getUpper();

            // Generate the intersection of all vibrator supported ranges, making sure that both
            // min supported frequencies are aligned w.r.t. the frequency resolution.

            for (int i = 1; i < infos.length; i++) {
                Range<Float> vibratorRange = infos[i].getFrequencyProfile().getFrequencyRangeHz();
                if (vibratorRange == null) {
                    // If one vibrator is undefined then the intersection is undefined.
                    return null;
                }

                if ((vibratorRange.getLower() >= intersectionUpper)
                        || (vibratorRange.getUpper() <= intersectionLower)) {
                    // If the range and intersection are disjoint then the intersection is undefined
                    return null;
                }

                float frequencyDelta = Math.abs(intersectionLower - vibratorRange.getLower());
                if ((frequencyDelta % frequencyResolution) > EPSILON) {
                    // If the intersection is not aligned with one vibrator then it's undefined
                    return null;
                }

                intersectionLower = Math.max(intersectionLower, vibratorRange.getLower());
                intersectionUpper = Math.min(intersectionUpper, vibratorRange.getUpper());
            }

            if ((intersectionUpper - intersectionLower) < frequencyResolution) {
                // If the intersection is empty then it's undefined.
                return null;
            }

            return Range.create(intersectionLower, intersectionUpper);
        }
    }

    /** Listener for all vibrators state change. */
+31 −10
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.content.res.Resources;
import android.hardware.vibrator.IVibrator;
import android.media.AudioAttributes;
import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibratorFrequencyProfile;
import android.util.Log;

import java.lang.annotation.Retention;
@@ -208,8 +209,8 @@ public abstract class Vibrator {
    /**
     * Check whether the vibrator has independent frequency control.
     *
     * @return True if the hardware can control the frequency of the vibrations, otherwise false.
     * @hide
     * @return True if the hardware can control the frequency of the vibrations independently of
     * the vibration amplitude, false otherwise.
     */
    public boolean hasFrequencyControl() {
        // We currently can only control frequency of the vibration using the compose PWLE method.
@@ -229,27 +230,47 @@ public abstract class Vibrator {
    }

    /**
     * Gets the resonant frequency of the vibrator.
     * Gets the resonant frequency of the vibrator, if applicable.
     *
     * @return the resonant frequency of the vibrator, or {@link Float#NaN NaN} if it's unknown or
     * this vibrator is a composite of multiple physical devices.
     * @hide
     * @return the resonant frequency of the vibrator, or {@link Float#NaN NaN} if it's unknown, not
     * applicable, or if this vibrator is a composite of multiple physical devices with different
     * frequencies.
     */
    public float getResonantFrequency() {
        return getInfo().getResonantFrequency();
        return getInfo().getResonantFrequencyHz();
    }

    /**
     * Gets the <a href="https://en.wikipedia.org/wiki/Q_factor">Q factor</a> of the vibrator.
     *
     * @return the Q factor of the vibrator, or {@link Float#NaN NaN} if it's unknown or
     *         this vibrator is a composite of multiple physical devices.
     * @hide
     * @return the Q factor of the vibrator, or {@link Float#NaN NaN} if it's unknown, not
     * applicable, or if this vibrator is a composite of multiple physical devices with different
     * Q factors.
     */
    public float getQFactor() {
        return getInfo().getQFactor();
    }

    /**
     * Gets the profile that describes the vibrator output across the supported frequency range.
     *
     * <p>The profile describes the relative 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.
     */
    @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.
     *
+115 −69

File changed.

Preview size limit exceeded, changes collapsed.

+100 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.FloatRange;
import android.annotation.NonNull;
import android.os.VibratorInfo;

import com.android.internal.util.Preconditions;

/**
 * Describes the output of a {@link android.os.Vibrator} for different vibration frequencies.
 *
 * <p>The profile contains the minimum and maximum supported vibration frequencies, if the device
 * supports independent frequency control.
 *
 * <p>It also describes the relative output acceleration of a vibration at different supported
 * frequencies. The acceleration is defined by a relative amplitude value between 0 and 1,
 * inclusive, where 0 represents the vibrator off state and 1 represents the maximum output
 * acceleration that the vibrator can reach across all supported frequencies.
 *
 * <p>The measurements are returned as an array of uniformly distributed amplitude values for
 * frequencies between the minimum and maximum supported ones. The measurement interval is the
 * frequency increment between each pair of amplitude values.
 *
 * <p>Vibrators without independent frequency control do not have a frequency profile.
 */
public final class VibratorFrequencyProfile {

    private final VibratorInfo.FrequencyProfile mFrequencyProfile;

    /** @hide */
    public VibratorFrequencyProfile(@NonNull VibratorInfo.FrequencyProfile frequencyProfile) {
        Preconditions.checkArgument(!frequencyProfile.isEmpty(),
                "Frequency profile must have a non-empty frequency range");
        mFrequencyProfile = frequencyProfile;
    }

    /**
     * Measurements of the maximum relative amplitude the vibrator can achieve for each supported
     * frequency.
     *
     * <p>The frequency of a measurement is determined as:
     *
     * {@code getMinFrequency() + measurementIndex * getMaxAmplitudeMeasurementInterval()}
     *
     * <p>The returned list will not be empty, and will have entries representing frequencies from
     * {@link #getMinFrequency()} to {@link #getMaxFrequency()}, inclusive.
     *
     * @return Array of maximum relative amplitude measurements, each value is between 0 and 1,
     * inclusive.
     */
    @NonNull
    @FloatRange(from = 0, to = 1)
    public float[] getMaxAmplitudeMeasurements() {
        // VibratorInfo getters always return a copy or clone of the data objects.
        return mFrequencyProfile.getMaxAmplitudes();
    }

    /**
     * Gets the frequency interval used to measure the maximum relative amplitudes.
     *
     * @return the frequency interval used for the measurement, in hertz.
     */
    public float getMaxAmplitudeMeasurementInterval() {
        return mFrequencyProfile.getFrequencyResolutionHz();
    }

    /**
     * Gets the minimum frequency supported by the vibrator.
     *
     * @return the minimum frequency supported by the vibrator, in hertz.
     */
    public float getMinFrequency() {
        return mFrequencyProfile.getFrequencyRangeHz().getLower();
    }

    /**
     * Gets the maximum frequency supported by the vibrator.
     *
     * @return the maximum frequency supported by the vibrator, in hertz.
     */
    public float getMaxFrequency() {
        return mFrequencyProfile.getFrequencyRangeHz().getUpper();
    }
}
Loading