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

Commit 915513c9 authored by Yeabkal Wubshit's avatar Yeabkal Wubshit
Browse files

Extract VibratorInfo aggregation logic to a factory

The aggregation logic used to live in SystemVibrator.
To allow the server side code to do VibratorInfo aggregation, we're
extracting this logic into a factory class in android.os that can be
used by both the client and server side codes. SystemVibrator now
uses the new class to combine its VibratorInfos. The factory will
later be used within VibratorManagerService.

Unit tests for the aggregation logic used to live in VibratorTest.
They have now been moved to the factory class's test, along with more
tests checking the factory method (ID assignment, handling of empty
vibrator info list, and handling of a size-1 list).

Bug: 296358077
Test: atest VibratorInfoFactoryTest
Test: atest VibratorTest
Test: atest VibratorInfoTest
Change-Id: I27a8245e3e9f322ccd046b7d748141ec05293b99
parent 32a09a1a
Loading
Loading
Loading
Loading
+3 −305
Original line number Diff line number Diff line
@@ -18,26 +18,19 @@ 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.hardware.vibrator.IVibrator;
import android.os.vibrator.VibratorInfoFactory;
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.
@@ -82,7 +75,7 @@ public class SystemVibrator extends Vibrator {
            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();
                return mVibratorInfo = VibratorInfo.EMPTY_VIBRATOR_INFO;
            }
            VibratorInfo[] vibratorInfos = new VibratorInfo[vibratorIds.length];
            for (int i = 0; i < vibratorIds.length; i++) {
@@ -96,12 +89,7 @@ public class SystemVibrator extends Vibrator {
                }
                vibratorInfos[i] = vibrator.getInfo();
            }
            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);
            return mVibratorInfo = VibratorInfoFactory.create(/* id= */ -1, vibratorInfos);
        }
    }

@@ -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.
     *
+13 −2
Original line number Diff line number Diff line
@@ -156,6 +156,16 @@ public class VibratorInfo implements Parcelable {
            return false;
        }
        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();
        if (supportedPrimitivesCount != that.mSupportedPrimitives.size()) {
            return false;
@@ -168,7 +178,7 @@ public class VibratorInfo implements Parcelable {
                return false;
            }
        }
        return mId == that.mId && mCapabilities == that.mCapabilities
        return mCapabilities == that.mCapabilities
                && mPrimitiveDelayMax == that.mPrimitiveDelayMax
                && mCompositionSizeMax == that.mCompositionSizeMax
                && mPwlePrimitiveDurationMax == that.mPwlePrimitiveDurationMax
@@ -445,7 +455,8 @@ public class VibratorInfo implements Parcelable {
        return mFrequencyProfile;
    }

    protected long getCapabilities() {
    /** Returns a single int representing all the capabilities of the vibrator. */
    public long getCapabilities() {
        return mCapabilities;
    }

+294 −0

File added.

Preview size limit exceeded, changes collapsed.

+52 −0
Original line number 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() {}
}
+25 −2
Original line number Diff line number Diff line
@@ -257,8 +257,13 @@ public class VibratorInfoTest {

    @Test
    public void testEquals() {
        VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
                .setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL)
        VibratorInfo.Builder completeBuilder = new VibratorInfo.Builder(TEST_VIBRATOR_ID);
        // 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)
                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 20)
                .setPrimitiveDelayMax(100)
@@ -268,31 +273,43 @@ public class VibratorInfoTest {
                .setPwleSizeMax(20)
                .setQFactor(2f)
                .setFrequencyProfile(TEST_FREQUENCY_PROFILE);
        }
        VibratorInfo complete = completeBuilder.build();

        assertEquals(complete, complete);
        assertTrue(complete.equalContent(complete));
        assertEquals(complete, completeBuilder.build());
        assertTrue(complete.equalContent(completeBuilder.build()));
        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
                .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS)
                .build();
        assertNotEquals(complete, completeWithComposeControl);
        assertFalse(complete.equalContent(completeWithComposeControl));

        VibratorInfo completeWithNoEffects = completeBuilder
                .setSupportedEffects(new int[0])
                .build();
        assertNotEquals(complete, completeWithNoEffects);
        assertFalse(complete.equalContent(completeWithNoEffects));

        VibratorInfo completeWithUnknownEffects = completeBuilder
                .setSupportedEffects(null)
                .build();
        assertNotEquals(complete, completeWithUnknownEffects);
        assertFalse(complete.equalContent(completeWithUnknownEffects));

        VibratorInfo completeWithDifferentPrimitiveDuration = completeBuilder
                .setSupportedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 10)
                .build();
        assertNotEquals(complete, completeWithDifferentPrimitiveDuration);
        assertFalse(complete.equalContent(completeWithDifferentPrimitiveDuration));

        VibratorInfo completeWithDifferentFrequencyProfile = completeBuilder
                .setFrequencyProfile(new VibratorInfo.FrequencyProfile(
@@ -302,31 +319,37 @@ public class VibratorInfoTest {
                        TEST_AMPLITUDE_MAP))
                .build();
        assertNotEquals(complete, completeWithDifferentFrequencyProfile);
        assertFalse(complete.equalContent(completeWithDifferentFrequencyProfile));

        VibratorInfo completeWithEmptyFrequencyProfile = completeBuilder
                .setFrequencyProfile(EMPTY_FREQUENCY_PROFILE)
                .build();
        assertNotEquals(complete, completeWithEmptyFrequencyProfile);
        assertFalse(complete.equalContent(completeWithEmptyFrequencyProfile));

        VibratorInfo completeWithUnknownQFactor = completeBuilder.setQFactor(Float.NaN).build();
        assertNotEquals(complete, completeWithUnknownQFactor);
        assertFalse(complete.equalContent(completeWithUnknownQFactor));

        VibratorInfo completeWithDifferentQFactor = completeBuilder
                .setQFactor(complete.getQFactor() + 3f)
                .build();
        assertNotEquals(complete, completeWithDifferentQFactor);
        assertFalse(complete.equalContent(completeWithDifferentQFactor));

        VibratorInfo unknownEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build();
        VibratorInfo knownEmptyEffectSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
                .setSupportedEffects(new int[0])
                .build();
        assertNotEquals(unknownEffectSupport, knownEmptyEffectSupport);
        assertFalse(unknownEffectSupport.equalContent(knownEmptyEffectSupport));

        VibratorInfo unknownBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build();
        VibratorInfo knownEmptyBrakingSupport = new VibratorInfo.Builder(TEST_VIBRATOR_ID)
                .setSupportedBraking(new int[0])
                .build();
        assertNotEquals(unknownBrakingSupport, knownEmptyBrakingSupport);
        assertFalse(unknownBrakingSupport.equalContent(knownEmptyBrakingSupport));
    }

    @Test
Loading