Loading core/java/android/os/SystemVibrator.java +3 −305 Original line number Original line Diff line number Diff line Loading @@ -18,26 +18,19 @@ package android.os; import android.annotation.CallbackExecutor; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Context; import android.hardware.vibrator.IVibrator; import android.os.vibrator.VibratorInfoFactory; import android.util.ArrayMap; import android.util.ArrayMap; import android.util.Log; import android.util.Log; import android.util.Range; import android.util.Slog; import android.util.SparseArray; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.ArrayList; import java.util.Arrays; import java.util.Objects; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.Executor; import java.util.function.Function; /** /** * Vibrator implementation that controls the main system vibrator. * Vibrator implementation that controls the main system vibrator. Loading Loading @@ -82,7 +75,7 @@ public class SystemVibrator extends Vibrator { if (vibratorIds.length == 0) { if (vibratorIds.length == 0) { // It is known that the device has no vibrator, so cache and return info that // It is known that the device has no vibrator, so cache and return info that // reflects the lack of support for effects/primitives. // reflects the lack of support for effects/primitives. return mVibratorInfo = new NoVibratorInfo(); return mVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO; } } VibratorInfo[] vibratorInfos = new VibratorInfo[vibratorIds.length]; VibratorInfo[] vibratorInfos = new VibratorInfo[vibratorIds.length]; for (int i = 0; i < vibratorIds.length; i++) { for (int i = 0; i < vibratorIds.length; i++) { Loading @@ -96,12 +89,7 @@ public class SystemVibrator extends Vibrator { } } vibratorInfos[i] = vibrator.getInfo(); vibratorInfos[i] = vibrator.getInfo(); } } if (vibratorInfos.length == 1) { return mVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, vibratorInfos); // 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 @@ -274,296 +262,6 @@ public class SystemVibrator extends Vibrator { } } } } /** * Represents a device with no vibrator as a single {@link VibratorInfo}. * * @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 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) { // Need to use an extra constructor to share the computation in super initialization. this(vibrators, frequencyProfileIntersection(vibrators)); } private MultiVibratorInfo(VibratorInfo[] vibrators, VibratorInfo.FrequencyProfile mergedProfile) { super(/* id= */ -1, capabilitiesIntersection(vibrators, mergedProfile.isEmpty()), 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), mergedProfile); } private static int capabilitiesIntersection(VibratorInfo[] infos, boolean frequencyProfileIsEmpty) { int intersection = ~0; for (VibratorInfo info : infos) { intersection &= info.getCapabilities(); } if (frequencyProfileIsEmpty) { // Revoke frequency control if the merged frequency profile ended up empty. intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL; } return intersection; } @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; } 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; } } intersection.put(brakingId, true); } return intersection; } @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; } } 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; } 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; } } intersection.put(effectId, true); } 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); } } 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. * Listener for all vibrators state change. * * Loading core/java/android/os/VibratorInfo.java +13 −2 Original line number Original line Diff line number Diff line Loading @@ -156,6 +156,16 @@ public class VibratorInfo implements Parcelable { return false; return false; } } VibratorInfo that = (VibratorInfo) o; VibratorInfo that = (VibratorInfo) o; return mId == that.mId && equalContent(that); } /** * Returns {@code true} only if the properties and capabilities of the provided info, except for * the ID, equals to this info. Returns {@code false} otherwise. * * @hide */ public boolean equalContent(VibratorInfo that) { int supportedPrimitivesCount = mSupportedPrimitives.size(); int supportedPrimitivesCount = mSupportedPrimitives.size(); if (supportedPrimitivesCount != that.mSupportedPrimitives.size()) { if (supportedPrimitivesCount != that.mSupportedPrimitives.size()) { return false; return false; Loading @@ -168,7 +178,7 @@ public class VibratorInfo implements Parcelable { return false; return false; } } } } return mId == that.mId && mCapabilities == that.mCapabilities return mCapabilities == that.mCapabilities && mPrimitiveDelayMax == that.mPrimitiveDelayMax && mPrimitiveDelayMax == that.mPrimitiveDelayMax && mCompositionSizeMax == that.mCompositionSizeMax && mCompositionSizeMax == that.mCompositionSizeMax && mPwlePrimitiveDurationMax == that.mPwlePrimitiveDurationMax && mPwlePrimitiveDurationMax == that.mPwlePrimitiveDurationMax Loading Loading @@ -445,7 +455,8 @@ public class VibratorInfo implements Parcelable { return mFrequencyProfile; return mFrequencyProfile; } } protected long getCapabilities() { /** Returns a single int representing all the capabilities of the vibrator. */ public long getCapabilities() { return mCapabilities; return mCapabilities; } } Loading core/java/android/os/vibrator/MultiVibratorInfo.java 0 → 100644 +294 −0 File added.Preview size limit exceeded, changes collapsed. Show changes core/java/android/os/vibrator/VibratorInfoFactory.java 0 → 100644 +52 −0 Original line number Original line Diff line number Diff line /* * Copyright 2023 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.NonNull; import android.os.VibratorInfo; /** * Factory for creating {@link VibratorInfo}s. * * @hide */ public final class VibratorInfoFactory { /** * Creates a single {@link VibratorInfo} that is an intersection of a given collection of * {@link VibratorInfo}s. That is, the capabilities of the returned info will be an * intersection of that of the provided infos. * * @param id the ID for the new {@link VibratorInfo}. * @param vibratorInfos the {@link VibratorInfo}s from which to create a single * {@link VibratorInfo}. * @return a {@link VibratorInfo} that represents the intersection of {@code vibratorInfos}. */ @NonNull public static VibratorInfo create(int id, @NonNull VibratorInfo[] vibratorInfos) { if (vibratorInfos.length == 0) { return new VibratorInfo.Builder(id).build(); } if (vibratorInfos.length == 1) { // Create an equivalent info with the requested ID. return new VibratorInfo(id, vibratorInfos[0]); } // Create a MultiVibratorInfo that intersects all the given infos and has the requested ID. return new MultiVibratorInfo(id, vibratorInfos); } private VibratorInfoFactory() {} } core/tests/vibrator/src/android/os/VibratorInfoTest.java +25 −2 Original line number Original line Diff line number Diff line Loading @@ -257,8 +257,13 @@ public class VibratorInfoTest { @Test @Test public void testEquals() { public void testEquals() { VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID) VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID); .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) // Create a builder with a different ID, but same properties the same as the first one. VibratorInfo.Builder completeBuilder2 = new VibratorInfo.Builder(TEST_VIBRATOR_ID + 2); for (VibratorInfo.Builder builder : new VibratorInfo.Builder[] {completeBuilder, completeBuilder2}) { builder.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) .setSupportedEffects(VibrationEffect.EFFECT_CLICK) .setSupportedEffects(VibrationEffect.EFFECT_CLICK) .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20) .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20) .setPrimitiveDelayMax(100) .setPrimitiveDelayMax(100) Loading @@ -268,31 +273,43 @@ public class VibratorInfoTest { .setPwleSizeMax(20) .setPwleSizeMax(20) .setQFactor(2f) .setQFactor(2f) .setFrequencyProfile(TEST_FREQUENCY_PROFILE); .setFrequencyProfile(TEST_FREQUENCY_PROFILE); } VibratorInfo complete = completeBuilder.build(); VibratorInfo complete = completeBuilder.build(); assertEquals(complete, complete); assertEquals(complete, complete); assertTrue(complete.equalContent(complete)); assertEquals(complete, completeBuilder.build()); assertEquals(complete, completeBuilder.build()); assertTrue(complete.equalContent(completeBuilder.build())); assertEquals(complete.hashCode(), completeBuilder.build().hashCode()); assertEquals(complete.hashCode(), completeBuilder.build().hashCode()); // The infos from the two builders should have equal content, but should not be equal due to // their different IDs. assertNotEquals(complete, completeBuilder2.build()); assertTrue(complete.equalContent(completeBuilder2.build())); VibratorInfo completeWithComposeControl = completeBuilder VibratorInfo completeWithComposeControl = completeBuilder .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) .build(); .build(); assertNotEquals(complete, completeWithComposeControl); assertNotEquals(complete, completeWithComposeControl); assertFalse(complete.equalContent(completeWithComposeControl)); VibratorInfo completeWithNoEffects = completeBuilder VibratorInfo completeWithNoEffects = completeBuilder .setSupportedEffects(new int[0]) .setSupportedEffects(new int[0]) .build(); .build(); assertNotEquals(complete, completeWithNoEffects); assertNotEquals(complete, completeWithNoEffects); assertFalse(complete.equalContent(completeWithNoEffects)); VibratorInfo completeWithUnknownEffects = completeBuilder VibratorInfo completeWithUnknownEffects = completeBuilder .setSupportedEffects(null) .setSupportedEffects(null) .build(); .build(); assertNotEquals(complete, completeWithUnknownEffects); assertNotEquals(complete, completeWithUnknownEffects); assertFalse(complete.equalContent(completeWithUnknownEffects)); VibratorInfo completeWithDifferentPrimitiveDuration = completeBuilder VibratorInfo completeWithDifferentPrimitiveDuration = completeBuilder .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) .build(); .build(); assertNotEquals(complete, completeWithDifferentPrimitiveDuration); assertNotEquals(complete, completeWithDifferentPrimitiveDuration); assertFalse(complete.equalContent(completeWithDifferentPrimitiveDuration)); VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder .setFrequencyProfile(new VibratorInfo.FrequencyProfile( .setFrequencyProfile(new VibratorInfo.FrequencyProfile( Loading @@ -302,31 +319,37 @@ public class VibratorInfoTest { TEST_AMPLITUDE_MAP)) TEST_AMPLITUDE_MAP)) .build(); .build(); assertNotEquals(complete, completeWithDifferentFrequencyProfile); assertNotEquals(complete, completeWithDifferentFrequencyProfile); assertFalse(complete.equalContent(completeWithDifferentFrequencyProfile)); VibratorInfo completeWithEmptyFrequencyProfile = completeBuilder VibratorInfo completeWithEmptyFrequencyProfile = completeBuilder .setFrequencyProfile(EMPTY_FREQUENCY_PROFILE) .setFrequencyProfile(EMPTY_FREQUENCY_PROFILE) .build(); .build(); assertNotEquals(complete, completeWithEmptyFrequencyProfile); assertNotEquals(complete, completeWithEmptyFrequencyProfile); assertFalse(complete.equalContent(completeWithEmptyFrequencyProfile)); VibratorInfo completeWithUnknownQFactor = completeBuilder.setQFactor(Float.NaN).build(); VibratorInfo completeWithUnknownQFactor = completeBuilder.setQFactor(Float.NaN).build(); assertNotEquals(complete, completeWithUnknownQFactor); assertNotEquals(complete, completeWithUnknownQFactor); assertFalse(complete.equalContent(completeWithUnknownQFactor)); VibratorInfo completeWithDifferentQFactor = completeBuilder VibratorInfo completeWithDifferentQFactor = completeBuilder .setQFactor(complete.getQFactor() + 3f) .setQFactor(complete.getQFactor() + 3f) .build(); .build(); assertNotEquals(complete, completeWithDifferentQFactor); assertNotEquals(complete, completeWithDifferentQFactor); assertFalse(complete.equalContent(completeWithDifferentQFactor)); VibratorInfo unknownEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo unknownEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo knownEmptyEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) VibratorInfo knownEmptyEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) .setSupportedEffects(new int[0]) .setSupportedEffects(new int[0]) .build(); .build(); assertNotEquals(unknownEffectSupport, knownEmptyEffectSupport); assertNotEquals(unknownEffectSupport, knownEmptyEffectSupport); assertFalse(unknownEffectSupport.equalContent(knownEmptyEffectSupport)); VibratorInfo unknownBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo unknownBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo knownEmptyBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) VibratorInfo knownEmptyBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) .setSupportedBraking(new int[0]) .setSupportedBraking(new int[0]) .build(); .build(); assertNotEquals(unknownBrakingSupport, knownEmptyBrakingSupport); assertNotEquals(unknownBrakingSupport, knownEmptyBrakingSupport); assertFalse(unknownBrakingSupport.equalContent(knownEmptyBrakingSupport)); } } @Test @Test Loading Loading
core/java/android/os/SystemVibrator.java +3 −305 Original line number Original line Diff line number Diff line Loading @@ -18,26 +18,19 @@ package android.os; import android.annotation.CallbackExecutor; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Context; import android.hardware.vibrator.IVibrator; import android.os.vibrator.VibratorInfoFactory; import android.util.ArrayMap; import android.util.ArrayMap; import android.util.Log; import android.util.Log; import android.util.Range; import android.util.Slog; import android.util.SparseArray; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.ArrayList; import java.util.Arrays; import java.util.Objects; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.Executor; import java.util.function.Function; /** /** * Vibrator implementation that controls the main system vibrator. * Vibrator implementation that controls the main system vibrator. Loading Loading @@ -82,7 +75,7 @@ public class SystemVibrator extends Vibrator { if (vibratorIds.length == 0) { if (vibratorIds.length == 0) { // It is known that the device has no vibrator, so cache and return info that // It is known that the device has no vibrator, so cache and return info that // reflects the lack of support for effects/primitives. // reflects the lack of support for effects/primitives. return mVibratorInfo = new NoVibratorInfo(); return mVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO; } } VibratorInfo[] vibratorInfos = new VibratorInfo[vibratorIds.length]; VibratorInfo[] vibratorInfos = new VibratorInfo[vibratorIds.length]; for (int i = 0; i < vibratorIds.length; i++) { for (int i = 0; i < vibratorIds.length; i++) { Loading @@ -96,12 +89,7 @@ public class SystemVibrator extends Vibrator { } } vibratorInfos[i] = vibrator.getInfo(); vibratorInfos[i] = vibrator.getInfo(); } } if (vibratorInfos.length == 1) { return mVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, vibratorInfos); // 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 @@ -274,296 +262,6 @@ public class SystemVibrator extends Vibrator { } } } } /** * Represents a device with no vibrator as a single {@link VibratorInfo}. * * @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 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) { // Need to use an extra constructor to share the computation in super initialization. this(vibrators, frequencyProfileIntersection(vibrators)); } private MultiVibratorInfo(VibratorInfo[] vibrators, VibratorInfo.FrequencyProfile mergedProfile) { super(/* id= */ -1, capabilitiesIntersection(vibrators, mergedProfile.isEmpty()), 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), mergedProfile); } private static int capabilitiesIntersection(VibratorInfo[] infos, boolean frequencyProfileIsEmpty) { int intersection = ~0; for (VibratorInfo info : infos) { intersection &= info.getCapabilities(); } if (frequencyProfileIsEmpty) { // Revoke frequency control if the merged frequency profile ended up empty. intersection &= ~IVibrator.CAP_FREQUENCY_CONTROL; } return intersection; } @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; } 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; } } intersection.put(brakingId, true); } return intersection; } @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; } } 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; } 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; } } intersection.put(effectId, true); } 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); } } 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. * Listener for all vibrators state change. * * Loading
core/java/android/os/VibratorInfo.java +13 −2 Original line number Original line Diff line number Diff line Loading @@ -156,6 +156,16 @@ public class VibratorInfo implements Parcelable { return false; return false; } } VibratorInfo that = (VibratorInfo) o; VibratorInfo that = (VibratorInfo) o; return mId == that.mId && equalContent(that); } /** * Returns {@code true} only if the properties and capabilities of the provided info, except for * the ID, equals to this info. Returns {@code false} otherwise. * * @hide */ public boolean equalContent(VibratorInfo that) { int supportedPrimitivesCount = mSupportedPrimitives.size(); int supportedPrimitivesCount = mSupportedPrimitives.size(); if (supportedPrimitivesCount != that.mSupportedPrimitives.size()) { if (supportedPrimitivesCount != that.mSupportedPrimitives.size()) { return false; return false; Loading @@ -168,7 +178,7 @@ public class VibratorInfo implements Parcelable { return false; return false; } } } } return mId == that.mId && mCapabilities == that.mCapabilities return mCapabilities == that.mCapabilities && mPrimitiveDelayMax == that.mPrimitiveDelayMax && mPrimitiveDelayMax == that.mPrimitiveDelayMax && mCompositionSizeMax == that.mCompositionSizeMax && mCompositionSizeMax == that.mCompositionSizeMax && mPwlePrimitiveDurationMax == that.mPwlePrimitiveDurationMax && mPwlePrimitiveDurationMax == that.mPwlePrimitiveDurationMax Loading Loading @@ -445,7 +455,8 @@ public class VibratorInfo implements Parcelable { return mFrequencyProfile; return mFrequencyProfile; } } protected long getCapabilities() { /** Returns a single int representing all the capabilities of the vibrator. */ public long getCapabilities() { return mCapabilities; return mCapabilities; } } Loading
core/java/android/os/vibrator/MultiVibratorInfo.java 0 → 100644 +294 −0 File added.Preview size limit exceeded, changes collapsed. Show changes
core/java/android/os/vibrator/VibratorInfoFactory.java 0 → 100644 +52 −0 Original line number Original line Diff line number Diff line /* * Copyright 2023 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.NonNull; import android.os.VibratorInfo; /** * Factory for creating {@link VibratorInfo}s. * * @hide */ public final class VibratorInfoFactory { /** * Creates a single {@link VibratorInfo} that is an intersection of a given collection of * {@link VibratorInfo}s. That is, the capabilities of the returned info will be an * intersection of that of the provided infos. * * @param id the ID for the new {@link VibratorInfo}. * @param vibratorInfos the {@link VibratorInfo}s from which to create a single * {@link VibratorInfo}. * @return a {@link VibratorInfo} that represents the intersection of {@code vibratorInfos}. */ @NonNull public static VibratorInfo create(int id, @NonNull VibratorInfo[] vibratorInfos) { if (vibratorInfos.length == 0) { return new VibratorInfo.Builder(id).build(); } if (vibratorInfos.length == 1) { // Create an equivalent info with the requested ID. return new VibratorInfo(id, vibratorInfos[0]); } // Create a MultiVibratorInfo that intersects all the given infos and has the requested ID. return new MultiVibratorInfo(id, vibratorInfos); } private VibratorInfoFactory() {} }
core/tests/vibrator/src/android/os/VibratorInfoTest.java +25 −2 Original line number Original line Diff line number Diff line Loading @@ -257,8 +257,13 @@ public class VibratorInfoTest { @Test @Test public void testEquals() { public void testEquals() { VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID) VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID); .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) // Create a builder with a different ID, but same properties the same as the first one. VibratorInfo.Builder completeBuilder2 = new VibratorInfo.Builder(TEST_VIBRATOR_ID + 2); for (VibratorInfo.Builder builder : new VibratorInfo.Builder[] {completeBuilder, completeBuilder2}) { builder.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL) .setSupportedEffects(VibrationEffect.EFFECT_CLICK) .setSupportedEffects(VibrationEffect.EFFECT_CLICK) .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20) .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20) .setPrimitiveDelayMax(100) .setPrimitiveDelayMax(100) Loading @@ -268,31 +273,43 @@ public class VibratorInfoTest { .setPwleSizeMax(20) .setPwleSizeMax(20) .setQFactor(2f) .setQFactor(2f) .setFrequencyProfile(TEST_FREQUENCY_PROFILE); .setFrequencyProfile(TEST_FREQUENCY_PROFILE); } VibratorInfo complete = completeBuilder.build(); VibratorInfo complete = completeBuilder.build(); assertEquals(complete, complete); assertEquals(complete, complete); assertTrue(complete.equalContent(complete)); assertEquals(complete, completeBuilder.build()); assertEquals(complete, completeBuilder.build()); assertTrue(complete.equalContent(completeBuilder.build())); assertEquals(complete.hashCode(), completeBuilder.build().hashCode()); assertEquals(complete.hashCode(), completeBuilder.build().hashCode()); // The infos from the two builders should have equal content, but should not be equal due to // their different IDs. assertNotEquals(complete, completeBuilder2.build()); assertTrue(complete.equalContent(completeBuilder2.build())); VibratorInfo completeWithComposeControl = completeBuilder VibratorInfo completeWithComposeControl = completeBuilder .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) .build(); .build(); assertNotEquals(complete, completeWithComposeControl); assertNotEquals(complete, completeWithComposeControl); assertFalse(complete.equalContent(completeWithComposeControl)); VibratorInfo completeWithNoEffects = completeBuilder VibratorInfo completeWithNoEffects = completeBuilder .setSupportedEffects(new int[0]) .setSupportedEffects(new int[0]) .build(); .build(); assertNotEquals(complete, completeWithNoEffects); assertNotEquals(complete, completeWithNoEffects); assertFalse(complete.equalContent(completeWithNoEffects)); VibratorInfo completeWithUnknownEffects = completeBuilder VibratorInfo completeWithUnknownEffects = completeBuilder .setSupportedEffects(null) .setSupportedEffects(null) .build(); .build(); assertNotEquals(complete, completeWithUnknownEffects); assertNotEquals(complete, completeWithUnknownEffects); assertFalse(complete.equalContent(completeWithUnknownEffects)); VibratorInfo completeWithDifferentPrimitiveDuration = completeBuilder VibratorInfo completeWithDifferentPrimitiveDuration = completeBuilder .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10) .build(); .build(); assertNotEquals(complete, completeWithDifferentPrimitiveDuration); assertNotEquals(complete, completeWithDifferentPrimitiveDuration); assertFalse(complete.equalContent(completeWithDifferentPrimitiveDuration)); VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder .setFrequencyProfile(new VibratorInfo.FrequencyProfile( .setFrequencyProfile(new VibratorInfo.FrequencyProfile( Loading @@ -302,31 +319,37 @@ public class VibratorInfoTest { TEST_AMPLITUDE_MAP)) TEST_AMPLITUDE_MAP)) .build(); .build(); assertNotEquals(complete, completeWithDifferentFrequencyProfile); assertNotEquals(complete, completeWithDifferentFrequencyProfile); assertFalse(complete.equalContent(completeWithDifferentFrequencyProfile)); VibratorInfo completeWithEmptyFrequencyProfile = completeBuilder VibratorInfo completeWithEmptyFrequencyProfile = completeBuilder .setFrequencyProfile(EMPTY_FREQUENCY_PROFILE) .setFrequencyProfile(EMPTY_FREQUENCY_PROFILE) .build(); .build(); assertNotEquals(complete, completeWithEmptyFrequencyProfile); assertNotEquals(complete, completeWithEmptyFrequencyProfile); assertFalse(complete.equalContent(completeWithEmptyFrequencyProfile)); VibratorInfo completeWithUnknownQFactor = completeBuilder.setQFactor(Float.NaN).build(); VibratorInfo completeWithUnknownQFactor = completeBuilder.setQFactor(Float.NaN).build(); assertNotEquals(complete, completeWithUnknownQFactor); assertNotEquals(complete, completeWithUnknownQFactor); assertFalse(complete.equalContent(completeWithUnknownQFactor)); VibratorInfo completeWithDifferentQFactor = completeBuilder VibratorInfo completeWithDifferentQFactor = completeBuilder .setQFactor(complete.getQFactor() + 3f) .setQFactor(complete.getQFactor() + 3f) .build(); .build(); assertNotEquals(complete, completeWithDifferentQFactor); assertNotEquals(complete, completeWithDifferentQFactor); assertFalse(complete.equalContent(completeWithDifferentQFactor)); VibratorInfo unknownEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo unknownEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo knownEmptyEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) VibratorInfo knownEmptyEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) .setSupportedEffects(new int[0]) .setSupportedEffects(new int[0]) .build(); .build(); assertNotEquals(unknownEffectSupport, knownEmptyEffectSupport); assertNotEquals(unknownEffectSupport, knownEmptyEffectSupport); assertFalse(unknownEffectSupport.equalContent(knownEmptyEffectSupport)); VibratorInfo unknownBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo unknownBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); VibratorInfo knownEmptyBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) VibratorInfo knownEmptyBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID) .setSupportedBraking(new int[0]) .setSupportedBraking(new int[0]) .build(); .build(); assertNotEquals(unknownBrakingSupport, knownEmptyBrakingSupport); assertNotEquals(unknownBrakingSupport, knownEmptyBrakingSupport); assertFalse(unknownBrakingSupport.equalContent(knownEmptyBrakingSupport)); } } @Test @Test Loading