Introduce vibrator frequency mapping.

Introduce frequency data to VibratorInfo and a transformation for
VibrationEffect that maps relative frequency and amplitude values to the
device absolute values.

The transformation also clips amplitude values to the device bandwidth
map provided.

Pending implementation of chaining composed waveforms and sending PWLE
primitives to the HAL.

Bug: 167947076
Test: DeviceVibrationEffectAdapterTest
Change-Id: Ie7505a1c0f50bb7d907b0752c02987f258316874
This commit is contained in:
Lais Andrade
2021-03-15 20:43:11 +00:00
parent 601a944e45
commit e574d3bbe8
8 changed files with 774 additions and 29 deletions

View File

@@ -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>() {

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}

View File

@@ -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()];

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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();