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:
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()]),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user