Merge "Introduce vibrator frequency mapping." into sc-dev

This commit is contained in:
TreeHugger Robot
2021-03-23 19:11:07 +00:00
committed by Android (Google) Code Review
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();