Merge "Introduce vibrator frequency mapping." into sc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
14278aecfd
@@ -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<Float> 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<String> 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}.
|
||||
*
|
||||
* <p>This mapping is defined by the following parameters:
|
||||
*
|
||||
* <ol>
|
||||
* <li>{@code minFrequency}, {@code resonantFrequency} and {@code frequencyResolution}, in
|
||||
* hertz, provided by the vibrator.
|
||||
* <li>{@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}.
|
||||
* <li>{@code maxFrequency = minFrequency + frequencyResolution * (maxAmplitudes.length-1)}
|
||||
* <li>{@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}.
|
||||
* </ol>
|
||||
*
|
||||
* <p>The mapping is defined linearly by the following points:
|
||||
*
|
||||
* <ol>
|
||||
* <li>{@code toHertz(relativeMinFrequency} = minFrequency
|
||||
* <li>{@code toHertz(-1) = resonantFrequency - safeRange / 2}
|
||||
* <li>{@code toHertz(0) = resonantFrequency}
|
||||
* <li>{@code toHertz(1) = resonantFrequency + safeRange / 2}
|
||||
* <li>{@code toHertz(relativeMaxFrequency) = maxFrequency}
|
||||
* </ol>
|
||||
*
|
||||
* @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<Float> 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<FrequencyMapping> CREATOR =
|
||||
new Creator<FrequencyMapping>() {
|
||||
@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<VibratorInfo> CREATOR =
|
||||
new Creator<VibratorInfo>() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<VibratorInfo> {
|
||||
|
||||
/**
|
||||
* Adapts a sequence of {@link VibrationEffectSegment} to device's absolute frequency values
|
||||
* and respective supported amplitudes.
|
||||
*
|
||||
* <p>This adapter preserves the segment count.
|
||||
*/
|
||||
interface AmplitudeFrequencyAdapter {
|
||||
List<VibrationEffectSegment> apply(List<VibrationEffectSegment> 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<VibrationEffectSegment> 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}.
|
||||
*
|
||||
* <p>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<VibrationEffectSegment> apply(List<VibrationEffectSegment> segments,
|
||||
VibratorInfo info) {
|
||||
List<VibrationEffectSegment> 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<Float> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T> {
|
||||
|
||||
/** Applies the modifier to given {@link VibrationEffect}. */
|
||||
VibrationEffect apply(VibrationEffect effect, T modifier);
|
||||
}
|
||||
@@ -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<VibratorInfo> mDeviceEffectAdapter =
|
||||
new DeviceVibrationEffectAdapter();
|
||||
private final Vibration mVibration;
|
||||
private final VibrationCallbacks mCallbacks;
|
||||
private final SparseArray<VibratorController> mVibrators = new SparseArray<>();
|
||||
@@ -628,6 +632,11 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
|
||||
}
|
||||
|
||||
private long startVibrating(VibrationEffect effect, List<Step> 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()];
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
* <p>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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user