Implement PWLE support

Implement composePwle method and related frequency control getter
introduced to the IVibrator interface.

Bug: 167947076
Test: VibratorControllerTest and VibraionThreadTest
Change-Id: If15f47fae33bce0bcb916e84af0c6712068f3a00
This commit is contained in:
Lais Andrade
2021-03-22 16:13:54 +00:00
parent 834b65d813
commit 68a199f60d
11 changed files with 510 additions and 89 deletions

View File

@@ -19,6 +19,7 @@ package android.os;
import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.hardware.vibrator.Braking;
import android.hardware.vibrator.IVibrator;
import android.util.Log;
import android.util.MathUtils;
@@ -45,6 +46,8 @@ public final class VibratorInfo implements Parcelable {
@Nullable
private final SparseBooleanArray mSupportedEffects;
@Nullable
private final SparseBooleanArray mSupportedBraking;
@Nullable
private final SparseBooleanArray mSupportedPrimitives;
private final float mQFactor;
private final FrequencyMapping mFrequencyMapping;
@@ -53,17 +56,19 @@ public final class VibratorInfo implements Parcelable {
mId = in.readInt();
mCapabilities = in.readLong();
mSupportedEffects = in.readSparseBooleanArray();
mSupportedBraking = in.readSparseBooleanArray();
mSupportedPrimitives = in.readSparseBooleanArray();
mQFactor = in.readFloat();
mFrequencyMapping = in.readParcelable(VibratorInfo.class.getClassLoader());
}
/** @hide */
public VibratorInfo(int id, long capabilities, int[] supportedEffects,
public VibratorInfo(int id, long capabilities, int[] supportedEffects, int[] supportedBraking,
int[] supportedPrimitives, float qFactor, @NonNull FrequencyMapping frequencyMapping) {
mId = id;
mCapabilities = capabilities;
mSupportedEffects = toSparseBooleanArray(supportedEffects);
mSupportedBraking = toSparseBooleanArray(supportedBraking);
mSupportedPrimitives = toSparseBooleanArray(supportedPrimitives);
mQFactor = qFactor;
mFrequencyMapping = frequencyMapping;
@@ -74,6 +79,7 @@ public final class VibratorInfo implements Parcelable {
dest.writeInt(mId);
dest.writeLong(mCapabilities);
dest.writeSparseBooleanArray(mSupportedEffects);
dest.writeSparseBooleanArray(mSupportedBraking);
dest.writeSparseBooleanArray(mSupportedPrimitives);
dest.writeFloat(mQFactor);
dest.writeParcelable(mFrequencyMapping, flags);
@@ -95,6 +101,7 @@ public final class VibratorInfo implements Parcelable {
VibratorInfo that = (VibratorInfo) o;
return mId == that.mId && mCapabilities == that.mCapabilities
&& Objects.equals(mSupportedEffects, that.mSupportedEffects)
&& Objects.equals(mSupportedBraking, that.mSupportedBraking)
&& Objects.equals(mSupportedPrimitives, that.mSupportedPrimitives)
&& Objects.equals(mQFactor, that.mQFactor)
&& Objects.equals(mFrequencyMapping, that.mFrequencyMapping);
@@ -102,8 +109,8 @@ public final class VibratorInfo implements Parcelable {
@Override
public int hashCode() {
return Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedPrimitives,
mQFactor, mFrequencyMapping);
return Objects.hash(mId, mCapabilities, mSupportedEffects, mSupportedBraking,
mSupportedPrimitives, mQFactor, mFrequencyMapping);
}
@Override
@@ -113,6 +120,7 @@ public final class VibratorInfo implements Parcelable {
+ ", mCapabilities=" + Arrays.toString(getCapabilitiesNames())
+ ", mCapabilities flags=" + Long.toBinaryString(mCapabilities)
+ ", mSupportedEffects=" + Arrays.toString(getSupportedEffectsNames())
+ ", mSupportedBraking=" + Arrays.toString(getSupportedBrakingNames())
+ ", mSupportedPrimitives=" + Arrays.toString(getSupportedPrimitivesNames())
+ ", mQFactor=" + mQFactor
+ ", mFrequencyMapping=" + mFrequencyMapping
@@ -133,6 +141,23 @@ public final class VibratorInfo implements Parcelable {
return hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL);
}
/**
* Returns a default value to be applied to composed PWLE effects for braking.
*
* @return a supported braking value, one of android.hardware.vibrator.Braking.*
*/
public int getDefaultBraking() {
if (mSupportedBraking != null) {
int size = mSupportedBraking.size();
for (int i = 0; i < size; i++) {
if (mSupportedBraking.keyAt(i) != Braking.NONE) {
return mSupportedBraking.keyAt(i);
}
}
}
return Braking.NONE;
}
/**
* Query whether the vibrator supports the given effect.
*
@@ -147,7 +172,7 @@ public final class VibratorInfo implements Parcelable {
if (mSupportedEffects == null) {
return Vibrator.VIBRATION_EFFECT_SUPPORT_UNKNOWN;
}
return mSupportedEffects.get(effectId, false) ? Vibrator.VIBRATION_EFFECT_SUPPORT_YES
return mSupportedEffects.get(effectId) ? Vibrator.VIBRATION_EFFECT_SUPPORT_YES
: Vibrator.VIBRATION_EFFECT_SUPPORT_NO;
}
@@ -160,7 +185,7 @@ public final class VibratorInfo implements Parcelable {
public boolean isPrimitiveSupported(
@VibrationEffect.Composition.PrimitiveType int primitiveId) {
return hasCapability(IVibrator.CAP_COMPOSE_EFFECTS) && mSupportedPrimitives != null
&& mSupportedPrimitives.get(primitiveId, false);
&& mSupportedPrimitives.get(primitiveId);
}
/**
@@ -251,12 +276,18 @@ public final class VibratorInfo implements Parcelable {
if (hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
names.add("COMPOSE_EFFECTS");
}
if (hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) {
names.add("COMPOSE_PWLE_EFFECTS");
}
if (hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) {
names.add("ALWAYS_ON_CONTROL");
}
if (hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) {
names.add("AMPLITUDE_CONTROL");
}
if (hasCapability(IVibrator.CAP_FREQUENCY_CONTROL)) {
names.add("FREQUENCY_CONTROL");
}
if (hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) {
names.add("EXTERNAL_CONTROL");
}
@@ -277,6 +308,26 @@ public final class VibratorInfo implements Parcelable {
return names;
}
private String[] getSupportedBrakingNames() {
if (mSupportedBraking == null) {
return new String[0];
}
String[] names = new String[mSupportedBraking.size()];
for (int i = 0; i < mSupportedBraking.size(); i++) {
switch (mSupportedBraking.keyAt(i)) {
case Braking.NONE:
names[i] = "NONE";
break;
case Braking.CLAB:
names[i] = "CLAB";
break;
default:
names[i] = Integer.toString(mSupportedBraking.keyAt(i));
}
}
return names;
}
private String[] getSupportedPrimitivesNames() {
if (mSupportedPrimitives == null) {
return new String[0];
@@ -478,7 +529,8 @@ public final class VibratorInfo implements Parcelable {
@Override
public String toString() {
return "FrequencyMapping{"
+ "mMinFrequency=" + mMinFrequencyHz
+ "mRelativeFrequencyRange=" + mRelativeFrequencyRange
+ ", mMinFrequency=" + mMinFrequencyHz
+ ", mResonantFrequency=" + mResonantFrequencyHz
+ ", mMaxFrequency="
+ (mMinFrequencyHz + mFrequencyResolutionHz * (mMaxAmplitudes.length - 1))

View File

@@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import android.hardware.vibrator.Braking;
import android.hardware.vibrator.IVibrator;
import android.platform.test.annotations.Presubmit;
import android.util.Range;
@@ -97,6 +98,16 @@ public class VibratorInfoTest {
assertFalse(info.isPrimitiveSupported(VibrationEffect.Composition.PRIMITIVE_CLICK));
}
@Test
public void testGetDefaultBraking_returnsFirstSupportedBraking() {
assertEquals(Braking.NONE, new InfoBuilder().build().getDefaultBraking());
assertEquals(Braking.CLAB,
new InfoBuilder()
.setSupportedBraking(Braking.NONE, Braking.CLAB)
.build()
.getDefaultBraking());
}
@Test
public void testGetFrequencyRange_invalidFrequencyMappingReturnsEmptyRange() {
// Invalid, contains NaN values or empty array.
@@ -318,6 +329,7 @@ public class VibratorInfoTest {
private int mId = 0;
private int mCapabilities = 0;
private int[] mSupportedEffects = null;
private int[] mSupportedBraking = null;
private int[] mSupportedPrimitives = null;
private float mQFactor = Float.NaN;
private VibratorInfo.FrequencyMapping mFrequencyMapping = EMPTY_FREQUENCY_MAPPING;
@@ -337,6 +349,11 @@ public class VibratorInfoTest {
return this;
}
public InfoBuilder setSupportedBraking(int... supportedBraking) {
mSupportedBraking = supportedBraking;
return this;
}
public InfoBuilder setSupportedPrimitives(int... supportedPrimitives) {
mSupportedPrimitives = supportedPrimitives;
return this;
@@ -353,8 +370,8 @@ public class VibratorInfoTest {
}
public VibratorInfo build() {
return new VibratorInfo(mId, mCapabilities, mSupportedEffects, mSupportedPrimitives,
mQFactor, mFrequencyMapping);
return new VibratorInfo(mId, mCapabilities, mSupportedEffects, mSupportedBraking,
mSupportedPrimitives, mQFactor, mFrequencyMapping);
}
}
}

View File

@@ -16,6 +16,7 @@
package com.android.server.vibrator;
import android.hardware.vibrator.IVibrator;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
import android.os.vibrator.RampSegment;
@@ -30,25 +31,31 @@ 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);
/** Adapts a sequence of {@link VibrationEffectSegment} to device's capabilities. */
interface SegmentsAdapter {
/**
* Modifies the given segments list by adding/removing segments to it based on the
* device capabilities specified by given {@link VibratorInfo}.
*
* @param segments List of {@link VibrationEffectSegment} to be adapter to the device.
* @param repeatIndex Repeat index on the current segment list.
* @param info The device vibrator info that the segments must be adapted to.
* @return The new repeat index on the modifies list.
*/
int apply(List<VibrationEffectSegment> segments, int repeatIndex, VibratorInfo info);
}
private final AmplitudeFrequencyAdapter mAmplitudeFrequencyAdapter;
private final SegmentsAdapter mAmplitudeFrequencyAdapter;
private final SegmentsAdapter mStepToRampAdapter;
DeviceVibrationEffectAdapter() {
this(new ClippingAmplitudeFrequencyAdapter());
}
DeviceVibrationEffectAdapter(AmplitudeFrequencyAdapter amplitudeFrequencyAdapter) {
DeviceVibrationEffectAdapter(SegmentsAdapter amplitudeFrequencyAdapter) {
mAmplitudeFrequencyAdapter = amplitudeFrequencyAdapter;
mStepToRampAdapter = new StepToRampAdapter();
}
@Override
@@ -58,14 +65,62 @@ final class DeviceVibrationEffectAdapter implements VibrationEffectModifier<Vibr
}
VibrationEffect.Composed composed = (VibrationEffect.Composed) effect;
List<VibrationEffectSegment> mappedSegments = mAmplitudeFrequencyAdapter.apply(
composed.getSegments(), info);
List<VibrationEffectSegment> newSegments = new ArrayList<>(composed.getSegments());
int newRepeatIndex = composed.getRepeatIndex();
// TODO(b/167947076): add ramp to step adapter once PWLE capability is introduced
// Maps steps that should be handled by PWLE to ramps.
// This should be done before frequency is converted from relative to absolute values.
newRepeatIndex = mStepToRampAdapter.apply(newSegments, newRepeatIndex, info);
// Adapt amplitude and frequency values to device supported ones, converting frequency
// to absolute values in Hertz.
newRepeatIndex = mAmplitudeFrequencyAdapter.apply(newSegments, newRepeatIndex, info);
// TODO(b/167947076): add ramp to step adapter
// 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());
return new VibrationEffect.Composed(newSegments, newRepeatIndex);
}
/**
* Adapter that converts step segments that should be handled as PWLEs to ramp segments.
*
* <p>This leves the list unchanged if the device do not have compose PWLE capability.
*/
private static final class StepToRampAdapter implements SegmentsAdapter {
@Override
public int apply(List<VibrationEffectSegment> segments, int repeatIndex,
VibratorInfo info) {
if (!info.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) {
// The vibrator do not have PWLE capability, so keep the segments unchanged.
return repeatIndex;
}
int segmentCount = segments.size();
// Convert steps that require frequency control to ramps.
for (int i = 0; i < segmentCount; i++) {
VibrationEffectSegment segment = segments.get(i);
if ((segment instanceof StepSegment)
&& ((StepSegment) segment).getFrequency() != 0) {
segments.set(i, apply((StepSegment) segment));
}
}
// Convert steps that are next to ramps to also become ramps, so they can be composed
// together in the same PWLE waveform.
for (int i = 1; i < segmentCount; i++) {
if (segments.get(i) instanceof RampSegment) {
for (int j = i - 1; j >= 0 && (segments.get(j) instanceof StepSegment); j--) {
segments.set(j, apply((StepSegment) segments.get(j)));
}
}
}
return repeatIndex;
}
private RampSegment apply(StepSegment segment) {
return new RampSegment(segment.getAmplitude(), segment.getAmplitude(),
segment.getFrequency(), segment.getFrequency(), (int) segment.getDuration());
}
}
/**
@@ -74,25 +129,24 @@ final class DeviceVibrationEffectAdapter implements VibrationEffectModifier<Vibr
*
* <p>Devices with no frequency control will collapse all frequencies to zero and leave
* amplitudes unchanged.
*
* <p>The frequency value returned in segments will be absolute, conveted with
* {@link VibratorInfo#getAbsoluteFrequency(float)}.
*/
private static final class ClippingAmplitudeFrequencyAdapter
implements AmplitudeFrequencyAdapter {
private static final class ClippingAmplitudeFrequencyAdapter implements SegmentsAdapter {
@Override
public List<VibrationEffectSegment> apply(List<VibrationEffectSegment> segments,
public int apply(List<VibrationEffectSegment> segments, int repeatIndex,
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));
segments.set(i, apply((StepSegment) segment, info));
} else if (segment instanceof RampSegment) {
result.add(apply((RampSegment) segment, info));
} else {
result.add(segment);
segments.set(i, apply((RampSegment) segment, info));
}
}
return result;
return repeatIndex;
}
private StepSegment apply(StepSegment segment, VibratorInfo info) {

View File

@@ -280,7 +280,6 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
vibratorOffTimeout);
}
if (segment instanceof RampSegment) {
// TODO(b/167947076): check capabilities to play steps with PWLE once APIs introduced
return new ComposePwleStep(latestStartTime, controller, effect, segmentIndex,
vibratorOffTimeout);
}
@@ -828,14 +827,14 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
}
if (DEBUG) {
Slog.d(TAG, "Compose " + primitives.size() + " primitives on vibrator "
Slog.d(TAG, "Compose " + primitives + " primitives on vibrator "
+ controller.getVibratorInfo().getId());
}
mVibratorOnResult = controller.on(
primitives.toArray(new PrimitiveSegment[primitives.size()]),
mVibration.id);
return nextSteps(/* segmntsPlayed= */ primitives.size());
return nextSteps(/* segmentsPlayed= */ primitives.size());
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
}
@@ -865,11 +864,6 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
VibrationEffectSegment segment = effect.getSegments().get(i);
if (segment instanceof RampSegment) {
pwles.add((RampSegment) segment);
} else if (segment instanceof StepSegment) {
StepSegment stepSegment = (StepSegment) segment;
pwles.add(new RampSegment(stepSegment.getAmplitude(),
stepSegment.getAmplitude(), stepSegment.getFrequency(),
stepSegment.getFrequency(), (int) stepSegment.getDuration()));
} else {
break;
}
@@ -882,7 +876,7 @@ final class VibrationThread extends Thread implements IBinder.DeathRecipient {
}
if (DEBUG) {
Slog.d(TAG, "Compose " + pwles.size() + " PWLEs on vibrator "
Slog.d(TAG, "Compose " + pwles + " PWLEs on vibrator "
+ controller.getVibratorInfo().getId());
}
mVibratorOnResult = controller.on(pwles.toArray(new RampSegment[pwles.size()]),

View File

@@ -66,7 +66,8 @@ final class VibratorController {
NativeWrapper nativeWrapper) {
mNativeWrapper = nativeWrapper;
mNativeWrapper.init(vibratorId, listener);
mVibratorInfo = mNativeWrapper.getInfo();
// TODO(b/167947076): load suggested range from config
mVibratorInfo = mNativeWrapper.getInfo(/* suggestedFrequencyRange= */ 100);
Preconditions.checkNotNull(mVibratorInfo, "Failed to retrieve data for vibrator %d",
vibratorId);
}
@@ -251,8 +252,17 @@ final class VibratorController {
* @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;
if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) {
return 0;
}
synchronized (mLock) {
int braking = mVibratorInfo.getDefaultBraking();
long duration = mNativeWrapper.composePwle(primitives, braking, vibrationId);
if (duration > 0) {
notifyVibratorOnLocked();
}
return duration;
}
}
/** Turns off the vibrator.This will affect the state of {@link #isVibrating()}. */
@@ -337,13 +347,21 @@ final class VibratorController {
private static native void setAmplitude(long nativePtr, float amplitude);
private static native long performEffect(long nativePtr, long effect, long strength,
long vibrationId);
private static native long performComposedEffect(long nativePtr, PrimitiveSegment[] effect,
long vibrationId);
private static native long performPwleEffect(long nativePtr, RampSegment[] effect,
int braking, long vibrationId);
private static native void setExternalControl(long nativePtr, boolean enabled);
private static native void alwaysOnEnable(long nativePtr, long id, long effect,
long strength);
private static native void alwaysOnDisable(long nativePtr, long id);
private static native VibratorInfo getInfo(long nativePtr);
private static native VibratorInfo getInfo(long nativePtr, float suggestedFrequencyRange);
private long mNativePtr = 0;
@@ -385,11 +403,16 @@ final class VibratorController {
return performEffect(mNativePtr, effect, strength, vibrationId);
}
/** Turns vibrator on to perform one of the supported composed effects. */
/** Turns vibrator on to perform effect composed of give primitives effect. */
public long compose(PrimitiveSegment[] primitives, long vibrationId) {
return performComposedEffect(mNativePtr, primitives, vibrationId);
}
/** Turns vibrator on to perform PWLE effect composed of given primitives. */
public long composePwle(RampSegment[] primitives, int braking, long vibrationId) {
return performPwleEffect(mNativePtr, primitives, braking, vibrationId);
}
/** Enabled the device vibrator to be controlled by another service. */
public void setExternalControl(boolean enabled) {
setExternalControl(mNativePtr, enabled);
@@ -406,8 +429,8 @@ final class VibratorController {
}
/** Return device vibrator metadata. */
public VibratorInfo getInfo() {
return getInfo(mNativePtr);
public VibratorInfo getInfo(float suggestedFrequencyRange) {
return getInfo(mNativePtr, suggestedFrequencyRange);
}
}
}

View File

@@ -1540,6 +1540,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
private void addWaveformToComposition(VibrationEffect.Composition composition) {
boolean hasAmplitudes = false;
boolean hasFrequencies = false;
boolean isContinuous = false;
int repeat = -1;
int delay = 0;
@@ -1552,35 +1554,49 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
repeat = Integer.parseInt(getNextArgRequired());
} else if ("-w".equals(nextOption)) {
delay = Integer.parseInt(getNextArgRequired());
} else if ("-f".equals(nextOption)) {
hasFrequencies = true;
} else if ("-c".equals(nextOption)) {
isContinuous = true;
}
}
List<Long> durations = new ArrayList<>();
List<Integer> amplitudes = new ArrayList<>();
VibrationEffect waveform;
List<Integer> durations = new ArrayList<>();
List<Float> amplitudes = new ArrayList<>();
List<Float> frequencies = new ArrayList<>();
float nextAmplitude = 0;
String nextArg;
while ((nextArg = peekNextArg()) != null) {
try {
durations.add(Long.parseLong(nextArg));
durations.add(Integer.parseInt(nextArg));
getNextArgRequired(); // consume the duration
} catch (NumberFormatException e) {
// nextArg is not a duration, finish reading.
break;
}
if (hasAmplitudes) {
amplitudes.add(Integer.parseInt(getNextArgRequired()));
amplitudes.add(
Float.parseFloat(getNextArgRequired()) / VibrationEffect.MAX_AMPLITUDE);
} else {
amplitudes.add(nextAmplitude);
nextAmplitude = 1 - nextAmplitude;
}
if (hasFrequencies) {
frequencies.add(Float.parseFloat(getNextArgRequired()));
} else {
frequencies.add(0f);
}
}
long[] durationArray = durations.stream().mapToLong(Long::longValue).toArray();
if (hasAmplitudes) {
int[] amplitudeArray = amplitudes.stream().mapToInt(Integer::intValue).toArray();
waveform = VibrationEffect.createWaveform(durationArray, amplitudeArray, repeat);
} else {
waveform = VibrationEffect.createWaveform(durationArray, repeat);
VibrationEffect.WaveformBuilder waveform = VibrationEffect.startWaveform();
for (int i = 0; i < durations.size(); i++) {
if (isContinuous) {
waveform.addRamp(amplitudes.get(i), frequencies.get(i), durations.get(i));
} else {
waveform.addStep(amplitudes.get(i), frequencies.get(i), durations.get(i));
}
}
composition.addEffect(waveform, delay);
composition.addEffect(waveform.build(repeat), delay);
}
private void addPrebakedToComposition(VibrationEffect.Composition composition) {
@@ -1659,7 +1675,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
pw.println(" wait time in milliseconds.");
pw.println(" If -a is provided, the command accepts a second argument for ");
pw.println(" amplitude, in a scale of 1-255.");
pw.println(" waveform [-w delay] [-r index] [-a] (<duration> [<amplitude>])...");
pw.print(" waveform [-w delay] [-r index] [-a] [-f] [-c] ");
pw.println("(<duration> [<amplitude>] [<frequency>])...");
pw.println(" Vibrates for durations and amplitudes in list; ignored when ");
pw.println(" device is on DND (Do Not Disturb) mode; touch feedback strength ");
pw.println(" user setting will be used to scale amplitude.");
@@ -1667,9 +1684,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
pw.println(" wait time in milliseconds.");
pw.println(" If -r is provided, the waveform loops back to the specified");
pw.println(" index (e.g. 0 loops from the beginning)");
pw.println(" If -a is provided, the command accepts duration-amplitude pairs;");
pw.println(" otherwise, it accepts durations only and alternates off/on");
pw.println(" Duration is in milliseconds; amplitude is a scale of 1-255.");
pw.println(" If -a is provided, the command expects amplitude to follow each");
pw.println(" duration; otherwise, it accepts durations only and alternates");
pw.println(" off/on");
pw.println(" If -f is provided, the command expects frequency to follow each");
pw.println(" amplitude or duration; otherwise, it uses resonant frequency");
pw.println(" If -c is provided, the waveform is continuous and will ramp");
pw.println(" between values; otherwise each entry is a fixed step.");
pw.println(" Duration is in milliseconds; amplitude is a scale of 1-255;");
pw.println(" frequency is a relative value around resonant frequency 0;");
pw.println(" prebaked [-w delay] [-b] <effect-id>");
pw.println(" Vibrates with prebaked effect; ignored when device is on DND ");
pw.println(" (Do Not Disturb) mode; touch feedback strength user setting ");

View File

@@ -48,6 +48,13 @@ static struct {
jfieldID scale;
jfieldID delay;
} sPrimitiveClassInfo;
static struct {
jfieldID startAmplitude;
jfieldID endAmplitude;
jfieldID startFrequency;
jfieldID endFrequency;
jfieldID duration;
} sRampClassInfo;
static_assert(static_cast<uint8_t>(V1_0::EffectStrength::LIGHT) ==
static_cast<uint8_t>(aidl::EffectStrength::LIGHT));
@@ -127,6 +134,37 @@ private:
const jobject mCallbackListener;
};
static aidl::BrakingPwle brakingPwle(aidl::Braking braking, int32_t duration) {
aidl::BrakingPwle pwle;
pwle.braking = braking;
pwle.duration = duration;
return pwle;
}
static aidl::ActivePwle activePwleFromJavaPrimitive(JNIEnv* env, jobject ramp) {
aidl::ActivePwle pwle;
pwle.startAmplitude =
static_cast<float>(env->GetFloatField(ramp, sRampClassInfo.startAmplitude));
pwle.endAmplitude = static_cast<float>(env->GetFloatField(ramp, sRampClassInfo.endAmplitude));
pwle.startFrequency =
static_cast<float>(env->GetFloatField(ramp, sRampClassInfo.startFrequency));
pwle.endFrequency = static_cast<float>(env->GetFloatField(ramp, sRampClassInfo.endFrequency));
pwle.duration = static_cast<int32_t>(env->GetIntField(ramp, sRampClassInfo.duration));
return pwle;
}
/* Return true if braking is not NONE and the active PWLE starts and ends with zero amplitude. */
static bool shouldBeReplacedWithBraking(aidl::ActivePwle activePwle, aidl::Braking braking) {
return (braking != aidl::Braking::NONE) && (activePwle.startAmplitude == 0) &&
(activePwle.endAmplitude == 0);
}
/* Return true if braking is not NONE and the active PWLE only ends with zero amplitude. */
static bool shouldAddLastBraking(aidl::ActivePwle lastActivePwle, aidl::Braking braking) {
return (braking != aidl::Braking::NONE) && (lastActivePwle.startAmplitude > 0) &&
(lastActivePwle.endAmplitude == 0);
}
static aidl::CompositeEffect effectFromJavaPrimitive(JNIEnv* env, jobject primitive) {
aidl::CompositeEffect effect;
effect.primitive = static_cast<aidl::CompositePrimitive>(
@@ -254,6 +292,40 @@ static jlong vibratorPerformComposedEffect(JNIEnv* env, jclass /* clazz */, jlon
return result.isOk() ? result.value().count() : (result.isUnsupported() ? 0 : -1);
}
static jlong vibratorPerformPwleEffect(JNIEnv* env, jclass /* clazz */, jlong ptr,
jobjectArray waveform, jint brakingId, jlong vibrationId) {
VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
if (wrapper == nullptr) {
ALOGE("vibratorPerformPwleEffect failed because native wrapper was not initialized");
return -1;
}
aidl::Braking braking = static_cast<aidl::Braking>(brakingId);
size_t size = env->GetArrayLength(waveform);
std::vector<aidl::PrimitivePwle> primitives;
std::chrono::milliseconds totalDuration(0);
for (size_t i = 0; i < size; i++) {
jobject element = env->GetObjectArrayElement(waveform, i);
aidl::ActivePwle activePwle = activePwleFromJavaPrimitive(env, element);
if ((i > 0) && shouldBeReplacedWithBraking(activePwle, braking)) {
primitives.push_back(brakingPwle(braking, activePwle.duration));
} else {
primitives.push_back(activePwle);
}
totalDuration += std::chrono::milliseconds(activePwle.duration);
if ((i == (size - 1)) && shouldAddLastBraking(activePwle, braking)) {
primitives.push_back(brakingPwle(braking, 0 /* duration */));
}
}
auto callback = wrapper->createCallback(vibrationId);
auto performPwleEffectFn = [&](std::shared_ptr<vibrator::HalWrapper> hal) {
return hal->performPwleEffect(primitives, callback);
};
auto result = wrapper->halCall<void>(performPwleEffectFn, "performPwleEffect");
return result.isOk() ? totalDuration.count() : (result.isUnsupported() ? 0 : -1);
}
static void vibratorAlwaysOnEnable(JNIEnv* env, jclass /* clazz */, jlong ptr, jlong id,
jlong effect, jlong strength) {
VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
@@ -280,7 +352,8 @@ static void vibratorAlwaysOnDisable(JNIEnv* env, jclass /* clazz */, jlong ptr,
wrapper->halCall<void>(alwaysOnDisableFn, "alwaysOnDisable");
}
static jobject vibratorGetInfo(JNIEnv* env, jclass /* clazz */, jlong ptr) {
static jobject vibratorGetInfo(JNIEnv* env, jclass /* clazz */, jlong ptr,
jfloat suggestedSafeRange) {
VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
if (wrapper == nullptr) {
ALOGE("vibratorGetInfo failed because native wrapper was not initialized");
@@ -290,10 +363,14 @@ static jobject vibratorGetInfo(JNIEnv* env, jclass /* clazz */, jlong ptr) {
jlong capabilities =
static_cast<jlong>(info.capabilities.valueOr(vibrator::Capabilities::NONE));
jfloat minFrequency = static_cast<jfloat>(info.minFrequency.valueOr(NAN));
jfloat resonantFrequency = static_cast<jfloat>(info.resonantFrequency.valueOr(NAN));
jfloat frequencyResolution = static_cast<jfloat>(info.frequencyResolution.valueOr(NAN));
jfloat qFactor = static_cast<jfloat>(info.qFactor.valueOr(NAN));
jintArray supportedEffects = nullptr;
jintArray supportedBraking = nullptr;
jintArray supportedPrimitives = nullptr;
jfloatArray maxAmplitudes = nullptr;
if (info.supportedEffects.isOk()) {
std::vector<aidl::Effect> effects = info.supportedEffects.value();
@@ -301,21 +378,32 @@ static jobject vibratorGetInfo(JNIEnv* env, jclass /* clazz */, jlong ptr) {
env->SetIntArrayRegion(supportedEffects, 0, effects.size(),
reinterpret_cast<jint*>(effects.data()));
}
if (info.supportedBraking.isOk()) {
std::vector<aidl::Braking> braking = info.supportedBraking.value();
supportedBraking = env->NewIntArray(braking.size());
env->SetIntArrayRegion(supportedBraking, 0, braking.size(),
reinterpret_cast<jint*>(braking.data()));
}
if (info.supportedPrimitives.isOk()) {
std::vector<aidl::CompositePrimitive> primitives = info.supportedPrimitives.value();
supportedPrimitives = env->NewIntArray(primitives.size());
env->SetIntArrayRegion(supportedPrimitives, 0, primitives.size(),
reinterpret_cast<jint*>(primitives.data()));
}
if (info.maxAmplitudes.isOk()) {
std::vector<float> amplitudes = info.maxAmplitudes.value();
maxAmplitudes = env->NewFloatArray(amplitudes.size());
env->SetFloatArrayRegion(maxAmplitudes, 0, amplitudes.size(),
reinterpret_cast<jfloat*>(amplitudes.data()));
}
jobject frequencyMapping =
env->NewObject(sFrequencyMappingClass, sFrequencyMappingCtor, NAN /* minFrequencyHz*/,
resonantFrequency, NAN /* frequencyResolutionHz*/,
NAN /* suggestedSafeRangeHz */, nullptr /* maxAmplitudes */);
jobject frequencyMapping = env->NewObject(sFrequencyMappingClass, sFrequencyMappingCtor,
minFrequency, resonantFrequency, frequencyResolution,
suggestedSafeRange, maxAmplitudes);
return env->NewObject(sVibratorInfoClass, sVibratorInfoCtor, wrapper->getVibratorId(),
capabilities, supportedEffects, supportedPrimitives, qFactor,
frequencyMapping);
capabilities, supportedEffects, supportedBraking, supportedPrimitives,
qFactor, frequencyMapping);
}
static const JNINativeMethod method_table[] = {
@@ -330,10 +418,12 @@ static const JNINativeMethod method_table[] = {
{"performEffect", "(JJJJ)J", (void*)vibratorPerformEffect},
{"performComposedEffect", "(J[Landroid/os/vibrator/PrimitiveSegment;J)J",
(void*)vibratorPerformComposedEffect},
{"performPwleEffect", "(J[Landroid/os/vibrator/RampSegment;IJ)J",
(void*)vibratorPerformPwleEffect},
{"setExternalControl", "(JZ)V", (void*)vibratorSetExternalControl},
{"alwaysOnEnable", "(JJJJ)V", (void*)vibratorAlwaysOnEnable},
{"alwaysOnDisable", "(JJ)V", (void*)vibratorAlwaysOnDisable},
{"getInfo", "(J)Landroid/os/VibratorInfo;", (void*)vibratorGetInfo},
{"getInfo", "(JF)Landroid/os/VibratorInfo;", (void*)vibratorGetInfo},
};
int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env) {
@@ -348,6 +438,13 @@ int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env
sPrimitiveClassInfo.scale = GetFieldIDOrDie(env, primitiveClass, "mScale", "F");
sPrimitiveClassInfo.delay = GetFieldIDOrDie(env, primitiveClass, "mDelay", "I");
jclass rampClass = FindClassOrDie(env, "android/os/vibrator/RampSegment");
sRampClassInfo.startAmplitude = GetFieldIDOrDie(env, rampClass, "mStartAmplitude", "F");
sRampClassInfo.endAmplitude = GetFieldIDOrDie(env, rampClass, "mEndAmplitude", "F");
sRampClassInfo.startFrequency = GetFieldIDOrDie(env, rampClass, "mStartFrequency", "F");
sRampClassInfo.endFrequency = GetFieldIDOrDie(env, rampClass, "mEndFrequency", "F");
sRampClassInfo.duration = GetFieldIDOrDie(env, rampClass, "mDuration", "I");
jclass frequencyMappingClass = FindClassOrDie(env, "android/os/VibratorInfo$FrequencyMapping");
sFrequencyMappingClass = (jclass)env->NewGlobalRef(frequencyMappingClass);
sFrequencyMappingCtor = GetMethodIDOrDie(env, sFrequencyMappingClass, "<init>", "(FFFF[F)V");
@@ -355,7 +452,7 @@ int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env
jclass vibratorInfoClass = FindClassOrDie(env, "android/os/VibratorInfo");
sVibratorInfoClass = (jclass)env->NewGlobalRef(vibratorInfoClass);
sVibratorInfoCtor = GetMethodIDOrDie(env, sVibratorInfoClass, "<init>",
"(IJ[I[IFLandroid/os/VibratorInfo$FrequencyMapping;)V");
"(IJ[I[I[IFLandroid/os/VibratorInfo$FrequencyMapping;)V");
return jniRegisterNativeMethods(env,
"com/android/server/vibrator/VibratorController$NativeWrapper",

View File

@@ -18,6 +18,7 @@ package com.android.server.vibrator;
import static org.junit.Assert.assertEquals;
import android.hardware.vibrator.IVibrator;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
import android.os.vibrator.PrebakedSegment;
@@ -30,6 +31,7 @@ import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.stream.IntStream;
/**
* Tests for {@link DeviceVibrationEffectAdapter}.
@@ -74,6 +76,60 @@ public class DeviceVibrationEffectAdapterTest {
assertEquals(effect, mAdapter.apply(effect, createVibratorInfo(TEST_FREQUENCY_MAPPING)));
}
@Test
public void testStepAndRampSegments_withPwleCapability_convertsStepsToRamps() {
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= */ 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 RampSegment(/* startAmplitude= */ 0, /* endAmplitude*/ 0,
/* startFrequency= */ 175, /* endFrequency= */ 175, /* duration= */ 10),
new RampSegment(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.5f,
/* startFrequency= */ 150, /* endFrequency= */ 150, /* 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);
VibratorInfo info = createVibratorInfo(TEST_FREQUENCY_MAPPING,
IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
assertEquals(expected, mAdapter.apply(effect, info));
}
@Test
public void testStepAndRampSegments_withPwleCapabilityAndNoFrequency_keepsOriginalSteps() {
VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 10),
new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 100),
new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 10),
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, /* frequency= */ 150, /* duration= */ 10),
new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 150, /* duration= */ 100),
new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_TICK, 1, 10),
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);
VibratorInfo info = createVibratorInfo(TEST_FREQUENCY_MAPPING,
IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
assertEquals(expected, mAdapter.apply(effect, info));
}
@Test
public void testStepAndRampSegments_emptyMapping_returnsSameAmplitudesAndFrequencyZero() {
VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
@@ -123,8 +179,10 @@ public class DeviceVibrationEffectAdapterTest {
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);
private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyMapping frequencyMapping,
int... capabilities) {
int cap = IntStream.of(capabilities).reduce((a, b) -> a | b).orElse(0);
return new VibratorInfo(/* id= */ 0, cap, null, null, null, /* qFactor= */ Float.NaN,
frequencyMapping);
}
}

View File

@@ -23,6 +23,7 @@ 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.os.vibrator.VibrationEffectSegment;
@@ -43,6 +44,7 @@ final class FakeVibratorControllerProvider {
private final Map<Long, PrebakedSegment> mEnabledAlwaysOnEffects = new HashMap<>();
private final List<VibrationEffectSegment> mEffectSegments = new ArrayList<>();
private final List<Integer> mBraking = new ArrayList<>();
private final List<Float> mAmplitudes = new ArrayList<>();
private final Handler mHandler;
private final FakeNativeWrapper mNativeWrapper;
@@ -52,9 +54,13 @@ final class FakeVibratorControllerProvider {
private int mCapabilities;
private int[] mSupportedEffects;
private int[] mSupportedBraking;
private int[] mSupportedPrimitives;
private float mResonantFrequency;
private float mQFactor;
private float mMinFrequency = Float.NaN;
private float mResonantFrequency = Float.NaN;
private float mFrequencyResolution = Float.NaN;
private float mQFactor = Float.NaN;
private float[] mMaxAmplitudes;
private final class FakeNativeWrapper extends VibratorController.NativeWrapper {
public int vibratorId;
@@ -116,6 +122,19 @@ final class FakeVibratorControllerProvider {
return duration;
}
@Override
public long composePwle(RampSegment[] primitives, int braking, long vibrationId) {
long duration = 0;
for (RampSegment primitive : primitives) {
duration += primitive.getDuration();
mEffectSegments.add(primitive);
}
mBraking.add(braking);
applyLatency();
scheduleListener(duration, vibrationId);
return duration;
}
@Override
public void setExternalControl(boolean enabled) {
}
@@ -132,10 +151,11 @@ final class FakeVibratorControllerProvider {
}
@Override
public VibratorInfo getInfo() {
public VibratorInfo getInfo(float suggestedFrequencyRange) {
VibratorInfo.FrequencyMapping frequencyMapping = new VibratorInfo.FrequencyMapping(
Float.NaN, mResonantFrequency, Float.NaN, Float.NaN, null);
return new VibratorInfo(vibratorId, mCapabilities, mSupportedEffects,
mMinFrequency, mResonantFrequency, mFrequencyResolution,
suggestedFrequencyRange, mMaxAmplitudes);
return new VibratorInfo(vibratorId, mCapabilities, mSupportedEffects, mSupportedBraking,
mSupportedPrimitives, mQFactor, frequencyMapping);
}
@@ -198,6 +218,15 @@ final class FakeVibratorControllerProvider {
mSupportedEffects = effects;
}
/** Set the effects supported by the fake vibrator hardware. */
public void setSupportedBraking(int... braking) {
if (braking != null) {
braking = Arrays.copyOf(braking, braking.length);
Arrays.sort(braking);
}
mSupportedBraking = braking;
}
/** Set the primitives supported by the fake vibrator hardware. */
public void setSupportedPrimitives(int... primitives) {
if (primitives != null) {
@@ -208,8 +237,18 @@ final class FakeVibratorControllerProvider {
}
/** Set the resonant frequency of the fake vibrator hardware. */
public void setResonantFrequency(float resonantFrequency) {
mResonantFrequency = resonantFrequency;
public void setResonantFrequency(float frequencyHz) {
mResonantFrequency = frequencyHz;
}
/** Set the minimum frequency of the fake vibrator hardware. */
public void setMinFrequency(float frequencyHz) {
mMinFrequency = frequencyHz;
}
/** Set the frequency resolution of the fake vibrator hardware. */
public void setFrequencyResolution(float frequencyHz) {
mFrequencyResolution = frequencyHz;
}
/** Set the Q factor of the fake vibrator hardware. */
@@ -217,6 +256,11 @@ final class FakeVibratorControllerProvider {
mQFactor = qFactor;
}
/** Set the max amplitude supported for each frequency f the fake vibrator hardware. */
public void setMaxAmplitudes(float... maxAmplitudes) {
mMaxAmplitudes = maxAmplitudes;
}
/**
* Return the amplitudes set by this controller, including zeroes for each time the vibrator was
* turned off.
@@ -225,6 +269,11 @@ final class FakeVibratorControllerProvider {
return new ArrayList<>(mAmplitudes);
}
/** Return the braking values passed to the compose PWLE method. */
public List<Integer> getBraking() {
return mBraking;
}
/** Return list of {@link VibrationEffectSegment} played by this controller, in order. */
public List<VibrationEffectSegment> getEffectSegments() {
return new ArrayList<>(mEffectSegments);

View File

@@ -31,6 +31,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.hardware.vibrator.Braking;
import android.hardware.vibrator.IVibrator;
import android.hardware.vibrator.IVibratorManager;
import android.os.CombinedVibrationEffect;
@@ -43,6 +44,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.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.platform.test.annotations.LargeTest;
@@ -418,6 +420,42 @@ public class VibrationThreadTest {
assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
}
@Test
public void vibrate_singleVibratorPwle_runsComposePwle() throws Exception {
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
mVibratorProviders.get(VIBRATOR_ID).setSupportedBraking(Braking.CLAB);
mVibratorProviders.get(VIBRATOR_ID).setMinFrequency(100);
mVibratorProviders.get(VIBRATOR_ID).setResonantFrequency(150);
mVibratorProviders.get(VIBRATOR_ID).setFrequencyResolution(50);
mVibratorProviders.get(VIBRATOR_ID).setMaxAmplitudes(
0.5f /* 100Hz*/, 1 /* 150Hz */, 0.6f /* 200Hz */);
long vibrationId = 1;
VibrationEffect effect = VibrationEffect.startWaveform()
.addStep(1, 10)
.addRamp(0, 20)
.addStep(0.8f, 1, 30)
.addRamp(0.6f, -1, 40)
.build();
VibrationThread thread = startThreadAndDispatcher(vibrationId, effect);
waitForCompletion(thread);
verify(mIBatteryStatsMock).noteVibratorOn(eq(UID), eq(100L));
verify(mIBatteryStatsMock).noteVibratorOff(eq(UID));
verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
verify(mThreadCallbacks).onVibrationEnded(eq(vibrationId), eq(Vibration.Status.FINISHED));
assertFalse(thread.getVibrators().get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(
expectedRamp(/* amplitude= */ 1, /* frequency= */ 150, /* duration= */ 10),
expectedRamp(/* StartAmplitude= */ 1, /* endAmplitude= */ 0,
/* startFrequency= */ 150, /* endFrequency= */ 150, /* duration= */ 20),
expectedRamp(/* amplitude= */ 0.6f, /* frequency= */ 200, /* duration= */ 30),
expectedRamp(/* StartAmplitude= */ 0.6f, /* endAmplitude= */ 0.5f,
/* startFrequency= */ 200, /* endFrequency= */ 100, /* duration= */ 40)),
mVibratorProviders.get(VIBRATOR_ID).getEffectSegments());
assertEquals(Arrays.asList(Braking.CLAB), mVibratorProviders.get(VIBRATOR_ID).getBraking());
}
@Test
public void vibrate_singleVibratorCancelled_vibratorStopped() throws Exception {
FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
@@ -969,6 +1007,16 @@ public class VibrationThreadTest {
return new PrimitiveSegment(primitiveId, scale, delay);
}
private VibrationEffectSegment expectedRamp(float amplitude, float frequency, int duration) {
return expectedRamp(amplitude, amplitude, frequency, frequency, duration);
}
private VibrationEffectSegment expectedRamp(float startAmplitude, float endAmplitude,
float startFrequency, float endFrequency, int duration) {
return new RampSegment(startAmplitude, endAmplitude, startFrequency, endFrequency,
duration);
}
private List<Float> expectedAmplitudes(int... amplitudes) {
return Arrays.stream(amplitudes)
.mapToObj(amplitude -> amplitude / 255f)

View File

@@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.notNull;
@@ -34,6 +35,7 @@ import static org.mockito.Mockito.when;
import android.content.ContentResolver;
import android.content.ContextWrapper;
import android.hardware.vibrator.Braking;
import android.hardware.vibrator.IVibrator;
import android.os.IBinder;
import android.os.IVibratorStateListener;
@@ -226,15 +228,19 @@ public class VibratorControllerTest {
}
@Test
public void on_withComposedPwle_ignoresEffect() {
public void on_withComposedPwle_performsEffect() {
mockVibratorCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
when(mNativeWrapperMock.composePwle(any(), anyInt(), anyLong())).thenReturn(15L);
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());
assertEquals(15L, controller.on(primitives, 12));
assertTrue(controller.isVibrating());
verify(mNativeWrapperMock).composePwle(eq(primitives), eq(Braking.NONE), eq(12L));
}
@Test
@@ -291,8 +297,8 @@ public class VibratorControllerTest {
private void mockVibratorCapabilities(int capabilities) {
VibratorInfo.FrequencyMapping frequencyMapping = new VibratorInfo.FrequencyMapping(
Float.NaN, Float.NaN, Float.NaN, Float.NaN, null);
when(mNativeWrapperMock.getInfo()).thenReturn(
new VibratorInfo(VIBRATOR_ID, capabilities, null, null, Float.NaN,
when(mNativeWrapperMock.getInfo(/* suggestedFrequencyRange= */ 100)).thenReturn(
new VibratorInfo(VIBRATOR_ID, capabilities, null, null, null, Float.NaN,
frequencyMapping));
}