diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java index 9c46bc9eb1497..64e51e71962a3 100644 --- a/core/java/android/os/VibratorInfo.java +++ b/core/java/android/os/VibratorInfo.java @@ -16,9 +16,13 @@ package android.os; +import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.vibrator.IVibrator; +import android.util.Log; +import android.util.MathUtils; +import android.util.Range; import android.util.SparseBooleanArray; import java.util.ArrayList; @@ -42,27 +46,27 @@ public final class VibratorInfo implements Parcelable { private final SparseBooleanArray mSupportedEffects; @Nullable private final SparseBooleanArray mSupportedPrimitives; - private final float mResonantFrequency; private final float mQFactor; + private final FrequencyMapping mFrequencyMapping; VibratorInfo(Parcel in) { mId = in.readInt(); mCapabilities = in.readLong(); mSupportedEffects = in.readSparseBooleanArray(); mSupportedPrimitives = in.readSparseBooleanArray(); - mResonantFrequency = in.readFloat(); mQFactor = in.readFloat(); + mFrequencyMapping = in.readParcelable(VibratorInfo.class.getClassLoader()); } /** @hide */ public VibratorInfo(int id, long capabilities, int[] supportedEffects, - int[] supportedPrimitives, float resonantFrequency, float qFactor) { + int[] supportedPrimitives, float qFactor, @NonNull FrequencyMapping frequencyMapping) { mId = id; mCapabilities = capabilities; mSupportedEffects = toSparseBooleanArray(supportedEffects); mSupportedPrimitives = toSparseBooleanArray(supportedPrimitives); - mResonantFrequency = resonantFrequency; mQFactor = qFactor; + mFrequencyMapping = frequencyMapping; } @Override @@ -71,8 +75,8 @@ public final class VibratorInfo implements Parcelable { dest.writeLong(mCapabilities); dest.writeSparseBooleanArray(mSupportedEffects); dest.writeSparseBooleanArray(mSupportedPrimitives); - dest.writeFloat(mResonantFrequency); dest.writeFloat(mQFactor); + dest.writeParcelable(mFrequencyMapping, flags); } @Override @@ -92,14 +96,14 @@ public final class VibratorInfo implements Parcelable { return mId == that.mId && mCapabilities == that.mCapabilities && Objects.equals(mSupportedEffects, that.mSupportedEffects) && Objects.equals(mSupportedPrimitives, that.mSupportedPrimitives) - && Objects.equals(mResonantFrequency, that.mResonantFrequency) - && Objects.equals(mQFactor, that.mQFactor); + && Objects.equals(mQFactor, that.mQFactor) + && Objects.equals(mFrequencyMapping, that.mFrequencyMapping); } @Override public int hashCode() { return Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedPrimitives, - mResonantFrequency, mQFactor); + mQFactor, mFrequencyMapping); } @Override @@ -110,8 +114,8 @@ public final class VibratorInfo implements Parcelable { + ", mCapabilities flags=" + Long.toBinaryString(mCapabilities) + ", mSupportedEffects=" + Arrays.toString(getSupportedEffectsNames()) + ", mSupportedPrimitives=" + Arrays.toString(getSupportedPrimitivesNames()) - + ", mResonantFrequency=" + mResonantFrequency + ", mQFactor=" + mQFactor + + ", mFrequencyMapping=" + mFrequencyMapping + '}'; } @@ -177,7 +181,7 @@ public final class VibratorInfo implements Parcelable { * this vibrator is a composite of multiple physical devices. */ public float getResonantFrequency() { - return mResonantFrequency; + return mFrequencyMapping.mResonantFrequencyHz; } /** @@ -190,6 +194,52 @@ public final class VibratorInfo implements Parcelable { return mQFactor; } + /** + * Return a range of relative frequency values supported by the vibrator. + * + * @return A range of relative frequency values supported. The range will always contain the + * value 0, representing the device resonant frequency. Devices without frequency control will + * return the range [0,0]. Devices with frequency control will always return a range containing + * the safe range [-1, 1]. + * @hide + */ + public Range getFrequencyRange() { + return mFrequencyMapping.mRelativeFrequencyRange; + } + + /** + * Return the maximum amplitude the vibrator can play at given relative frequency. + * + * @return a value in [0,1] representing the maximum amplitude the device can play at given + * relative frequency. Devices without frequency control will return 1 for the input zero + * (resonant frequency), and 0 to any other input. Devices with frequency control will return + * the supported value, for input in {@code #getFrequencyRange()}, and 0 for any other input. + * @hide + */ + @FloatRange(from = 0, to = 1) + public float getMaxAmplitude(float relativeFrequency) { + if (mFrequencyMapping.isEmpty()) { + // The vibrator has not provided values for frequency mapping. + // Return the expected behavior for devices without frequency control. + return Float.compare(relativeFrequency, 0) == 0 ? 1 : 0; + } + return mFrequencyMapping.getMaxAmplitude(relativeFrequency); + } + + /** + * Return absolute frequency value for this vibrator, in hertz, that corresponds to given + * relative frequency. + * + * @retur a value in hertz that corresponds to given relative frequency. Input values outside + * {@link #getFrequencyRange()} will return {@link Float#NaN}. Devices without frequency control + * will return {@link Float#NaN} for any input. + * @hide + */ + @FloatRange(from = 0) + public float getAbsoluteFrequency(float relativeFrequency) { + return mFrequencyMapping.toHertz(relativeFrequency); + } + private String[] getCapabilitiesNames() { List names = new ArrayList<>(); if (hasCapability(IVibrator.CAP_ON_CALLBACK)) { @@ -250,6 +300,209 @@ public final class VibratorInfo implements Parcelable { return array; } + /** + * Describes how frequency should be mapped to absolute values for a specific {@link Vibrator}. + * + *

This mapping is defined by the following parameters: + * + *

    + *
  1. {@code minFrequency}, {@code resonantFrequency} and {@code frequencyResolution}, in + * hertz, provided by the vibrator. + *
  2. {@code maxAmplitudes} a list of values in [0,1] provided by the vibrator, where + * {@code maxAmplitudes[i]} represents max supported amplitude at frequency + * {@code minFrequency + frequencyResolution * i}. + *
  3. {@code maxFrequency = minFrequency + frequencyResolution * (maxAmplitudes.length-1)} + *
  4. {@code suggestedSafeRangeHz} is the suggested frequency range in hertz that should be + * mapped to relative values -1 and 1, where 0 maps to {@code resonantFrequency}. + *
+ * + *

The mapping is defined linearly by the following points: + * + *

    + *
  1. {@code toHertz(relativeMinFrequency} = minFrequency + *
  2. {@code toHertz(-1) = resonantFrequency - safeRange / 2} + *
  3. {@code toHertz(0) = resonantFrequency} + *
  4. {@code toHertz(1) = resonantFrequency + safeRange / 2} + *
  5. {@code toHertz(relativeMaxFrequency) = maxFrequency} + *
+ * + * @hide + */ + public static final class FrequencyMapping implements Parcelable { + private final float mMinFrequencyHz; + private final float mResonantFrequencyHz; + private final float mFrequencyResolutionHz; + private final float mSuggestedSafeRangeHz; + private final float[] mMaxAmplitudes; + + // Relative fields calculated from input values: + private final Range mRelativeFrequencyRange; + + FrequencyMapping(Parcel in) { + this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), + in.createFloatArray()); + } + + /** @hide */ + public FrequencyMapping(float minFrequencyHz, float resonantFrequencyHz, + float frequencyResolutionHz, float suggestedSafeRangeHz, float[] maxAmplitudes) { + mMinFrequencyHz = minFrequencyHz; + mResonantFrequencyHz = resonantFrequencyHz; + mFrequencyResolutionHz = frequencyResolutionHz; + mSuggestedSafeRangeHz = suggestedSafeRangeHz; + mMaxAmplitudes = new float[maxAmplitudes == null ? 0 : maxAmplitudes.length]; + if (maxAmplitudes != null) { + System.arraycopy(maxAmplitudes, 0, mMaxAmplitudes, 0, maxAmplitudes.length); + } + + float maxFrequencyHz = + minFrequencyHz + frequencyResolutionHz * (mMaxAmplitudes.length - 1); + if (Float.isNaN(resonantFrequencyHz) || Float.isNaN(minFrequencyHz) + || Float.isNaN(frequencyResolutionHz) || Float.isNaN(suggestedSafeRangeHz) + || resonantFrequencyHz < minFrequencyHz + || resonantFrequencyHz > maxFrequencyHz) { + // Some required fields are undefined or have bad values. + // Leave this mapping empty. + mRelativeFrequencyRange = Range.create(0f, 0f); + return; + } + + // Calculate actual safe range, limiting the suggested one by the device supported range + float safeDelta = MathUtils.min( + suggestedSafeRangeHz / 2, + resonantFrequencyHz - minFrequencyHz, + maxFrequencyHz - resonantFrequencyHz); + mRelativeFrequencyRange = Range.create( + (minFrequencyHz - resonantFrequencyHz) / safeDelta, + (maxFrequencyHz - resonantFrequencyHz) / safeDelta); + } + + /** + * Returns true if this frequency mapping is empty, i.e. the only supported relative + * frequency is 0 (resonant frequency). + */ + public boolean isEmpty() { + return Float.compare(mRelativeFrequencyRange.getLower(), + mRelativeFrequencyRange.getUpper()) == 0; + } + + /** + * Returns the frequency value in hertz that is mapped to the given relative frequency. + * + * @return The mapped frequency, in hertz, or {@link Float#NaN} is value outside the device + * supported range. + */ + public float toHertz(float relativeFrequency) { + if (!mRelativeFrequencyRange.contains(relativeFrequency)) { + return Float.NaN; + } + float relativeMinFrequency = mRelativeFrequencyRange.getLower(); + if (Float.compare(relativeMinFrequency, 0) == 0) { + // relative supported range is [0,0], so toHertz(0) should be the resonant frequency + return mResonantFrequencyHz; + } + float shift = (mMinFrequencyHz - mResonantFrequencyHz) / relativeMinFrequency; + return mResonantFrequencyHz + relativeFrequency * shift; + } + + /** + * Returns the maximum amplitude the vibrator can reach while playing at given relative + * frequency. + * + * @return A value in [0,1] representing the max amplitude supported at given relative + * frequency. This will return 0 if frequency is outside supported range, or if max + * amplitude mapping is empty. + */ + public float getMaxAmplitude(float relativeFrequency) { + float frequencyHz = toHertz(relativeFrequency); + if (Float.isNaN(frequencyHz)) { + // Unsupported frequency requested, vibrator cannot play at this frequency. + return 0; + } + float position = (frequencyHz - mMinFrequencyHz) / mFrequencyResolutionHz; + int floorIndex = (int) Math.floor(position); + int ceilIndex = (int) Math.ceil(position); + if (floorIndex < 0 || floorIndex >= mMaxAmplitudes.length) { + if (mMaxAmplitudes.length > 0) { + // This should never happen if the setup of relative frequencies was correct. + Log.w(TAG, "Max amplitudes has " + mMaxAmplitudes.length + + " entries and was expected to cover the frequency " + frequencyHz + + " Hz when starting at min frequency of " + mMinFrequencyHz + + " Hz with resolution of " + mFrequencyResolutionHz + " Hz."); + } + return 0; + } + if (floorIndex != ceilIndex && ceilIndex < mMaxAmplitudes.length) { + // Value in between two mapped frequency values, use the lowest supported one. + return MathUtils.min(mMaxAmplitudes[floorIndex], mMaxAmplitudes[ceilIndex]); + } + return mMaxAmplitudes[floorIndex]; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(mMinFrequencyHz); + dest.writeFloat(mResonantFrequencyHz); + dest.writeFloat(mFrequencyResolutionHz); + dest.writeFloat(mSuggestedSafeRangeHz); + dest.writeFloatArray(mMaxAmplitudes); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FrequencyMapping)) { + return false; + } + FrequencyMapping that = (FrequencyMapping) o; + return Float.compare(mMinFrequencyHz, that.mMinFrequencyHz) == 0 + && Float.compare(mResonantFrequencyHz, that.mResonantFrequencyHz) == 0 + && Float.compare(mFrequencyResolutionHz, that.mFrequencyResolutionHz) == 0 + && Float.compare(mSuggestedSafeRangeHz, that.mSuggestedSafeRangeHz) == 0 + && Arrays.equals(mMaxAmplitudes, that.mMaxAmplitudes); + } + + @Override + public int hashCode() { + return Objects.hash(mMinFrequencyHz, mFrequencyResolutionHz, mFrequencyResolutionHz, + mSuggestedSafeRangeHz, mMaxAmplitudes); + } + + @Override + public String toString() { + return "FrequencyMapping{" + + "mMinFrequency=" + mMinFrequencyHz + + ", mResonantFrequency=" + mResonantFrequencyHz + + ", mMaxFrequency=" + + (mMinFrequencyHz + mFrequencyResolutionHz * (mMaxAmplitudes.length - 1)) + + ", mFrequencyResolution=" + mFrequencyResolutionHz + + ", mSuggestedSafeRange=" + mSuggestedSafeRangeHz + + ", mMaxAmplitudes count=" + mMaxAmplitudes.length + + '}'; + } + + @NonNull + public static final Creator CREATOR = + new Creator() { + @Override + public FrequencyMapping createFromParcel(Parcel in) { + return new FrequencyMapping(in); + } + + @Override + public FrequencyMapping[] newArray(int size) { + return new FrequencyMapping[size]; + } + }; + } + @NonNull public static final Creator CREATOR = new Creator() { diff --git a/core/tests/coretests/src/android/os/VibratorInfoTest.java b/core/tests/coretests/src/android/os/VibratorInfoTest.java index 09c36dd261bdf..40fc00a971222 100644 --- a/core/tests/coretests/src/android/os/VibratorInfoTest.java +++ b/core/tests/coretests/src/android/os/VibratorInfoTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertTrue; import android.hardware.vibrator.IVibrator; import android.platform.test.annotations.Presubmit; +import android.util.Range; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,6 +32,20 @@ import org.junit.runners.JUnit4; @Presubmit @RunWith(JUnit4.class) public class VibratorInfoTest { + private static final float TEST_TOLERANCE = 1e-5f; + + private static final float TEST_MIN_FREQUENCY = 50; + private static final float TEST_RESONANT_FREQUENCY = 150; + private static final float TEST_FREQUENCY_RESOLUTION = 25; + private static final float[] TEST_AMPLITUDE_MAP = new float[]{ + /* 50Hz= */ 0.1f, 0.2f, 0.4f, 0.8f, /* 150Hz= */ 1f, 0.9f, /* 200Hz= */ 0.8f}; + + private static final VibratorInfo.FrequencyMapping EMPTY_FREQUENCY_MAPPING = + new VibratorInfo.FrequencyMapping(Float.NaN, Float.NaN, Float.NaN, Float.NaN, null); + private static final VibratorInfo.FrequencyMapping TEST_FREQUENCY_MAPPING = + new VibratorInfo.FrequencyMapping(TEST_MIN_FREQUENCY, + TEST_RESONANT_FREQUENCY, TEST_FREQUENCY_RESOLUTION, + /* suggestedSafeRangeHz= */ 50, TEST_AMPLITUDE_MAP); @Test public void testHasAmplitudeControl() { @@ -82,6 +97,139 @@ public class VibratorInfoTest { assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK)); } + @Test + public void testGetFrequencyRange_invalidFrequencyMappingReturnsEmptyRange() { + // Invalid, contains NaN values or empty array. + assertEquals(Range.create(0f, 0f), new InfoBuilder().build().getFrequencyRange()); + assertEquals(Range.create(0f, 0f), new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping( + Float.NaN, 150, 25, 50, TEST_AMPLITUDE_MAP)) + .build().getFrequencyRange()); + assertEquals(Range.create(0f, 0f), new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping( + 50, Float.NaN, 25, 50, TEST_AMPLITUDE_MAP)) + .build().getFrequencyRange()); + assertEquals(Range.create(0f, 0f), new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping( + 50, 150, Float.NaN, 50, TEST_AMPLITUDE_MAP)) + .build().getFrequencyRange()); + assertEquals(Range.create(0f, 0f), new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping( + 50, 150, 25, Float.NaN, TEST_AMPLITUDE_MAP)) + .build().getFrequencyRange()); + assertEquals(Range.create(0f, 0f), new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping(50, 150, 25, 50, null)) + .build().getFrequencyRange()); + // Invalid, minFrequency > resonantFrequency + assertEquals(Range.create(0f, 0f), new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping( + /* minFrequencyHz= */ 250, /* resonantFrequency= */ 150, 25, 50, null)) + .build().getFrequencyRange()); + // Invalid, maxFrequency < resonantFrequency by changing resolution. + assertEquals(Range.create(0f, 0f), new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping( + 50, 150, /* frequencyResolutionHz= */10, 50, null)) + .build().getFrequencyRange()); + } + + @Test + public void testGetFrequencyRange_safeRangeLimitedByMaxFrequency() { + VibratorInfo info = new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping( + /* minFrequencyHz= */ 50, /* resonantFrequencyHz= */ 150, + /* frequencyResolutionHz= */ 25, /* suggestedSafeRangeHz= */ 200, + TEST_AMPLITUDE_MAP)) + .build(); + + // Mapping should range from 50Hz = -2 to 200Hz = 1 + // Safe range [-1, 1] = [100Hz, 200Hz] defined by max - resonant = 50Hz + assertEquals(Range.create(-2f, 1f), info.getFrequencyRange()); + } + + @Test + public void testGetFrequencyRange_safeRangeLimitedByMinFrequency() { + VibratorInfo info = new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping( + /* minFrequencyHz= */ 50, /* resonantFrequencyHz= */ 150, + /* frequencyResolutionHz= */ 50, /* suggestedSafeRangeHz= */ 200, + TEST_AMPLITUDE_MAP)) + .build(); + + // Mapping should range from 50Hz = -1 to 350Hz = 2 + // Safe range [-1, 1] = [50Hz, 250Hz] defined by resonant - min = 100Hz + assertEquals(Range.create(-1f, 2f), info.getFrequencyRange()); + } + + @Test + public void testGetFrequencyRange_validMappingReturnsFullRelativeRange() { + VibratorInfo info = new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping( + /* minFrequencyHz= */ 50, /* resonantFrequencyHz= */ 150, + /* frequencyResolutionHz= */ 50, /* suggestedSafeRangeHz= */ 100, + TEST_AMPLITUDE_MAP)) + .build(); + + // Mapping should range from 50Hz = -2 to 350Hz = 4 + // Safe range [-1, 1] = [100Hz, 200Hz] defined by suggested safe range 100Hz + assertEquals(Range.create(-2f, 4f), info.getFrequencyRange()); + } + + @Test + public void testAbsoluteFrequency_emptyMappingReturnsNaN() { + VibratorInfo info = new InfoBuilder().build(); + assertTrue(Float.isNaN(info.getAbsoluteFrequency(-1))); + assertTrue(Float.isNaN(info.getAbsoluteFrequency(0))); + assertTrue(Float.isNaN(info.getAbsoluteFrequency(1))); + } + + @Test + public void testAbsoluteFrequency_validRangeReturnsOriginalValue() { + VibratorInfo info = new InfoBuilder().setFrequencyMapping(TEST_FREQUENCY_MAPPING).build(); + assertEquals(TEST_RESONANT_FREQUENCY, info.getAbsoluteFrequency(0), TEST_TOLERANCE); + + // Safe range [-1, 1] = [125Hz, 175Hz] defined by suggested safe range 100Hz + assertEquals(125, info.getAbsoluteFrequency(-1), TEST_TOLERANCE); + assertEquals(175, info.getAbsoluteFrequency(1), TEST_TOLERANCE); + assertEquals(155, info.getAbsoluteFrequency(0.2f), TEST_TOLERANCE); + assertEquals(140, info.getAbsoluteFrequency(-0.4f), TEST_TOLERANCE); + + // Full range [-4, 2] = [50Hz, 200Hz] defined by min frequency and amplitude mapping size + assertEquals(50, info.getAbsoluteFrequency(info.getFrequencyRange().getLower()), + TEST_TOLERANCE); + assertEquals(200, info.getAbsoluteFrequency(info.getFrequencyRange().getUpper()), + TEST_TOLERANCE); + } + + @Test + public void testGetMaxAmplitude_emptyMappingReturnsOnlyResonantFrequency() { + VibratorInfo info = new InfoBuilder().build(); + assertEquals(1f, info.getMaxAmplitude(0), TEST_TOLERANCE); + assertEquals(0f, info.getMaxAmplitude(0.1f), TEST_TOLERANCE); + assertEquals(0f, info.getMaxAmplitude(-1), TEST_TOLERANCE); + } + + @Test + public void testGetMaxAmplitude_validMappingReturnsMappedValues() { + VibratorInfo info = new InfoBuilder() + .setFrequencyMapping(new VibratorInfo.FrequencyMapping(/* minFrequencyHz= */ 50, + /* resonantFrequencyHz= */ 150, /* frequencyResolutionHz= */ 25, + /* suggestedSafeRangeHz= */ 50, TEST_AMPLITUDE_MAP)) + .build(); + + assertEquals(1f, info.getMaxAmplitude(0), TEST_TOLERANCE); // 150Hz + assertEquals(0.9f, info.getMaxAmplitude(1), TEST_TOLERANCE); // 175Hz + assertEquals(0.8f, info.getMaxAmplitude(-1), TEST_TOLERANCE); // 125Hz + assertEquals(0.8f, info.getMaxAmplitude(info.getFrequencyRange().getUpper()), + TEST_TOLERANCE); // 200Hz + assertEquals(0.1f, info.getMaxAmplitude(info.getFrequencyRange().getLower()), + TEST_TOLERANCE); // 50Hz + + // Rounds 145Hz to the max amplitude for 125Hz, which is lower. + assertEquals(0.8f, info.getMaxAmplitude(-0.1f), TEST_TOLERANCE); // 145Hz + // Rounds 185Hz to the max amplitude for 200Hz, which is lower. + assertEquals(0.8f, info.getMaxAmplitude(1.2f), TEST_TOLERANCE); // 185Hz + } + @Test public void testEquals() { InfoBuilder completeBuilder = new InfoBuilder() @@ -90,7 +238,7 @@ public class VibratorInfoTest { .setSupportedEffects(VibrationEffect.EFFECT_CLICK) .setSupportedPrimitives(VibrationEffect.Composition.PRIMITIVE_CLICK) .setQFactor(2f) - .setResonantFrequency(150f); + .setFrequencyMapping(TEST_FREQUENCY_MAPPING); VibratorInfo complete = completeBuilder.build(); assertEquals(complete, complete); @@ -110,22 +258,24 @@ public class VibratorInfoTest { VibratorInfo completeWithUnknownEffects = completeBuilder .setSupportedEffects(null) .build(); - assertNotEquals(complete, completeWithNoEffects); + assertNotEquals(complete, completeWithUnknownEffects); VibratorInfo completeWithUnknownPrimitives = completeBuilder .setSupportedPrimitives(null) .build(); assertNotEquals(complete, completeWithUnknownPrimitives); - VibratorInfo completeWithDifferentF0 = completeBuilder - .setResonantFrequency(complete.getResonantFrequency() + 3f) + VibratorInfo completeWithDifferentFrequencyMapping = completeBuilder + .setFrequencyMapping(new VibratorInfo.FrequencyMapping(TEST_MIN_FREQUENCY + 10, + TEST_RESONANT_FREQUENCY + 20, TEST_FREQUENCY_RESOLUTION + 5, + /* suggestedSafeRangeHz= */ 100, TEST_AMPLITUDE_MAP)) .build(); - assertNotEquals(complete, completeWithDifferentF0); + assertNotEquals(complete, completeWithDifferentFrequencyMapping); - VibratorInfo completeWithUnknownF0 = completeBuilder - .setResonantFrequency(Float.NaN) + VibratorInfo completeWithEmptyFrequencyMapping = completeBuilder + .setFrequencyMapping(EMPTY_FREQUENCY_MAPPING) .build(); - assertNotEquals(complete, completeWithUnknownF0); + assertNotEquals(complete, completeWithEmptyFrequencyMapping); VibratorInfo completeWithUnknownQFactor = completeBuilder .setQFactor(Float.NaN) @@ -153,8 +303,8 @@ public class VibratorInfoTest { .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS) .setSupportedEffects(VibrationEffect.EFFECT_CLICK) .setSupportedPrimitives(null) - .setResonantFrequency(1.3f) .setQFactor(Float.NaN) + .setFrequencyMapping(TEST_FREQUENCY_MAPPING) .build(); Parcel parcel = Parcel.obtain(); @@ -169,8 +319,8 @@ public class VibratorInfoTest { private int mCapabilities = 0; private int[] mSupportedEffects = null; private int[] mSupportedPrimitives = null; - private float mResonantFrequency = Float.NaN; private float mQFactor = Float.NaN; + private VibratorInfo.FrequencyMapping mFrequencyMapping = EMPTY_FREQUENCY_MAPPING; public InfoBuilder setId(int id) { mId = id; @@ -192,19 +342,19 @@ public class VibratorInfoTest { return this; } - public InfoBuilder setResonantFrequency(float resonantFrequency) { - mResonantFrequency = resonantFrequency; - return this; - } - public InfoBuilder setQFactor(float qFactor) { mQFactor = qFactor; return this; } + public InfoBuilder setFrequencyMapping(VibratorInfo.FrequencyMapping frequencyMapping) { + mFrequencyMapping = frequencyMapping; + return this; + } + public VibratorInfo build() { return new VibratorInfo(mId, mCapabilities, mSupportedEffects, mSupportedPrimitives, - mResonantFrequency, mQFactor); + mQFactor, mFrequencyMapping); } } } diff --git a/services/core/java/com/android/server/vibrator/DeviceVibrationEffectAdapter.java b/services/core/java/com/android/server/vibrator/DeviceVibrationEffectAdapter.java new file mode 100644 index 0000000000000..953837a566982 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/DeviceVibrationEffectAdapter.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2021 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 com.android.server.vibrator; + +import android.os.VibrationEffect; +import android.os.VibratorInfo; +import android.os.vibrator.RampSegment; +import android.os.vibrator.StepSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.util.MathUtils; +import android.util.Range; + +import java.util.ArrayList; +import java.util.List; + +/** Adapts a {@link VibrationEffect} to a specific device, taking into account its capabilities. */ +final class DeviceVibrationEffectAdapter implements VibrationEffectModifier { + + /** + * Adapts a sequence of {@link VibrationEffectSegment} to device's absolute frequency values + * and respective supported amplitudes. + * + *

This adapter preserves the segment count. + */ + interface AmplitudeFrequencyAdapter { + List apply(List segments, + VibratorInfo info); + } + + private final AmplitudeFrequencyAdapter mAmplitudeFrequencyAdapter; + + DeviceVibrationEffectAdapter() { + this(new ClippingAmplitudeFrequencyAdapter()); + } + + DeviceVibrationEffectAdapter(AmplitudeFrequencyAdapter amplitudeFrequencyAdapter) { + mAmplitudeFrequencyAdapter = amplitudeFrequencyAdapter; + } + + @Override + public VibrationEffect apply(VibrationEffect effect, VibratorInfo info) { + if (!(effect instanceof VibrationEffect.Composed)) { + return effect; + } + + VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; + List mappedSegments = mAmplitudeFrequencyAdapter.apply( + composed.getSegments(), info); + + // TODO(b/167947076): add ramp to step adapter once PWLE capability is introduced + // TODO(b/167947076): add filter that removes unsupported primitives + // TODO(b/167947076): add filter that replaces unsupported prebaked with fallback + + return new VibrationEffect.Composed(mappedSegments, composed.getRepeatIndex()); + } + + /** + * Adapter that clips frequency values to {@link VibratorInfo#getFrequencyRange()} and + * amplitude values to respective {@link VibratorInfo#getMaxAmplitude}. + * + *

Devices with no frequency control will collapse all frequencies to zero and leave + * amplitudes unchanged. + */ + private static final class ClippingAmplitudeFrequencyAdapter + implements AmplitudeFrequencyAdapter { + @Override + public List apply(List segments, + VibratorInfo info) { + List result = new ArrayList<>(); + int segmentCount = segments.size(); + for (int i = 0; i < segmentCount; i++) { + VibrationEffectSegment segment = segments.get(i); + if (segment instanceof StepSegment) { + result.add(apply((StepSegment) segment, info)); + } else if (segment instanceof RampSegment) { + result.add(apply((RampSegment) segment, info)); + } else { + result.add(segment); + } + } + return result; + } + + private StepSegment apply(StepSegment segment, VibratorInfo info) { + float clampedFrequency = info.getFrequencyRange().clamp(segment.getFrequency()); + return new StepSegment( + MathUtils.min(segment.getAmplitude(), info.getMaxAmplitude(clampedFrequency)), + info.getAbsoluteFrequency(clampedFrequency), + (int) segment.getDuration()); + } + + private RampSegment apply(RampSegment segment, VibratorInfo info) { + Range frequencyRange = info.getFrequencyRange(); + float clampedStartFrequency = frequencyRange.clamp(segment.getStartFrequency()); + float clampedEndFrequency = frequencyRange.clamp(segment.getEndFrequency()); + return new RampSegment( + MathUtils.min(segment.getStartAmplitude(), + info.getMaxAmplitude(clampedStartFrequency)), + MathUtils.min(segment.getEndAmplitude(), + info.getMaxAmplitude(clampedEndFrequency)), + info.getAbsoluteFrequency(clampedStartFrequency), + info.getAbsoluteFrequency(clampedEndFrequency), + (int) segment.getDuration()); + } + } +} diff --git a/services/core/java/com/android/server/vibrator/VibrationEffectModifier.java b/services/core/java/com/android/server/vibrator/VibrationEffectModifier.java new file mode 100644 index 0000000000000..d287c8faa34d1 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/VibrationEffectModifier.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 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 com.android.server.vibrator; + +import android.os.VibrationEffect; + +/** Function that applies a generic modifier to a {@link VibrationEffect}. */ +interface VibrationEffectModifier { + + /** Applies the modifier to given {@link VibrationEffect}. */ + VibrationEffect apply(VibrationEffect effect, T modifier); +} diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index 18063de9f0835..3090e6d8c6224 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -27,9 +27,11 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; import android.os.VibrationEffect; +import android.os.VibratorInfo; import android.os.WorkSource; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; import android.util.Slog; @@ -91,6 +93,8 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { private final WorkSource mWorkSource = new WorkSource(); private final PowerManager.WakeLock mWakeLock; private final IBatteryStats mBatteryStatsService; + private final VibrationEffectModifier mDeviceEffectAdapter = + new DeviceVibrationEffectAdapter(); private final Vibration mVibration; private final VibrationCallbacks mCallbacks; private final SparseArray mVibrators = new SparseArray<>(); @@ -628,6 +632,11 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { } private long startVibrating(VibrationEffect effect, List nextSteps) { + // TODO(b/167947076): split this into 4 different step implementations: + // VibratorPerformStep, VibratorComposePrimitiveStep, VibratorComposePwleStep and + // VibratorAmplitudeStep. + // Make sure each step carries over the full VibrationEffect and an incremental segment + // index, and triggers a final VibratorOffStep once all segments are done. VibrationEffect.Composed composed = (VibrationEffect.Composed) effect; VibrationEffectSegment firstSegment = composed.getSegments().get(0); final long duration; @@ -672,6 +681,28 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { nextSteps.add(new VibratorOffStep(now + duration + CALLBACKS_EXTRA_TIMEOUT, controller)); } + } else if (firstSegment instanceof RampSegment) { + int segmentCount = composed.getSegments().size(); + RampSegment[] primitives = new RampSegment[segmentCount]; + for (int i = 0; i < segmentCount; i++) { + VibrationEffectSegment segment = composed.getSegments().get(i); + if (segment instanceof RampSegment) { + primitives[i] = (RampSegment) segment; + } else if (segment instanceof StepSegment) { + StepSegment stepSegment = (StepSegment) segment; + primitives[i] = new RampSegment( + stepSegment.getAmplitude(), stepSegment.getAmplitude(), + stepSegment.getFrequency(), stepSegment.getFrequency(), + (int) stepSegment.getDuration()); + } else { + primitives[i] = new RampSegment(0, 0, 0, 0, 0); + } + } + duration = controller.on(primitives, mVibration.id); + if (duration > 0) { + nextSteps.add(new VibratorOffStep(now + duration + CALLBACKS_EXTRA_TIMEOUT, + controller)); + } } else { duration = 0; } @@ -851,7 +882,9 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { mVibratorIds = new int[mVibrators.size()]; for (int i = 0; i < mVibrators.size(); i++) { int vibratorId = mVibrators.keyAt(i); - mVibratorEffects.put(vibratorId, mono.getEffect()); + VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo(); + VibrationEffect effect = mDeviceEffectAdapter.apply(mono.getEffect(), vibratorInfo); + mVibratorEffects.put(vibratorId, effect); mVibratorIds[i] = vibratorId; } mRequiredSyncCapabilities = calculateRequiredSyncCapabilities(mVibratorEffects); @@ -863,7 +896,10 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient { for (int i = 0; i < stereoEffects.size(); i++) { int vibratorId = stereoEffects.keyAt(i); if (mVibrators.contains(vibratorId)) { - mVibratorEffects.put(vibratorId, stereoEffects.valueAt(i)); + VibratorInfo vibratorInfo = mVibrators.valueAt(i).getVibratorInfo(); + VibrationEffect effect = mDeviceEffectAdapter.apply( + stereoEffects.valueAt(i), vibratorInfo); + mVibratorEffects.put(vibratorId, effect); } } mVibratorIds = new int[mVibratorEffects.size()]; diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index 66200b32ac0c2..c8977025b846e 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -25,6 +25,7 @@ import android.os.RemoteException; import android.os.VibratorInfo; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -65,9 +66,12 @@ final class VibratorController { mNativeWrapper = nativeWrapper; mNativeWrapper.init(vibratorId, listener); + // TODO(b/167947076): load supported ones from HAL once API introduced + VibratorInfo.FrequencyMapping frequencyMapping = new VibratorInfo.FrequencyMapping( + Float.NaN, nativeWrapper.getResonantFrequency(), Float.NaN, Float.NaN, null); mVibratorInfo = new VibratorInfo(vibratorId, nativeWrapper.getCapabilities(), nativeWrapper.getSupportedEffects(), nativeWrapper.getSupportedPrimitives(), - nativeWrapper.getResonantFrequency(), nativeWrapper.getQFactor()); + nativeWrapper.getQFactor(), frequencyMapping); } /** Register state listener for this vibrator. */ @@ -233,6 +237,19 @@ final class VibratorController { } } + /** + * Plays a composition of pwle primitives, using {@code vibrationId} or completion callback + * to {@link OnVibrationCompleteListener}. + * + *

This will affect the state of {@link #isVibrating()}. + * + * @return The duration of the effect playing, or 0 if unsupported. + */ + public long on(RampSegment[] primitives, long vibrationId) { + // TODO(b/167947076): forward to the HAL once APIs are introduced + return 0; + } + /** Turns off the vibrator.This will affect the state of {@link #isVibrating()}. */ public void off() { synchronized (mLock) { diff --git a/services/tests/servicestests/src/com/android/server/vibrator/DeviceVibrationEffectAdapterTest.java b/services/tests/servicestests/src/com/android/server/vibrator/DeviceVibrationEffectAdapterTest.java new file mode 100644 index 0000000000000..dcff4791a907a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/vibrator/DeviceVibrationEffectAdapterTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2020 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 com.android.server.vibrator; + +import static org.junit.Assert.assertEquals; + +import android.os.VibrationEffect; +import android.os.VibratorInfo; +import android.os.vibrator.PrebakedSegment; +import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; +import android.os.vibrator.StepSegment; +import android.platform.test.annotations.Presubmit; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; + +/** + * Tests for {@link DeviceVibrationEffectAdapter}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:DeviceVibrationEffectAdapterTest + */ +@Presubmit +public class DeviceVibrationEffectAdapterTest { + private static final float TEST_MIN_FREQUENCY = 50; + private static final float TEST_RESONANT_FREQUENCY = 150; + private static final float TEST_FREQUENCY_RESOLUTION = 25; + private static final float[] TEST_AMPLITUDE_MAP = new float[]{ + /* 50Hz= */ 0.1f, 0.2f, 0.4f, 0.8f, /* 150Hz= */ 1f, 0.9f, /* 200Hz= */ 0.8f}; + + private static final VibratorInfo.FrequencyMapping EMPTY_FREQUENCY_MAPPING = + new VibratorInfo.FrequencyMapping(Float.NaN, Float.NaN, Float.NaN, Float.NaN, null); + private static final VibratorInfo.FrequencyMapping TEST_FREQUENCY_MAPPING = + new VibratorInfo.FrequencyMapping(TEST_MIN_FREQUENCY, + TEST_RESONANT_FREQUENCY, TEST_FREQUENCY_RESOLUTION, + /* suggestedSafeRangeHz= */ 50, TEST_AMPLITUDE_MAP); + + private DeviceVibrationEffectAdapter mAdapter; + + @Before + public void setUp() throws Exception { + mAdapter = new DeviceVibrationEffectAdapter(); + } + + @Test + public void testPrebakedAndPrimitiveSegments_returnsOriginalSegment() { + VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList( + new PrebakedSegment( + VibrationEffect.EFFECT_CLICK, false, VibrationEffect.EFFECT_STRENGTH_LIGHT), + new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 10), + new PrebakedSegment( + VibrationEffect.EFFECT_THUD, true, VibrationEffect.EFFECT_STRENGTH_STRONG), + new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.5f, 100)), + /* repeatIndex= */ -1); + + assertEquals(effect, mAdapter.apply(effect, createVibratorInfo(EMPTY_FREQUENCY_MAPPING))); + assertEquals(effect, mAdapter.apply(effect, createVibratorInfo(TEST_FREQUENCY_MAPPING))); + } + + @Test + public void testStepAndRampSegments_emptyMapping_returnsSameAmplitudesAndFrequencyZero() { + VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList( + new StepSegment(/* amplitude= */ 0, /* frequency= */ 1, /* duration= */ 10), + new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 100), + new RampSegment(/* startAmplitude= */ 0.8f, /* endAmplitude= */ 1, + /* startFrequency= */ -1, /* endFrequency= */ 1, /* duration= */ 50), + new RampSegment(/* startAmplitude= */ 0.7f, /* endAmplitude= */ 0.5f, + /* startFrequency= */ 10, /* endFrequency= */ -5, /* duration= */ 20)), + /* repeatIndex= */ 2); + + VibrationEffect.Composed expected = new VibrationEffect.Composed(Arrays.asList( + new StepSegment(/* amplitude= */ 0, /* frequency= */ Float.NaN, /* duration= */ 10), + new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ Float.NaN, + /* duration= */ 100), + new RampSegment(/* startAmplitude= */ 0.8f, /* endAmplitude= */ 1, + /* startFrequency= */ Float.NaN, /* endFrequency= */ Float.NaN, + /* duration= */ 50), + new RampSegment(/* startAmplitude= */ 0.7f, /* endAmplitude= */ 0.5f, + /* startFrequency= */ Float.NaN, /* endFrequency= */ Float.NaN, + /* duration= */ 20)), + /* repeatIndex= */ 2); + + assertEquals(expected, mAdapter.apply(effect, createVibratorInfo(EMPTY_FREQUENCY_MAPPING))); + } + + @Test + public void testStepAndRampSegments_nonEmptyMapping_returnsClippedValues() { + VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList( + new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 10), + new StepSegment(/* amplitude= */ 1, /* frequency= */ -1, /* duration= */ 100), + new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 1, + /* startFrequency= */ -4, /* endFrequency= */ 2, /* duration= */ 50), + new RampSegment(/* startAmplitude= */ 0.8f, /* endAmplitude= */ 0.2f, + /* startFrequency= */ 10, /* endFrequency= */ -5, /* duration= */ 20)), + /* repeatIndex= */ 2); + + VibrationEffect.Composed expected = new VibrationEffect.Composed(Arrays.asList( + new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 150, /* duration= */ 10), + new StepSegment(/* amplitude= */ 0.8f, /* frequency= */ 125, /* duration= */ 100), + new RampSegment(/* startAmplitude= */ 0.1f, /* endAmplitude= */ 0.8f, + /* startFrequency= */ 50, /* endFrequency= */ 200, /* duration= */ 50), + new RampSegment(/* startAmplitude= */ 0.8f, /* endAmplitude= */ 0.1f, + /* startFrequency= */ 200, /* endFrequency= */ 50, /* duration= */ 20)), + /* repeatIndex= */ 2); + + assertEquals(expected, mAdapter.apply(effect, createVibratorInfo(TEST_FREQUENCY_MAPPING))); + } + + private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyMapping frequencyMapping) { + return new VibratorInfo(/* id= */ 0, /* capabilities= */ 0, null, null, + /* qFactor= */ Float.NaN, frequencyMapping); + } +} diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java index 0ba3a21b96ea6..70ea219911b7e 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorControllerTest.java @@ -41,6 +41,7 @@ import android.os.VibrationEffect; import android.os.test.TestLooper; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; +import android.os.vibrator.RampSegment; import android.platform.test.annotations.Presubmit; import androidx.test.InstrumentationRegistry; @@ -226,6 +227,18 @@ public class VibratorControllerTest { verify(mNativeWrapperMock).compose(eq(primitives), eq(12L)); } + @Test + public void on_withComposedPwle_ignoresEffect() { + VibratorController controller = createController(); + + RampSegment[] primitives = new RampSegment[]{ + new RampSegment(/* startAmplitude= */ 0, /* endAmplitude= */ 1, + /* startFrequency= */ -1, /* endFrequency= */ 1, /* duration= */ 10) + }; + assertEquals(0L, controller.on(primitives, 12)); + assertFalse(controller.isVibrating()); + } + @Test public void off_turnsOffVibrator() { VibratorController controller = createController();