diff --git a/core/api/current.txt b/core/api/current.txt index ead304f979b80..0316875332237 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -31928,6 +31928,9 @@ package android.os { method public static android.os.VibrationEffect createWaveform(long[], int[], int); method public int describeContents(); method @NonNull public static android.os.VibrationEffect.Composition startComposition(); + method @NonNull public static android.os.VibrationEffect.WaveformBuilder startWaveform(); + method @NonNull public static android.os.VibrationEffect.WaveformBuilder startWaveform(@NonNull android.os.VibrationEffect.VibrationParameter); + method @NonNull public static android.os.VibrationEffect.WaveformBuilder startWaveform(@NonNull android.os.VibrationEffect.VibrationParameter, @NonNull android.os.VibrationEffect.VibrationParameter); field @NonNull public static final android.os.Parcelable.Creator CREATOR; field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff field public static final int EFFECT_CLICK = 0; // 0x0 @@ -31937,10 +31940,13 @@ package android.os { } public static final class VibrationEffect.Composition { + method @NonNull public android.os.VibrationEffect.Composition addEffect(@NonNull android.os.VibrationEffect); + method @NonNull public android.os.VibrationEffect.Composition addOffDuration(@NonNull java.time.Duration); method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int); method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int, @FloatRange(from=0.0f, to=1.0f) float); method @NonNull public android.os.VibrationEffect.Composition addPrimitive(int, @FloatRange(from=0.0f, to=1.0f) float, @IntRange(from=0) int); method @NonNull public android.os.VibrationEffect compose(); + method @NonNull public android.os.VibrationEffect.Composition repeatEffectIndefinitely(@NonNull android.os.VibrationEffect); field public static final int PRIMITIVE_CLICK = 1; // 0x1 field public static final int PRIMITIVE_LOW_TICK = 8; // 0x8 field public static final int PRIMITIVE_QUICK_FALL = 6; // 0x6 @@ -31951,6 +31957,21 @@ package android.os { field public static final int PRIMITIVE_TICK = 7; // 0x7 } + public static final class VibrationEffect.Composition.UnreachableAfterRepeatingIndefinitelyException extends java.lang.IllegalStateException { + } + + public static class VibrationEffect.VibrationParameter { + method @NonNull public static android.os.VibrationEffect.VibrationParameter targetAmplitude(@FloatRange(from=0, to=1) float); + method @NonNull public static android.os.VibrationEffect.VibrationParameter targetFrequency(@FloatRange(from=1) float); + } + + public static final class VibrationEffect.WaveformBuilder { + method @NonNull public android.os.VibrationEffect.WaveformBuilder addSustain(@NonNull java.time.Duration); + method @NonNull public android.os.VibrationEffect.WaveformBuilder addTransition(@NonNull java.time.Duration, @NonNull android.os.VibrationEffect.VibrationParameter); + method @NonNull public android.os.VibrationEffect.WaveformBuilder addTransition(@NonNull java.time.Duration, @NonNull android.os.VibrationEffect.VibrationParameter, @NonNull android.os.VibrationEffect.VibrationParameter); + method @NonNull public android.os.VibrationEffect build(); + } + public abstract class Vibrator { method public final int areAllEffectsSupported(@NonNull int...); method public final boolean areAllPrimitivesSupported(@NonNull int...); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 8190880c07009..169346b973eb6 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1793,7 +1793,6 @@ package android.os { method public static android.os.VibrationEffect get(int, boolean); method @Nullable public static android.os.VibrationEffect get(android.net.Uri, android.content.Context); method public abstract long getDuration(); - method @NonNull public static android.os.VibrationEffect.WaveformBuilder startWaveform(); field public static final int EFFECT_POP = 4; // 0x4 field public static final int EFFECT_STRENGTH_LIGHT = 0; // 0x0 field public static final int EFFECT_STRENGTH_MEDIUM = 1; // 0x1 @@ -1811,20 +1810,6 @@ package android.os { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } - public static final class VibrationEffect.Composition { - method @NonNull public android.os.VibrationEffect.Composition addEffect(@NonNull android.os.VibrationEffect); - method @NonNull public android.os.VibrationEffect.Composition addEffect(@NonNull android.os.VibrationEffect, @IntRange(from=0) int); - } - - public static final class VibrationEffect.WaveformBuilder { - method @NonNull public android.os.VibrationEffect.WaveformBuilder addRamp(@FloatRange(from=0.0f, to=1.0f) float, @IntRange(from=0) int); - method @NonNull public android.os.VibrationEffect.WaveformBuilder addRamp(@FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=1.0f) float, @IntRange(from=0) int); - method @NonNull public android.os.VibrationEffect.WaveformBuilder addStep(@FloatRange(from=0.0f, to=1.0f) float, @IntRange(from=0) int); - method @NonNull public android.os.VibrationEffect.WaveformBuilder addStep(@FloatRange(from=0.0f, to=1.0f) float, @FloatRange(from=1.0f) float, @IntRange(from=0) int); - method @NonNull public android.os.VibrationEffect build(); - method @NonNull public android.os.VibrationEffect build(int); - } - public abstract class Vibrator { method public int getDefaultVibrationIntensity(int); field public static final int VIBRATION_INTENSITY_HIGH = 3; // 0x3 diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index ae37a714e0c8f..f490587ae1e1e 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -21,7 +21,6 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SuppressLint; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ContentResolver; @@ -40,6 +39,7 @@ import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -196,20 +196,20 @@ public abstract class VibrationEffect implements Parcelable { /** * Create a waveform vibration. * - * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For + *

Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For * each pair, the value in the amplitude array determines the strength of the vibration and the * value in the timing array determines how long it vibrates for. An amplitude of 0 implies no * vibration (i.e. off), and any pairs with a timing value of 0 will be ignored. - *

- * The amplitude array of the generated waveform will be the same size as the given + * + *

The amplitude array of the generated waveform will be the same size as the given * timing array with alternating values of 0 (i.e. off) and {@link #DEFAULT_AMPLITUDE}, * starting with 0. Therefore the first timing value will be the period to wait before turning * the vibrator on, the second value will be how long to vibrate at {@link #DEFAULT_AMPLITUDE} * strength, etc. - *

- * To cause the pattern to repeat, pass the index into the timings array at which to start the - * repetition, or -1 to disable repeating. - *

+ * + *

To cause the pattern to repeat, pass the index into the timings array at which to start + * the repetition, or -1 to disable repeating. Repeating effects will be played indefinitely + * and should be cancelled via {@link Vibrator#cancel()}. * * @param timings The pattern of alternating on-off timings, starting with off. Timing values * of 0 will cause the timing / amplitude pair to be ignored. @@ -229,15 +229,15 @@ public abstract class VibrationEffect implements Parcelable { /** * Create a waveform vibration. * - * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For + *

Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For * each pair, the value in the amplitude array determines the strength of the vibration and the * value in the timing array determines how long it vibrates for, in milliseconds. Amplitude * values must be between 0 and 255, and an amplitude of 0 implies no vibration (i.e. off). Any * pairs with a timing value of 0 will be ignored. - *

- * To cause the pattern to repeat, pass the index into the timings array at which to start the - * repetition, or -1 to disable repeating. - *

+ * + *

To cause the pattern to repeat, pass the index into the timings array at which to start + * the repetition, or -1 to disable repeating. Repeating effects will be played indefinitely + * and should be cancelled via {@link Vibrator#cancel()}. * * @param timings The timing values, in milliseconds, of the timing / amplitude pairs. Timing * values of 0 will cause the pair to be ignored. @@ -407,20 +407,59 @@ public abstract class VibrationEffect implements Parcelable { * Start building a waveform vibration. * *

The waveform builder offers more flexibility for creating waveform vibrations, allowing - * control over vibration frequency and ramping up or down the vibration amplitude, frequency or - * both. + * control over vibration amplitude and frequency via smooth transitions between values. * - *

For simpler waveform patterns see {@link #createWaveform} methods. + *

The waveform will start the first transition from the vibrator off state, with the + * resonant frequency by default. To provide an initial state, use + * {@link #startWaveform(VibrationParameter)}. * - * @hide - * @see VibrationEffect.WaveformBuilder + * @return The {@link VibrationEffect.WaveformBuilder} started with the initial parameters. */ - @TestApi @NonNull public static WaveformBuilder startWaveform() { return new WaveformBuilder(); } + /** + * Start building a waveform vibration with an initial state specified by a + * {@link VibrationParameter}. + * + *

The waveform builder offers more flexibility for creating waveform vibrations, allowing + * control over vibration amplitude and frequency via smooth transitions between values. + * + * @param initialParameter The initial {@link VibrationParameter} value to be applied at the + * beginning of the vibration. + * @return The {@link VibrationEffect.WaveformBuilder} started with the initial parameters. + */ + @NonNull + public static WaveformBuilder startWaveform(@NonNull VibrationParameter initialParameter) { + WaveformBuilder builder = startWaveform(); + builder.addTransition(Duration.ZERO, initialParameter); + return builder; + } + + /** + * Start building a waveform vibration with an initial state specified by two + * {@link VibrationParameter VibrationParameters}. + * + *

The waveform builder offers more flexibility for creating waveform vibrations, allowing + * control over vibration amplitude and frequency via smooth transitions between values. + * + * @param initialParameter1 The initial {@link VibrationParameter} value to be applied at the + * beginning of the vibration. + * @param initialParameter2 The initial {@link VibrationParameter} value to be applied at the + * beginning of the vibration, must be a different type of parameter + * than the one specified by the first argument. + * @return The {@link VibrationEffect.WaveformBuilder} started with the initial parameters. + */ + @NonNull + public static WaveformBuilder startWaveform(@NonNull VibrationParameter initialParameter1, + @NonNull VibrationParameter initialParameter2) { + WaveformBuilder builder = startWaveform(); + builder.addTransition(Duration.ZERO, initialParameter1, initialParameter2); + return builder; + } + @Override public int describeContents() { return 0; @@ -784,10 +823,23 @@ public abstract class VibrationEffect implements Parcelable { PRIMITIVE_LOW_TICK, }) @Retention(RetentionPolicy.SOURCE) - public @interface PrimitiveType {} + public @interface PrimitiveType { + } + + /** + * Exception thrown when adding an element to a {@link Composition} that already ends in an + * indefinitely repeating effect. + */ + public static final class UnreachableAfterRepeatingIndefinitelyException + extends IllegalStateException { + UnreachableAfterRepeatingIndefinitelyException() { + super("Compositions ending in an indefinitely repeating effect can't be extended"); + } + } /** * No haptic effect. Used to generate extended delays between primitives. + * * @hide */ public static final int PRIMITIVE_NOOP = 0; @@ -837,41 +889,76 @@ public abstract class VibrationEffect implements Parcelable { Composition() {} /** - * Add a haptic effect to the end of the current composition. + * Adds a time duration to the current composition, during which the vibrator will be + * turned off * - *

Similar to {@link #addEffect(VibrationEffect, int)} , but with no delay applied. + * @param duration The length of time the vibrator should be off. Value must be non-negative + * and will be truncated to milliseconds. + * @return This {@link Composition} object to enable adding multiple elements in one chain. * - * @param effect The effect to add to this composition as a primitive - * @return The {@link Composition} object to enable adding multiple primitives in one chain. - * @hide + * @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently + * ending with a repeating effect. */ - @TestApi @NonNull - public Composition addEffect(@NonNull VibrationEffect effect) { - return addEffect(effect, /* delay= */ 0); + public Composition addOffDuration(@NonNull Duration duration) { + int durationMs = (int) duration.toMillis(); + Preconditions.checkArgumentNonnegative(durationMs, "Off period must be non-negative"); + if (durationMs > 0) { + // Created a segment sustaining the zero amplitude to represent the delay. + addSegment(new StepSegment(/* amplitude= */ 0, /* frequencyHz= */ 0, + (int) duration.toMillis())); + } + return this; } /** * Add a haptic effect to the end of the current composition. * - * @param effect The effect to add to this composition as a primitive - * @param delay The amount of time in milliseconds to wait before playing this primitive - * @return The {@link Composition} object to enable adding multiple primitives in one chain. - * @hide + *

If this effect is repeating (e.g. created by {@link VibrationEffect#createWaveform} + * with a non-negative repeat index, or created by another composition that has effects + * repeating indefinitely), then no more effects or primitives will be accepted by this + * composition after this method. Such effects should be cancelled via + * {@link Vibrator#cancel()}. + * + * @param effect The effect to add to the end of this composition. + * @return This {@link Composition} object to enable adding multiple elements in one chain. + * + * @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently + * ending with a repeating effect. */ - @TestApi @NonNull - public Composition addEffect(@NonNull VibrationEffect effect, - @IntRange(from = 0) int delay) { - Preconditions.checkArgumentNonnegative(delay); - if (delay > 0) { - // Created a segment sustaining the zero amplitude to represent the delay. - addSegment(new StepSegment(/* amplitude= */ 0, /* frequencyHz= */ 0, - /* duration= */ delay)); - } + public Composition addEffect(@NonNull VibrationEffect effect) { return addSegments(effect); } + /** + * Add a haptic effect to the end of the current composition and play it on repeat, + * indefinitely. + * + *

The entire effect will be played on repeat, indefinitely, after all other elements + * already added to this composition are played. No more effects or primitives will be + * accepted by this composition after this method. Such effects should be cancelled via + * {@link Vibrator#cancel()}. + * + * @param effect The effect to add to the end of this composition, must be finite. + * @return This {@link Composition} object to enable adding multiple elements in one chain, + * although only {@link #compose()} can follow this call. + * + * @throws IllegalArgumentException if the given effect is already repeating indefinitely. + * @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently + * ending with a repeating effect. + */ + @NonNull + public Composition repeatEffectIndefinitely(@NonNull VibrationEffect effect) { + Preconditions.checkArgument(effect.getDuration() < Long.MAX_VALUE, + "Can't repeat an indefinitely repeating effect. Consider addEffect instead."); + int previousSegmentCount = mSegments.size(); + addSegments(effect); + // Set repeat after segments were added, since addSegments checks this index. + mRepeatIndex = previousSegmentCount; + return this; + } + /** * Add a haptic primitive to the end of the current composition. * @@ -879,8 +966,10 @@ public abstract class VibrationEffect implements Parcelable { * default scale applied. * * @param primitiveId The primitive to add + * @return This {@link Composition} object to enable adding multiple elements in one chain. * - * @return The {@link Composition} object to enable adding multiple primitives in one chain. + * @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently + * ending with a repeating effect. */ @NonNull public Composition addPrimitive(@PrimitiveType int primitiveId) { @@ -894,8 +983,10 @@ public abstract class VibrationEffect implements Parcelable { * * @param primitiveId The primitive to add * @param scale The scale to apply to the intensity of the primitive. + * @return This {@link Composition} object to enable adding multiple elements in one chain. * - * @return The {@link Composition} object to enable adding multiple primitives in one chain. + * @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently + * ending with a repeating effect. */ @NonNull public Composition addPrimitive(@PrimitiveType int primitiveId, @@ -910,7 +1001,10 @@ public abstract class VibrationEffect implements Parcelable { * @param scale The scale to apply to the intensity of the primitive. * @param delay The amount of time in milliseconds to wait before playing this primitive, * starting at the time the previous element in this composition is finished. - * @return The {@link Composition} object to enable adding multiple primitives in one chain. + * @return This {@link Composition} object to enable adding multiple elements in one chain. + * + * @throws UnreachableAfterRepeatingIndefinitelyException if the composition is currently + * ending with a repeating effect. */ @NonNull public Composition addPrimitive(@PrimitiveType int primitiveId, @@ -923,9 +1017,7 @@ public abstract class VibrationEffect implements Parcelable { private Composition addSegment(VibrationEffectSegment segment) { if (mRepeatIndex >= 0) { - throw new IllegalStateException( - "Composition already have a repeating effect so any new primitive would be" - + " unreachable."); + throw new UnreachableAfterRepeatingIndefinitelyException(); } mSegments.add(segment); return this; @@ -933,9 +1025,7 @@ public abstract class VibrationEffect implements Parcelable { private Composition addSegments(VibrationEffect effect) { if (mRepeatIndex >= 0) { - throw new IllegalStateException( - "Composition already have a repeating effect so any new primitive would be" - + " unreachable."); + throw new UnreachableAfterRepeatingIndefinitelyException(); } Composed composed = (Composed) effect; if (composed.getRepeatIndex() >= 0) { @@ -1001,162 +1091,251 @@ public abstract class VibrationEffect implements Parcelable { /** * A builder for waveform haptic effects. * - *

Waveform vibrations constitute of one or more timed segments where the vibration - * amplitude, frequency or both can linearly ramp to new values. + *

Waveform vibrations constitute of one or more timed transitions to new sets of vibration + * parameters. These parameters can be the vibration amplitude or frequency, for example. * - *

Waveform segments may have zero duration, which represent a jump to new vibration - * amplitude and/or frequency values. + *

Note that physical vibration actuators have different reaction times for changing + * amplitude and frequency. Durations specified here represent a timeline for the target + * parameters, and quality of effects may be improved if the durations allow time for a + * transition to be smoothly applied. * - *

Waveform segments may have the same start and end vibration amplitude and frequency, - * which represent a step where the amplitude and frequency are maintained for that duration. + *

Repeating waveforms can be built by constructing the repeating block separately and adding + * it to the end of a composition using + * {@link Composition#repeatEffectIndefinitely(VibrationEffect)}. * - * @hide - * @see VibrationEffect#startWaveform() + * @see VibrationEffect#startWaveform */ - @TestApi public static final class WaveformBuilder { + // Epsilon used for float comparison of amplitude and frequency values on transitions. + private static final float EPSILON = 1e-5f; + private ArrayList mSegments = new ArrayList<>(); + private float mLastAmplitude = 0f; + private float mLastFrequencyHz = 0f; WaveformBuilder() {} /** - * Vibrate with given amplitude for the given duration, in millis, keeping the previous - * frequency the same. + * Add a transition to new vibration parameter value to the end of this waveform. * - *

If the duration is zero the vibrator will jump to new amplitude. + *

The duration represents how long the vibrator should take to smoothly transition to + * the new vibration parameter. If the duration is zero then the vibrator will jump to the + * new value as fast as possible. * - * @param amplitude The amplitude for this step - * @param duration The duration of this step in milliseconds - * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. + *

Vibration parameter values will be truncated to conform to the device capabilities + * according to the {@link android.os.vibrator.VibratorFrequencyProfile}. + * + * @param duration The length of time this transition should take. Value must be + * non-negative and will be truncated to milliseconds. + * @param targetParameter The new target {@link VibrationParameter} value to be reached + * after the given duration. + * @return This {@link WaveformBuilder} object to enable adding multiple transitions in + * chain. */ - @SuppressLint("MissingGetterMatchingBuilder") + @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created. @NonNull - public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude, - @IntRange(from = 0) int duration) { - mSegments.add(new StepSegment(amplitude, getPreviousFrequencyHz(), duration)); + public WaveformBuilder addTransition(@NonNull Duration duration, + @NonNull VibrationParameter targetParameter) { + Preconditions.checkNotNull(duration, "Duration is null"); + checkVibrationParameter(targetParameter, "targetParameter"); + float amplitude = extractTargetAmplitude(targetParameter, /* target2= */ null); + float frequencyHz = extractTargetFrequency(targetParameter, /* target2= */ null); + addTransitionSegment(duration, amplitude, frequencyHz); return this; } /** - * Vibrate with given amplitude and frequency for the given duration, in millis. + * Add a transition to new vibration parameters to the end of this waveform. * - *

If the duration is zero the vibrator will jump to new amplitude. + *

The duration represents how long the vibrator should take to smoothly transition to + * the new vibration parameters. If the duration is zero then the vibrator will jump to the + * new values as fast as possible. * - * @param amplitude The amplitude for this step - * @param frequencyHz The frequency for this step, in hertz - * @param duration The duration of this step in milliseconds - * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. + *

Vibration parameters values will be truncated to conform to the device capabilities + * according to the {@link android.os.vibrator.VibratorFrequencyProfile}. + * + * @param duration The length of time this transition should take. Value must be + * non-negative and will be truncated to milliseconds. + * @param targetParameter1 The first target {@link VibrationParameter} value to be reached + * after the given duration. + * @param targetParameter2 The second target {@link VibrationParameter} value to be reached + * after the given duration, must be a different type of parameter + * than the one specified by the first argument. + * @return This {@link WaveformBuilder} object to enable adding multiple transitions in + * chain. */ - @SuppressLint("MissingGetterMatchingBuilder") + @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created. @NonNull - public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude, - @FloatRange(from = 1f) float frequencyHz, - @IntRange(from = 0) int duration) { - Preconditions.checkArgument(frequencyHz >= 1, "Frequency must be >= 1"); - mSegments.add(new StepSegment(amplitude, frequencyHz, duration)); + public WaveformBuilder addTransition(@NonNull Duration duration, + @NonNull VibrationParameter targetParameter1, + @NonNull VibrationParameter targetParameter2) { + Preconditions.checkNotNull(duration, "Duration is null"); + checkVibrationParameter(targetParameter1, "targetParameter1"); + checkVibrationParameter(targetParameter2, "targetParameter2"); + Preconditions.checkArgument( + !Objects.equals(targetParameter1.getClass(), targetParameter2.getClass()), + "Parameter arguments must specify different parameter types"); + float amplitude = extractTargetAmplitude(targetParameter1, targetParameter2); + float frequencyHz = extractTargetFrequency(targetParameter1, targetParameter2); + addTransitionSegment(duration, amplitude, frequencyHz); return this; } /** - * Ramp vibration linearly for the given duration, in millis, from previous amplitude value - * to the given one, keeping previous frequency. + * Add a duration to sustain the last vibration parameters of this waveform. * - *

If the duration is zero the vibrator will jump to new amplitude. + *

The duration represents how long the vibrator should sustain the last set of + * parameters provided to this builder. * - * @param amplitude The final amplitude this ramp should reach - * @param duration The duration of this ramp in milliseconds - * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. + * @param duration The length of time the last values should be sustained by the vibrator. + * Value must be >= 1ms. + * @return This {@link WaveformBuilder} object to enable adding multiple transitions in + * chain. */ - @SuppressLint("MissingGetterMatchingBuilder") + @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created. @NonNull - public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude, - @IntRange(from = 0) int duration) { - mSegments.add(new RampSegment(getPreviousAmplitude(), amplitude, - getPreviousFrequencyHz(), getPreviousFrequencyHz(), duration)); + public WaveformBuilder addSustain(@NonNull Duration duration) { + int durationMs = (int) duration.toMillis(); + Preconditions.checkArgument(durationMs >= 1, "Sustain duration must be >= 1ms"); + mSegments.add(new StepSegment(mLastAmplitude, mLastFrequencyHz, durationMs)); return this; } /** - * Ramp vibration linearly for the given duration, in millis, from previous amplitude and - * frequency values to the given ones. - * - *

If the duration is zero the vibrator will jump to new amplitude and frequency. - * - * @param amplitude The final amplitude this ramp should reach - * @param frequencyHz The final frequency this ramp should reach, in hertz - * @param duration The duration of this ramp in milliseconds - * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. - */ - @SuppressLint("MissingGetterMatchingBuilder") - @NonNull - public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude, - @FloatRange(from = 1f) float frequencyHz, - @IntRange(from = 0) int duration) { - Preconditions.checkArgument(frequencyHz >= 1, "Frequency must be >= 1"); - mSegments.add(new RampSegment(getPreviousAmplitude(), amplitude, - getPreviousFrequencyHz(), frequencyHz, duration)); - return this; - } - - /** - * Compose all the steps together into a single {@link VibrationEffect}. + * Build the waveform as a single {@link VibrationEffect}. * * The {@link WaveformBuilder} object is still valid after this call, so you can * continue adding more primitives to it and generating more {@link VibrationEffect}s by * calling this method again. * - * @return The {@link VibrationEffect} resulting from the composition of the steps. + * @return The {@link VibrationEffect} resulting from the list of transitions. */ @NonNull public VibrationEffect build() { - return build(/* repeat= */ -1); - } - - /** - * Compose all the steps together into a single {@link VibrationEffect}. - * - *

To cause the pattern to repeat, pass the index at which to start the repetition - * (starting at 0), or -1 to disable repeating. - * - *

The {@link WaveformBuilder} object is still valid after this call, so you can - * continue adding more primitives to it and generating more {@link VibrationEffect}s by - * calling this method again. - * - * @return The {@link VibrationEffect} resulting from the composition of the steps. - */ - @NonNull - public VibrationEffect build(int repeat) { if (mSegments.isEmpty()) { throw new IllegalStateException( - "WaveformBuilder must have at least one element to build."); + "WaveformBuilder must have at least one transition to build."); } - VibrationEffect effect = new Composed(mSegments, repeat); + VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1); effect.validate(); return effect; } - private float getPreviousFrequencyHz() { - if (!mSegments.isEmpty()) { - VibrationEffectSegment segment = mSegments.get(mSegments.size() - 1); - if (segment instanceof StepSegment) { - return ((StepSegment) segment).getFrequencyHz(); - } else if (segment instanceof RampSegment) { - return ((RampSegment) segment).getEndFrequencyHz(); - } - } - return 0; + private void checkVibrationParameter(@NonNull VibrationParameter vibrationParameter, + String paramName) { + Preconditions.checkNotNull(vibrationParameter, "%s is null", paramName); + Preconditions.checkArgument( + (vibrationParameter instanceof AmplitudeVibrationParameter) + || (vibrationParameter instanceof FrequencyVibrationParameter), + "%s is a unknown parameter", paramName); } - private float getPreviousAmplitude() { - if (!mSegments.isEmpty()) { - VibrationEffectSegment segment = mSegments.get(mSegments.size() - 1); - if (segment instanceof StepSegment) { - return ((StepSegment) segment).getAmplitude(); - } else if (segment instanceof RampSegment) { - return ((RampSegment) segment).getEndAmplitude(); + private float extractTargetAmplitude(@Nullable VibrationParameter target1, + @Nullable VibrationParameter target2) { + if (target2 instanceof AmplitudeVibrationParameter) { + return ((AmplitudeVibrationParameter) target2).amplitude; + } + if (target1 instanceof AmplitudeVibrationParameter) { + return ((AmplitudeVibrationParameter) target1).amplitude; + } + return mLastAmplitude; + } + + private float extractTargetFrequency(@Nullable VibrationParameter target1, + @Nullable VibrationParameter target2) { + if (target2 instanceof FrequencyVibrationParameter) { + return ((FrequencyVibrationParameter) target2).frequencyHz; + } + if (target1 instanceof FrequencyVibrationParameter) { + return ((FrequencyVibrationParameter) target1).frequencyHz; + } + return mLastFrequencyHz; + } + + private void addTransitionSegment(Duration duration, float targetAmplitude, + float targetFrequency) { + Preconditions.checkNotNull(duration, "Duration is null"); + Preconditions.checkArgument(!duration.isNegative(), + "Transition duration must be non-negative"); + int durationMs = (int) duration.toMillis(); + + // Ignore transitions with zero duration, but keep values for next additions. + if (durationMs > 0) { + if ((Math.abs(mLastAmplitude - targetAmplitude) < EPSILON) + && (Math.abs(mLastFrequencyHz - targetFrequency) < EPSILON)) { + // No value is changing, this can be best represented by a step segment. + mSegments.add(new StepSegment(targetAmplitude, targetFrequency, durationMs)); + } else { + mSegments.add(new RampSegment(mLastAmplitude, targetAmplitude, + mLastFrequencyHz, targetFrequency, durationMs)); } } - return 0; + + mLastAmplitude = targetAmplitude; + mLastFrequencyHz = targetFrequency; + } + } + + /** + * A representation of a single vibration parameter. + * + *

This is to describe a waveform haptic effect, which consists of one or more timed + * transitions to a new set of {@link VibrationParameter}s. + * + *

Examples of concrete parameters are the vibration amplitude or frequency. + * + * @see VibrationEffect.WaveformBuilder + */ + @SuppressWarnings("UserHandleName") // This is not a regular set of parameters, no *Params. + public static class VibrationParameter { + VibrationParameter() { + } + + /** + * The target vibration amplitude. + * + * @param amplitude The amplitude value, between 0 and 1, inclusive, where 0 represents the + * vibrator turned off and 1 represents the maximum amplitude the vibrator + * can reach across all supported frequencies. + * @return The {@link VibrationParameter} instance that represents given amplitude. + */ + @NonNull + public static VibrationParameter targetAmplitude( + @FloatRange(from = 0, to = 1) float amplitude) { + return new AmplitudeVibrationParameter(amplitude); + } + + /** + * The target vibration frequency. + * + * @param frequencyHz The frequency value, in hertz. + * @return The {@link VibrationParameter} instance that represents given frequency. + */ + @NonNull + public static VibrationParameter targetFrequency(@FloatRange(from = 1) float frequencyHz) { + return new FrequencyVibrationParameter(frequencyHz); + } + } + + /** The vibration amplitude, represented by a value in [0,1]. */ + private static final class AmplitudeVibrationParameter extends VibrationParameter { + public final float amplitude; + + AmplitudeVibrationParameter(float amplitude) { + Preconditions.checkArgument((amplitude >= 0) && (amplitude <= 1), + "Amplitude must be within [0,1]"); + this.amplitude = amplitude; + } + } + + /** The vibration frequency, in hertz, or zero to represent undefined frequency. */ + private static final class FrequencyVibrationParameter extends VibrationParameter { + public final float frequencyHz; + + FrequencyVibrationParameter(float frequencyHz) { + Preconditions.checkArgument(frequencyHz >= 1, "Frequency must be >= 1"); + Preconditions.checkArgument(Float.isFinite(frequencyHz), "Frequency must be finite"); + this.frequencyHz = frequencyHz; } } diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java index 10cec8243b8e5..104f077e5ad22 100644 --- a/core/tests/coretests/src/android/os/VibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java @@ -16,6 +16,9 @@ package android.os; +import static android.os.VibrationEffect.VibrationParameter.targetAmplitude; +import static android.os.VibrationEffect.VibrationParameter.targetFrequency; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -32,6 +35,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.net.Uri; +import android.os.VibrationEffect.Composition.UnreachableAfterRepeatingIndefinitelyException; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; @@ -43,6 +47,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; +import java.time.Duration; + @Presubmit @RunWith(MockitoJUnitRunner.class) public class VibrationEffectTest { @@ -122,16 +128,7 @@ public class VibrationEffectTest { VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1).validate(); VibrationEffect.createWaveform(new long[]{10, 10}, new int[] {0, 0}, -1).validate(); VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, 0).validate(); - VibrationEffect.startWaveform() - .addStep(/* amplitude= */ 1, /* duration= */ 10) - .addRamp(/* amplitude= */ 0, /* duration= */ 20) - .addStep(/* amplitude= */ 1, /* frequencyHz= */ 1, /* duration= */ 100) - .addRamp(/* amplitude= */ 0.5f, /* frequencyHz= */ 100, /* duration= */ 50) - .build() - .validate(); - assertThrows(IllegalStateException.class, - () -> VibrationEffect.startWaveform().build().validate()); assertThrows(IllegalArgumentException.class, () -> VibrationEffect.createWaveform(new long[0], new int[0], -1).validate()); assertThrows(IllegalArgumentException.class, @@ -145,27 +142,31 @@ public class VibrationEffectTest { assertThrows(IllegalArgumentException.class, () -> VibrationEffect.createWaveform( TEST_TIMINGS, TEST_AMPLITUDES, TEST_TIMINGS.length).validate()); + } + + @Test + public void testValidateWaveformBuilder() { + VibrationEffect.startWaveform(targetAmplitude(1)) + .addTransition(Duration.ofSeconds(1), targetAmplitude(0.5f), targetFrequency(100)) + .addTransition(Duration.ZERO, targetAmplitude(0f), targetFrequency(200)) + .addSustain(Duration.ofMinutes(2)) + .addTransition(Duration.ofMillis(10), targetAmplitude(1f), targetFrequency(50)) + .addSustain(Duration.ofMillis(1)) + .addTransition(Duration.ZERO, targetFrequency(150)) + .addSustain(Duration.ofMillis(2)) + .addTransition(Duration.ofSeconds(15), targetAmplitude(1)) + .build() + .validate(); + + assertThrows(IllegalStateException.class, + () -> VibrationEffect.startWaveform().build().validate()); + assertThrows(IllegalArgumentException.class, () -> targetAmplitude(-2)); + assertThrows(IllegalArgumentException.class, () -> targetFrequency(0)); assertThrows(IllegalArgumentException.class, - () -> VibrationEffect.startWaveform() - .addStep(/* amplitude= */ -2, 10).build().validate()); + () -> VibrationEffect.startWaveform().addTransition( + Duration.ofMillis(-10), targetAmplitude(1)).build().validate()); assertThrows(IllegalArgumentException.class, - () -> VibrationEffect.startWaveform() - .addStep(1, /* frequencyHz= */ -1f, 10).build().validate()); - assertThrows(IllegalArgumentException.class, - () -> VibrationEffect.startWaveform() - .addStep(1, /* duration= */ -1).build().validate()); - assertThrows(IllegalArgumentException.class, - () -> VibrationEffect.startWaveform() - .addStep(1, 100f, /* duration= */ -1).build().validate()); - assertThrows(IllegalArgumentException.class, - () -> VibrationEffect.startWaveform() - .addRamp(/* amplitude= */ -3, 10).build().validate()); - assertThrows(IllegalArgumentException.class, - () -> VibrationEffect.startWaveform() - .addRamp(1, /* frequencyHz= */ 0, 10).build().validate()); - assertThrows(IllegalArgumentException.class, - () -> VibrationEffect.startWaveform() - .addRamp(1, 10f, /* duration= */ -3).build().validate()); + () -> VibrationEffect.startWaveform().addSustain(Duration.ZERO).build().validate()); } @Test @@ -174,14 +175,24 @@ public class VibrationEffectTest { .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .addEffect(TEST_ONE_SHOT) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) - .addEffect(TEST_WAVEFORM, 100) + .addOffDuration(Duration.ofMillis(100)) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10) .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) .compose() .validate(); + VibrationEffect.startComposition() + .repeatEffectIndefinitely(TEST_ONE_SHOT) + .compose() + .validate(); + assertThrows(IllegalStateException.class, () -> VibrationEffect.startComposition().compose().validate()); + assertThrows(IllegalStateException.class, + () -> VibrationEffect.startComposition() + .addOffDuration(Duration.ofSeconds(0)) + .compose() + .validate()); assertThrows(IllegalArgumentException.class, () -> VibrationEffect.startComposition().addPrimitive(-1).compose().validate()); assertThrows(IllegalArgumentException.class, @@ -196,12 +207,27 @@ public class VibrationEffectTest { .validate()); assertThrows(IllegalArgumentException.class, () -> VibrationEffect.startComposition() - .addEffect(TEST_ONE_SHOT, /* delay= */ -10) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, -1) .compose() .validate()); assertThrows(IllegalArgumentException.class, () -> VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, -1) + .repeatEffectIndefinitely( + // Repeating waveform. + VibrationEffect.createWaveform( + new long[] { 10 }, new int[] { 100}, 0)) + .compose() + .validate()); + assertThrows(UnreachableAfterRepeatingIndefinitelyException.class, + () -> VibrationEffect.startComposition() + .repeatEffectIndefinitely(TEST_WAVEFORM) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .compose() + .validate()); + assertThrows(UnreachableAfterRepeatingIndefinitelyException.class, + () -> VibrationEffect.startComposition() + .repeatEffectIndefinitely(TEST_WAVEFORM) + .addEffect(TEST_ONE_SHOT) .compose() .validate()); } @@ -354,9 +380,9 @@ public class VibrationEffectTest { assertFalse(VibrationEffect.createWaveform( new long[]{200, 200, 700}, new int[]{1, 2, 3}, -1).isHapticFeedbackCandidate()); assertFalse(VibrationEffect.startWaveform() - .addRamp(1, 500) - .addStep(1, 200) - .addRamp(0, 500) + .addTransition(Duration.ofMillis(500), targetAmplitude(1)) + .addTransition(Duration.ofMillis(200), targetAmplitude(0.5f)) + .addTransition(Duration.ofMillis(500), targetAmplitude(0)) .build() .isHapticFeedbackCandidate()); } @@ -367,9 +393,9 @@ public class VibrationEffectTest { assertTrue(VibrationEffect.createWaveform( new long[]{100, 200, 300}, new int[]{1, 2, 3}, -1).isHapticFeedbackCandidate()); assertTrue(VibrationEffect.startWaveform() - .addRamp(1, 300) - .addStep(1, 200) - .addRamp(0, 300) + .addTransition(Duration.ofMillis(300), targetAmplitude(1)) + .addTransition(Duration.ofMillis(200), targetAmplitude(0.5f)) + .addTransition(Duration.ofMillis(300), targetAmplitude(0)) .build() .isHapticFeedbackCandidate()); } diff --git a/services/core/java/com/android/server/notification/VibratorHelper.java b/services/core/java/com/android/server/notification/VibratorHelper.java index 54dd113253ec2..e5d07bc152c01 100644 --- a/services/core/java/com/android/server/notification/VibratorHelper.java +++ b/services/core/java/com/android/server/notification/VibratorHelper.java @@ -16,6 +16,9 @@ package com.android.server.notification; +import static android.os.VibrationEffect.VibrationParameter.targetAmplitude; +import static android.os.VibrationEffect.VibrationParameter.targetFrequency; + import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; @@ -30,6 +33,7 @@ import android.util.Slog; import com.android.internal.R; import com.android.server.pm.PackageManagerService; +import java.time.Duration; import java.util.Arrays; /** @@ -89,8 +93,7 @@ public final class VibratorHelper { * Safely create a {@link VibrationEffect} from given waveform description. * *

The waveform is described by a sequence of values for target amplitude, frequency and - * duration, that are forwarded to - * {@link VibrationEffect.WaveformBuilder#addRamp(float, float, int)}. + * duration, that are forwarded to {@link VibrationEffect.WaveformBuilder#addTransition}. * *

This method returns {@code null} if the pattern is also {@code null} or invalid. * @@ -114,16 +117,17 @@ public final class VibratorHelper { VibrationEffect.WaveformBuilder waveformBuilder = VibrationEffect.startWaveform(); for (int i = 0; i < length; i += 3) { - waveformBuilder.addRamp( - /* amplitude= */ values[i], - /* frequencyHz= */ values[i + 1], - /* duration= */ (int) values[i + 2]); + waveformBuilder.addTransition(Duration.ofMillis((int) values[i + 2]), + targetAmplitude(values[i]), targetFrequency(values[i + 1])); } + VibrationEffect effect = waveformBuilder.build(); if (insistent) { - return waveformBuilder.build(/* repeat= */ 0); + return VibrationEffect.startComposition() + .repeatEffectIndefinitely(effect) + .compose(); } - return waveformBuilder.build(); + return effect; } catch (IllegalArgumentException e) { Slog.e(TAG, "Error creating vibration PWLE waveform with pattern: " + Arrays.toString(values)); diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index a95b6c955d632..b2e34da6545ae 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -16,6 +16,9 @@ package com.android.server.vibrator; +import static android.os.VibrationEffect.VibrationParameter.targetAmplitude; +import static android.os.VibrationEffect.VibrationParameter.targetFrequency; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -65,6 +68,7 @@ import libcore.util.NativeAllocationRegistry; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; @@ -1711,7 +1715,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { long duration = Long.parseLong(getNextArgRequired()); int amplitude = hasAmplitude ? Integer.parseInt(getNextArgRequired()) : VibrationEffect.DEFAULT_AMPLITUDE; - composition.addEffect(VibrationEffect.createOneShot(duration, amplitude), delay); + composition.addOffDuration(Duration.ofMillis(delay)); + composition.addEffect(VibrationEffect.createOneShot(duration, amplitude)); } private void addWaveformToComposition(VibrationEffect.Composition composition) { @@ -1762,23 +1767,44 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + // Add delay before the waveform. + composition.addOffDuration(Duration.ofMillis(delay)); + VibrationEffect.WaveformBuilder waveform = VibrationEffect.startWaveform(); for (int i = 0; i < durations.size(); i++) { - if (isContinuous) { - if (hasFrequencies) { - waveform.addRamp(amplitudes.get(i), frequencies.get(i), durations.get(i)); - } else { - waveform.addRamp(amplitudes.get(i), durations.get(i)); - } + Duration transitionDuration = isContinuous + ? Duration.ofMillis(durations.get(i)) + : Duration.ZERO; + + if (hasFrequencies) { + waveform.addTransition(transitionDuration, targetAmplitude(amplitudes.get(i)), + targetFrequency(frequencies.get(i))); } else { + waveform.addTransition(transitionDuration, targetAmplitude(amplitudes.get(i))); + } + if (!isContinuous) { + waveform.addSustain(Duration.ofMillis(durations.get(i))); + } + + if ((i > 0) && (i == repeat)) { + // Add segment that is not repeated to the composition and reset builder. + composition.addEffect(waveform.build()); + if (hasFrequencies) { - waveform.addStep(amplitudes.get(i), frequencies.get(i), durations.get(i)); + waveform = VibrationEffect.startWaveform(targetAmplitude(amplitudes.get(i)), + targetFrequency(frequencies.get(i))); } else { - waveform.addStep(amplitudes.get(i), durations.get(i)); + waveform = VibrationEffect.startWaveform( + targetAmplitude(amplitudes.get(i))); } } } - composition.addEffect(waveform.build(repeat), delay); + if (repeat < 0) { + composition.addEffect(waveform.build()); + } else { + // The waveform was already split at the repeat index, just repeat what remains. + composition.repeatEffectIndefinitely(waveform.build()); + } } private void addPrebakedToComposition(VibrationEffect.Composition composition) { @@ -1796,7 +1822,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } int effectId = Integer.parseInt(getNextArgRequired()); - composition.addEffect(VibrationEffect.get(effectId, shouldFallback), delay); + composition.addOffDuration(Duration.ofMillis(delay)); + composition.addEffect(VibrationEffect.get(effectId, shouldFallback)); } private void addPrimitivesToComposition(VibrationEffect.Composition composition) { diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java index 5dd44ffc664fc..020d9f8361d23 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java @@ -16,6 +16,9 @@ package com.android.server.vibrator; +import static android.os.VibrationEffect.VibrationParameter.targetAmplitude; +import static android.os.VibrationEffect.VibrationParameter.targetFrequency; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -69,6 +72,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.time.Duration; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -525,7 +529,8 @@ public class VibrationThreadTest { .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) - .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK), /* delay= */ 100) + .addOffDuration(Duration.ofMillis(100)) + .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) .compose(); VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); @@ -558,11 +563,12 @@ public class VibrationThreadTest { 0.5f /* 100Hz*/, 1 /* 150Hz */, 0.6f /* 200Hz */); long vibrationId = 1; - VibrationEffect effect = VibrationEffect.startWaveform() - .addStep(1, 10) - .addRamp(0, 20) - .addStep(0.8f, 100, 30) - .addRamp(0.6f, 200, 40) + VibrationEffect effect = VibrationEffect.startWaveform(targetAmplitude(1)) + .addSustain(Duration.ofMillis(10)) + .addTransition(Duration.ofMillis(20), targetAmplitude(0)) + .addTransition(Duration.ZERO, targetAmplitude(0.8f), targetFrequency(100)) + .addSustain(Duration.ofMillis(30)) + .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200)) .build(); VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); @@ -595,11 +601,12 @@ public class VibrationThreadTest { fakeVibrator.setPwleSizeMax(2); long vibrationId = 1; - VibrationEffect effect = VibrationEffect.startWaveform() - .addStep(1, 10) - .addRamp(0, 20) - .addStep(0.8f, 10, 30) - .addRamp(0.6f, 100, 40) + VibrationEffect effect = VibrationEffect.startWaveform(targetAmplitude(1)) + .addSustain(Duration.ofMillis(10)) + .addTransition(Duration.ofMillis(20), targetAmplitude(0)) + .addTransition(Duration.ZERO, targetAmplitude(0.8f), targetFrequency(100)) + .addSustain(Duration.ofMillis(30)) + .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200)) .build(); VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread); @@ -1261,7 +1268,9 @@ public class VibrationThreadTest { fakeVibrator.setPwleSizeMax(2); long vibrationId = 1; - VibrationEffect effect = VibrationEffect.startWaveform().addRamp(1, 1).build(); + VibrationEffect effect = VibrationEffect.startWaveform() + .addTransition(Duration.ofMillis(1), targetAmplitude(1)) + .build(); VibrationThread thread = startThreadAndDispatcher(vibrationId, effect); waitForCompletion(thread);