Loading core/api/current.txt +15 −0 Original line number Diff line number Diff line Loading @@ -33045,9 +33045,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); Loading Loading @@ -33373,6 +33377,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 { core/java/android/os/SystemVibrator.java +268 −46 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) { Loading @@ -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]); Loading @@ -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); } } Loading Loading @@ -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. */ Loading core/java/android/os/Vibrator.java +31 −10 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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. Loading @@ -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. * Loading core/java/android/os/VibratorInfo.java +115 −69 File changed.Preview size limit exceeded, changes collapsed. Show changes core/java/android/os/vibrator/VibratorFrequencyProfile.java 0 → 100644 +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
core/api/current.txt +15 −0 Original line number Diff line number Diff line Loading @@ -33045,9 +33045,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); Loading Loading @@ -33373,6 +33377,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 {
core/java/android/os/SystemVibrator.java +268 −46 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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) { Loading @@ -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]); Loading @@ -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); } } Loading Loading @@ -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. */ Loading
core/java/android/os/Vibrator.java +31 −10 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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. Loading @@ -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. * Loading
core/java/android/os/VibratorInfo.java +115 −69 File changed.Preview size limit exceeded, changes collapsed. Show changes
core/java/android/os/vibrator/VibratorFrequencyProfile.java 0 → 100644 +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(); } }