Introduce WaveformBuilder to VibrationEffect

Create a new parcelable RampSegment and introduce
VibrationEffect.startWaveform() method that returns a
WaveformBuilder that allows creating step/ramp waveforms that
changes amplitude and frequency of the effect.

Implementations of VibrationThread and InputManagerService for now just
ignore the new segments and keep existing behavior.

Bug: 167947076
Test: VibrationEffectTest
Change-Id: Ibac895b63196e779457efc297f1f5497a7814ae9
This commit is contained in:
Lais Andrade
2021-03-04 20:00:39 +00:00
parent bd992109e0
commit 601a944e45
12 changed files with 688 additions and 32 deletions

View File

@@ -1665,6 +1665,7 @@ 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
@@ -1686,6 +1687,20 @@ package android.os {
field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect.Composed> 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, to=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, to=1.0f) float, @IntRange(from=0) int);
method @NonNull public android.os.VibrationEffect build();
method @NonNull public android.os.VibrationEffect build(int);
}
public class VintfObject {
method public static String[] getHalNamesAndVersions();
method public static String getSepolicyVersion();
@@ -1832,11 +1847,28 @@ package android.os.vibrator {
field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PrimitiveSegment> CREATOR;
}
public final class RampSegment extends android.os.vibrator.VibrationEffectSegment {
method @NonNull public android.os.vibrator.RampSegment applyEffectStrength(int);
method public int describeContents();
method public long getDuration();
method public float getEndAmplitude();
method public float getEndFrequency();
method public float getStartAmplitude();
method public float getStartFrequency();
method public boolean hasNonZeroAmplitude();
method @NonNull public android.os.vibrator.RampSegment resolve(int);
method @NonNull public android.os.vibrator.RampSegment scale(float);
method public void validate();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.RampSegment> CREATOR;
}
public final class StepSegment extends android.os.vibrator.VibrationEffectSegment {
method @NonNull public android.os.vibrator.StepSegment applyEffectStrength(int);
method public int describeContents();
method public float getAmplitude();
method public long getDuration();
method public float getFrequency();
method public boolean hasNonZeroAmplitude();
method @NonNull public android.os.vibrator.StepSegment resolve(int);
method @NonNull public android.os.vibrator.StepSegment scale(float);

View File

@@ -21,6 +21,7 @@ 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;
@@ -30,6 +31,7 @@ import android.hardware.vibrator.V1_3.Effect;
import android.net.Uri;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.MathUtils;
@@ -250,7 +252,7 @@ public abstract class VibrationEffect implements Parcelable {
for (int i = 0; i < timings.length; i++) {
float parsedAmplitude = amplitudes[i] == DEFAULT_AMPLITUDE
? DEFAULT_AMPLITUDE : (float) amplitudes[i] / MAX_AMPLITUDE;
segments.add(new StepSegment(parsedAmplitude, (int) timings[i]));
segments.add(new StepSegment(parsedAmplitude, /* frequency= */ 0, (int) timings[i]));
}
VibrationEffect effect = new Composed(segments, repeat);
effect.validate();
@@ -389,8 +391,26 @@ public abstract class VibrationEffect implements Parcelable {
* @see VibrationEffect.Composition
*/
@NonNull
public static VibrationEffect.Composition startComposition() {
return new VibrationEffect.Composition();
public static Composition startComposition() {
return new Composition();
}
/**
* Start building a waveform vibration.
*
* <p>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.
*
* <p>For simpler waveform patterns see {@link #createWaveform} methods.
*
* @hide
* @see VibrationEffect.WaveformBuilder
*/
@TestApi
@NonNull
public static WaveformBuilder startWaveform() {
return new WaveformBuilder();
}
@Override
@@ -770,6 +790,42 @@ public abstract class VibrationEffect implements Parcelable {
Composition() {}
/**
* Add a haptic effect to the end of the current composition.
*
* <p>Similar to {@link #addEffect(VibrationEffect, int)} , but with no delay applied.
*
* @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
*/
@TestApi
@NonNull
public Composition addEffect(@NonNull VibrationEffect effect) {
return addEffect(effect, /* delay= */ 0);
}
/**
* 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
*/
@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, /* frequency= */ 0,
/* duration= */ delay));
}
return addSegments(effect);
}
/**
* Add a haptic primitive to the end of the current composition.
*
@@ -829,6 +885,21 @@ public abstract class VibrationEffect implements Parcelable {
return this;
}
private Composition addSegments(VibrationEffect effect) {
if (mRepeatIndex >= 0) {
throw new IllegalStateException(
"Composition already have a repeating effect so any new primitive would be"
+ " unreachable.");
}
Composed composed = (Composed) effect;
if (composed.getRepeatIndex() >= 0) {
// Start repeating from the index relative to the composed waveform.
mRepeatIndex = mSegments.size() + composed.getRepeatIndex();
}
mSegments.addAll(composed.getSegments());
return this;
}
/**
* Compose all of the added primitives together into a single {@link VibrationEffect}.
*
@@ -881,6 +952,164 @@ public abstract class VibrationEffect implements Parcelable {
}
}
/**
* A builder for waveform haptic effects.
*
* <p>Waveform vibrations constitute of one or more timed segments where the vibration
* amplitude, frequency or both can linearly ramp to new values.
*
* <p>Waveform segments may have zero duration, which represent a jump to new vibration
* amplitude and/or frequency values.
*
* <p>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.
*
* @hide
* @see VibrationEffect#startWaveform()
*/
@TestApi
public static final class WaveformBuilder {
private ArrayList<VibrationEffectSegment> mSegments = new ArrayList<>();
WaveformBuilder() {}
/**
* Vibrate with given amplitude for the given duration, in millis, keeping the previous
* frequency the same.
*
* <p>If the duration is zero the vibrator will jump to new amplitude.
*
* @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.
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude,
@IntRange(from = 0) int duration) {
return addStep(amplitude, getPreviousFrequency(), duration);
}
/**
* Vibrate with given amplitude for the given duration, in millis, keeping the previous
* vibration frequency the same.
*
* <p>If the duration is zero the vibrator will jump to new amplitude.
*
* @param amplitude The amplitude for this step
* @param frequency The frequency for this step
* @param duration The duration of this step in milliseconds
* @return The {@link WaveformBuilder} object to enable adding multiple steps in chain.
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude,
@FloatRange(from = -1f, to = 1f) float frequency,
@IntRange(from = 0) int duration) {
mSegments.add(new StepSegment(amplitude, frequency, duration));
return this;
}
/**
* Ramp vibration linearly for the given duration, in millis, from previous amplitude value
* to the given one, keeping previous frequency.
*
* <p>If the duration is zero the vibrator will jump to new amplitude.
*
* @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.
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude,
@IntRange(from = 0) int duration) {
return addRamp(amplitude, getPreviousFrequency(), duration);
}
/**
* Ramp vibration linearly for the given duration, in millis, from previous amplitude and
* frequency values to the given ones.
*
* <p>If the duration is zero the vibrator will jump to new amplitude and frequency.
*
* @param amplitude The final amplitude this ramp should reach
* @param frequency The final frequency 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.
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude,
@FloatRange(from = -1f, to = 1f) float frequency,
@IntRange(from = 0) int duration) {
mSegments.add(new RampSegment(getPreviousAmplitude(), amplitude, getPreviousFrequency(),
frequency, duration));
return this;
}
/**
* Compose all of the steps together into 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.
*/
@NonNull
public VibrationEffect build() {
return build(/* repeat= */ -1);
}
/**
* Compose all of the steps together into a single {@link VibrationEffect}.
*
* <p>To cause the pattern to repeat, pass the index at which to start the repetition
* (starting at 0), or -1 to disable repeating.
*
* <p>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.");
}
VibrationEffect effect = new Composed(mSegments, repeat);
effect.validate();
return effect;
}
private float getPreviousFrequency() {
if (!mSegments.isEmpty()) {
VibrationEffectSegment segment = mSegments.get(mSegments.size() - 1);
if (segment instanceof StepSegment) {
return ((StepSegment) segment).getFrequency();
} else if (segment instanceof RampSegment) {
return ((RampSegment) segment).getEndFrequency();
}
}
return 0;
}
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();
}
}
return 0;
}
}
@NonNull
public static final Parcelable.Creator<VibrationEffect> CREATOR =
new Parcelable.Creator<VibrationEffect>() {

View File

@@ -0,0 +1,176 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.os.vibrator;
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.VibrationEffect;
import com.android.internal.util.Preconditions;
import java.util.Objects;
/**
* Representation of {@link VibrationEffectSegment} that ramps vibration amplitude and/or frequency
* for a specified duration.
*
* @hide
*/
@TestApi
public final class RampSegment extends VibrationEffectSegment {
private final float mStartAmplitude;
private final float mStartFrequency;
private final float mEndAmplitude;
private final float mEndFrequency;
private final int mDuration;
RampSegment(@NonNull Parcel in) {
this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readInt());
}
/** @hide */
public RampSegment(float startAmplitude, float endAmplitude, float startFrequency,
float endFrequency, int duration) {
mStartAmplitude = startAmplitude;
mEndAmplitude = endAmplitude;
mStartFrequency = startFrequency;
mEndFrequency = endFrequency;
mDuration = duration;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof RampSegment)) {
return false;
}
RampSegment other = (RampSegment) o;
return Float.compare(mStartAmplitude, other.mStartAmplitude) == 0
&& Float.compare(mEndAmplitude, other.mEndAmplitude) == 0
&& Float.compare(mStartFrequency, other.mStartFrequency) == 0
&& Float.compare(mEndFrequency, other.mEndFrequency) == 0
&& mDuration == other.mDuration;
}
public float getStartAmplitude() {
return mStartAmplitude;
}
public float getEndAmplitude() {
return mEndAmplitude;
}
public float getStartFrequency() {
return mStartFrequency;
}
public float getEndFrequency() {
return mEndFrequency;
}
@Override
public long getDuration() {
return mDuration;
}
@Override
public boolean hasNonZeroAmplitude() {
return mStartAmplitude > 0 || mEndAmplitude > 0;
}
@Override
public void validate() {
Preconditions.checkArgumentNonnegative(mDuration,
"Durations must all be >= 0, got " + mDuration);
Preconditions.checkArgumentInRange(mStartAmplitude, 0f, 1f, "startAmplitude");
Preconditions.checkArgumentInRange(mEndAmplitude, 0f, 1f, "endAmplitude");
}
@NonNull
@Override
public RampSegment resolve(int defaultAmplitude) {
// Default amplitude is not supported for ramping.
return this;
}
@NonNull
@Override
public RampSegment scale(float scaleFactor) {
float newStartAmplitude = VibrationEffect.scale(mStartAmplitude, scaleFactor);
float newEndAmplitude = VibrationEffect.scale(mEndAmplitude, scaleFactor);
if (Float.compare(mStartAmplitude, newStartAmplitude) == 0
&& Float.compare(mEndAmplitude, newEndAmplitude) == 0) {
return this;
}
return new RampSegment(newStartAmplitude, newEndAmplitude, mStartFrequency, mEndFrequency,
mDuration);
}
@NonNull
@Override
public RampSegment applyEffectStrength(int effectStrength) {
return this;
}
@Override
public int hashCode() {
return Objects.hash(mStartAmplitude, mEndAmplitude, mStartFrequency, mEndFrequency,
mDuration);
}
@Override
public String toString() {
return "Ramp{startAmplitude=" + mStartAmplitude
+ ", endAmplitude=" + mEndAmplitude
+ ", startFrequency=" + mStartFrequency
+ ", endFrequency=" + mEndFrequency
+ ", duration=" + mDuration
+ "}";
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
out.writeInt(PARCEL_TOKEN_RAMP);
out.writeFloat(mStartAmplitude);
out.writeFloat(mEndAmplitude);
out.writeFloat(mStartFrequency);
out.writeFloat(mEndFrequency);
out.writeInt(mDuration);
}
@NonNull
public static final Creator<RampSegment> CREATOR =
new Creator<RampSegment>() {
@Override
public RampSegment createFromParcel(Parcel in) {
// Skip the type token
in.readInt();
return new RampSegment(in);
}
@Override
public RampSegment[] newArray(int size) {
return new RampSegment[size];
}
};
}

View File

@@ -27,23 +27,25 @@ import com.android.internal.util.Preconditions;
import java.util.Objects;
/**
* Representation of {@link VibrationEffectSegment} that holds a fixed vibration amplitude for a
* specified duration.
* Representation of {@link VibrationEffectSegment} that holds a fixed vibration amplitude and
* frequency for a specified duration.
*
* @hide
*/
@TestApi
public final class StepSegment extends VibrationEffectSegment {
private final float mAmplitude;
private final float mFrequency;
private final int mDuration;
StepSegment(@NonNull Parcel in) {
this(in.readFloat(), in.readInt());
this(in.readFloat(), in.readFloat(), in.readInt());
}
/** @hide */
public StepSegment(float amplitude, int duration) {
public StepSegment(float amplitude, float frequency, int duration) {
mAmplitude = amplitude;
mFrequency = frequency;
mDuration = duration;
}
@@ -54,6 +56,7 @@ public final class StepSegment extends VibrationEffectSegment {
}
StepSegment other = (StepSegment) o;
return Float.compare(mAmplitude, other.mAmplitude) == 0
&& Float.compare(mFrequency, other.mFrequency) == 0
&& mDuration == other.mDuration;
}
@@ -61,6 +64,10 @@ public final class StepSegment extends VibrationEffectSegment {
return mAmplitude;
}
public float getFrequency() {
return mFrequency;
}
@Override
public long getDuration() {
return mDuration;
@@ -92,7 +99,8 @@ public final class StepSegment extends VibrationEffectSegment {
if (Float.compare(mAmplitude, VibrationEffect.DEFAULT_AMPLITUDE) != 0) {
return this;
}
return new StepSegment((float) defaultAmplitude / VibrationEffect.MAX_AMPLITUDE, mDuration);
return new StepSegment((float) defaultAmplitude / VibrationEffect.MAX_AMPLITUDE, mFrequency,
mDuration);
}
@NonNull
@@ -101,7 +109,8 @@ public final class StepSegment extends VibrationEffectSegment {
if (Float.compare(mAmplitude, VibrationEffect.DEFAULT_AMPLITUDE) == 0) {
return this;
}
return new StepSegment(VibrationEffect.scale(mAmplitude, scaleFactor), mDuration);
return new StepSegment(VibrationEffect.scale(mAmplitude, scaleFactor), mFrequency,
mDuration);
}
@NonNull
@@ -112,12 +121,13 @@ public final class StepSegment extends VibrationEffectSegment {
@Override
public int hashCode() {
return Objects.hash(mAmplitude, mDuration);
return Objects.hash(mAmplitude, mFrequency, mDuration);
}
@Override
public String toString() {
return "Step{amplitude=" + mAmplitude
+ ", frequency=" + mFrequency
+ ", duration=" + mDuration
+ "}";
}
@@ -131,6 +141,7 @@ public final class StepSegment extends VibrationEffectSegment {
public void writeToParcel(@NonNull Parcel out, int flags) {
out.writeInt(PARCEL_TOKEN_STEP);
out.writeFloat(mAmplitude);
out.writeFloat(mFrequency);
out.writeInt(mDuration);
}

View File

@@ -31,7 +31,8 @@ import android.os.VibrationEffect;
* <ol>
* <li>A predefined vibration effect;
* <li>A composable effect primitive;
* <li>Fixed amplitude value to be held for a specified duration;
* <li>Fixed amplitude and frequency values to be held for a specified duration;
* <li>Pairs of amplitude and frequency values to be ramped to for a specified duration;
* </ol>
*
* @hide
@@ -42,6 +43,7 @@ public abstract class VibrationEffectSegment implements Parcelable {
static final int PARCEL_TOKEN_PREBAKED = 1;
static final int PARCEL_TOKEN_PRIMITIVE = 2;
static final int PARCEL_TOKEN_STEP = 3;
static final int PARCEL_TOKEN_RAMP = 4;
/** Prevent subclassing from outside of this package */
VibrationEffectSegment() {
@@ -96,6 +98,8 @@ public abstract class VibrationEffectSegment implements Parcelable {
switch (in.readInt()) {
case PARCEL_TOKEN_STEP:
return new StepSegment(in);
case PARCEL_TOKEN_RAMP:
return new RampSegment(in);
case PARCEL_TOKEN_PREBAKED:
return new PrebakedSegment(in);
case PARCEL_TOKEN_PRIMITIVE:

View File

@@ -25,6 +25,16 @@ message StepSegmentProto {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
optional int32 duration = 1;
optional float amplitude = 2;
optional float frequency = 3;
}
message RampSegmentProto {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
optional int32 duration = 1;
optional float startAmplitude = 2;
optional float endAmplitude = 3;
optional float startFrequency = 4;
optional float endFrequency = 5;
}
message PrebakedSegmentProto {
@@ -46,6 +56,7 @@ message SegmentProto {
optional PrebakedSegmentProto prebaked = 1;
optional PrimitiveSegmentProto primitive = 2;
optional StepSegmentProto step = 3;
optional RampSegmentProto ramp = 4;
}
// A com.android.os.VibrationEffect object.

View File

@@ -118,7 +118,16 @@ public class VibrationEffectTest {
public void testValidateWaveform() {
VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -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, /* frequency*/ 1, /* duration= */ 100)
.addRamp(/* amplitude= */ 0.5f, /* frequency*/ -1, /* 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,
@@ -132,17 +141,31 @@ public class VibrationEffectTest {
assertThrows(IllegalArgumentException.class,
() -> VibrationEffect.createWaveform(
TEST_TIMINGS, TEST_AMPLITUDES, TEST_TIMINGS.length).validate());
assertThrows(IllegalArgumentException.class,
() -> VibrationEffect.startWaveform()
.addStep(/* amplitude= */ -2, 10).build().validate());
assertThrows(IllegalArgumentException.class,
() -> VibrationEffect.startWaveform()
.addStep(1, /* duration= */ -1).build().validate());
assertThrows(IllegalArgumentException.class,
() -> VibrationEffect.startWaveform()
.addStep(1, 0, /* duration= */ -1).build().validate());
}
@Test
public void testValidateComposed() {
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
.addEffect(TEST_ONE_SHOT)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
.addEffect(TEST_WAVEFORM, 100)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10)
.addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
.compose()
.validate();
assertThrows(IllegalStateException.class,
() -> VibrationEffect.startComposition().compose().validate());
assertThrows(IllegalArgumentException.class,
() -> VibrationEffect.startComposition().addPrimitive(-1).compose().validate());
assertThrows(IllegalArgumentException.class,
@@ -150,6 +173,16 @@ public class VibrationEffectTest {
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, -1, 10)
.compose()
.validate());
assertThrows(IllegalArgumentException.class,
() -> VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, -10)
.compose()
.validate());
assertThrows(IllegalArgumentException.class,
() -> VibrationEffect.startComposition()
.addEffect(TEST_ONE_SHOT, /* delay= */ -10)
.compose()
.validate());
assertThrows(IllegalArgumentException.class,
() -> VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, -1)
@@ -185,6 +218,12 @@ public class VibrationEffectTest {
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 1)
.compose();
assertEquals(effect, effect.resolve(51));
VibrationEffect.Composed resolved = VibrationEffect.startComposition()
.addEffect(DEFAULT_ONE_SHOT)
.compose()
.resolve(51);
assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude());
}
@Test
@@ -215,6 +254,13 @@ public class VibrationEffectTest {
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 1)
.compose();
assertEquals(effect, effect.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_LIGHT));
VibrationEffect.Composed applied = VibrationEffect.startComposition()
.addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
.compose()
.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_LIGHT);
assertEquals(VibrationEffect.EFFECT_STRENGTH_LIGHT,
((PrebakedSegment) applied.getSegments().get(0)).getEffectStrength());
}
@Test
@@ -251,13 +297,16 @@ public class VibrationEffectTest {
VibrationEffect.Composed effect =
(VibrationEffect.Composed) VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 1)
.addEffect(TEST_ONE_SHOT)
.compose();
VibrationEffect.Composed scaledUp = effect.scale(1.5f);
assertTrue(0.5f < ((PrimitiveSegment) scaledUp.getSegments().get(0)).getScale());
assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(1)).getAmplitude());
VibrationEffect.Composed scaledDown = effect.scale(0.5f);
assertTrue(0.5f > ((PrimitiveSegment) scaledDown.getSegments().get(0)).getScale());
assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(1)).getAmplitude());
}
private Resources mockRingtoneResources() {
@@ -293,4 +342,4 @@ public class VibrationEffectTest {
return context;
}
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.os.vibrator;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertSame;
import static junit.framework.Assert.assertTrue;
import static org.testng.Assert.assertThrows;
import android.os.Parcel;
import android.os.VibrationEffect;
import android.platform.test.annotations.Presubmit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@Presubmit
@RunWith(MockitoJUnitRunner.class)
public class RampSegmentTest {
private static final float TOLERANCE = 1e-2f;
@Test
public void testCreation() {
RampSegment ramp = new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 0,
/* StartFrequency= */ -1, /* endFrequency= */ 1, /* duration= */ 100);
assertEquals(100L, ramp.getDuration());
assertTrue(ramp.hasNonZeroAmplitude());
assertEquals(1f, ramp.getStartAmplitude());
assertEquals(0f, ramp.getEndAmplitude());
assertEquals(-1f, ramp.getStartFrequency());
assertEquals(1f, ramp.getEndFrequency());
}
@Test
public void testSerialization() {
RampSegment original = new RampSegment(0, 1, 0, 0.5f, 10);
Parcel parcel = Parcel.obtain();
original.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
assertEquals(original, RampSegment.CREATOR.createFromParcel(parcel));
}
@Test
public void testValidate() {
new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 0,
/* StartFrequency= */ -1, /* endFrequency= */ 1, /* duration= */ 100).validate();
assertThrows(IllegalArgumentException.class,
() -> new RampSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0, 0, 0).validate());
assertThrows(IllegalArgumentException.class,
() -> new RampSegment(/* startAmplitude= */ -2, 0, 0, 0, 0).validate());
assertThrows(IllegalArgumentException.class,
() -> new RampSegment(0, /* endAmplitude= */ 2, 0, 0, 0).validate());
assertThrows(IllegalArgumentException.class,
() -> new RampSegment(0, 0, 0, 0, /* duration= */ -1).validate());
}
@Test
public void testHasNonZeroAmplitude() {
assertTrue(new RampSegment(0, 1, 0, 0, 0).hasNonZeroAmplitude());
assertTrue(new RampSegment(0.01f, 0, 0, 0, 0).hasNonZeroAmplitude());
assertFalse(new RampSegment(0, 0, 0, 0, 0).hasNonZeroAmplitude());
}
@Test
public void testResolve() {
RampSegment ramp = new RampSegment(0, 1, 0, 0, 0);
assertSame(ramp, ramp.resolve(100));
}
@Test
public void testApplyEffectStrength_ignoresAndReturnsSameEffect() {
RampSegment ramp = new RampSegment(1, 0, 1, 0, 0);
assertSame(ramp, ramp.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG));
}
@Test
public void testScale() {
RampSegment initial = new RampSegment(0, 1, 0, 0, 0);
assertEquals(0f, initial.scale(1).getStartAmplitude(), TOLERANCE);
assertEquals(0f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
assertEquals(0f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
assertEquals(0f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
assertEquals(0f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
assertEquals(1f, initial.scale(1).getEndAmplitude(), TOLERANCE);
assertEquals(0.34f, initial.scale(0.5f).getEndAmplitude(), TOLERANCE);
// The original value was not scaled up, so this only scales it down.
assertEquals(1f, initial.scale(1.5f).getEndAmplitude(), TOLERANCE);
assertEquals(0.53f, initial.scale(1.5f).scale(2 / 3f).getEndAmplitude(), TOLERANCE);
// Does not restore to the exact original value because scale up is a bit offset.
assertEquals(0.71f, initial.scale(0.8f).getEndAmplitude(), TOLERANCE);
assertEquals(0.84f, initial.scale(0.8f).scale(1.25f).getEndAmplitude(), TOLERANCE);
}
@Test
public void testScale_halfPrimitiveScaleValue() {
RampSegment initial = new RampSegment(0.5f, 1, 0, 0, 0);
assertEquals(0.5f, initial.scale(1).getStartAmplitude(), TOLERANCE);
assertEquals(0.17f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
// Does not restore to the exact original value because scale up is a bit offset.
assertEquals(0.86f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
assertEquals(0.47f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
// Does not restore to the exact original value because scale up is a bit offset.
assertEquals(0.35f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE);
assertEquals(0.5f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
}
}

View File

@@ -38,16 +38,18 @@ public class StepSegmentTest {
@Test
public void testCreation() {
StepSegment step = new StepSegment(/* amplitude= */ 1f, /* duration= */ 100);
StepSegment step = new StepSegment(/* amplitude= */ 1f, /* frequency= */ -1f,
/* duration= */ 100);
assertEquals(100, step.getDuration());
assertTrue(step.hasNonZeroAmplitude());
assertEquals(1f, step.getAmplitude());
assertEquals(-1f, step.getFrequency());
}
@Test
public void testSerialization() {
StepSegment original = new StepSegment(0.5f, 10);
StepSegment original = new StepSegment(0.5f, 1f, 10);
Parcel parcel = Parcel.obtain();
original.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
@@ -56,31 +58,31 @@ public class StepSegmentTest {
@Test
public void testValidate() {
new StepSegment(/* amplitude= */ 0f, /* duration= */ 100).validate();
new StepSegment(/* amplitude= */ 0f, /* frequency= */ -1f, /* duration= */ 100).validate();
assertThrows(IllegalArgumentException.class,
() -> new StepSegment(/* amplitude= */ -2, 10).validate());
() -> new StepSegment(/* amplitude= */ -2, 1f, 10).validate());
assertThrows(IllegalArgumentException.class,
() -> new StepSegment(/* amplitude= */ 2, 10).validate());
() -> new StepSegment(/* amplitude= */ 2, 1f, 10).validate());
assertThrows(IllegalArgumentException.class,
() -> new StepSegment(2, /* duration= */ -1).validate());
() -> new StepSegment(2, 1f, /* duration= */ -1).validate());
}
@Test
public void testHasNonZeroAmplitude() {
assertTrue(new StepSegment(1f, 0).hasNonZeroAmplitude());
assertTrue(new StepSegment(0.01f, 0).hasNonZeroAmplitude());
assertTrue(new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0).hasNonZeroAmplitude());
assertFalse(new StepSegment(0, 0).hasNonZeroAmplitude());
assertTrue(new StepSegment(1f, 0, 0).hasNonZeroAmplitude());
assertTrue(new StepSegment(0.01f, 0, 0).hasNonZeroAmplitude());
assertTrue(new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0).hasNonZeroAmplitude());
assertFalse(new StepSegment(0, 0, 0).hasNonZeroAmplitude());
}
@Test
public void testResolve() {
StepSegment original = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0);
StepSegment original = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0);
assertEquals(1f, original.resolve(VibrationEffect.MAX_AMPLITUDE).getAmplitude());
assertEquals(0.2f, original.resolve(51).getAmplitude(), TOLERANCE);
StepSegment resolved = new StepSegment(0, 0);
StepSegment resolved = new StepSegment(0, 0, 0);
assertSame(resolved, resolved.resolve(100));
assertThrows(IllegalArgumentException.class, () -> resolved.resolve(1000));
@@ -88,13 +90,13 @@ public class StepSegmentTest {
@Test
public void testApplyEffectStrength_ignoresAndReturnsSameEffect() {
StepSegment step = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0);
StepSegment step = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0);
assertSame(step, step.applyEffectStrength(VibrationEffect.EFFECT_STRENGTH_STRONG));
}
@Test
public void testScale_fullAmplitude() {
StepSegment initial = new StepSegment(1f, 0);
StepSegment initial = new StepSegment(1f, 0, 0);
assertEquals(1f, initial.scale(1).getAmplitude(), TOLERANCE);
assertEquals(0.34f, initial.scale(0.5f).getAmplitude(), TOLERANCE);
@@ -108,7 +110,7 @@ public class StepSegmentTest {
@Test
public void testScale_halfAmplitude() {
StepSegment initial = new StepSegment(0.5f, 0);
StepSegment initial = new StepSegment(0.5f, 0, 0);
assertEquals(0.5f, initial.scale(1).getAmplitude(), TOLERANCE);
assertEquals(0.17f, initial.scale(0.5f).getAmplitude(), TOLERANCE);
@@ -122,7 +124,7 @@ public class StepSegmentTest {
@Test
public void testScale_zeroAmplitude() {
StepSegment initial = new StepSegment(0, 0);
StepSegment initial = new StepSegment(0, 0, 0);
assertEquals(0f, initial.scale(1).getAmplitude(), TOLERANCE);
assertEquals(0f, initial.scale(0.5f).getAmplitude(), TOLERANCE);
@@ -131,7 +133,7 @@ public class StepSegmentTest {
@Test
public void testScale_defaultAmplitude() {
StepSegment initial = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0);
StepSegment initial = new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, 0, 0);
assertEquals(VibrationEffect.DEFAULT_AMPLITUDE, initial.scale(1).getAmplitude(), TOLERANCE);
assertEquals(VibrationEffect.DEFAULT_AMPLITUDE, initial.scale(0.5f).getAmplitude(),

View File

@@ -24,6 +24,7 @@ import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.SparseArray;
@@ -345,6 +346,8 @@ final class Vibration {
final long token = proto.start(fieldId);
if (segment instanceof StepSegment) {
dumpEffect(proto, SegmentProto.STEP, (StepSegment) segment);
} else if (segment instanceof RampSegment) {
dumpEffect(proto, SegmentProto.RAMP, (RampSegment) segment);
} else if (segment instanceof PrebakedSegment) {
dumpEffect(proto, SegmentProto.PREBAKED, (PrebakedSegment) segment);
} else if (segment instanceof PrimitiveSegment) {
@@ -357,6 +360,17 @@ final class Vibration {
final long token = proto.start(fieldId);
proto.write(StepSegmentProto.DURATION, segment.getDuration());
proto.write(StepSegmentProto.AMPLITUDE, segment.getAmplitude());
proto.write(StepSegmentProto.FREQUENCY, segment.getFrequency());
proto.end(token);
}
private void dumpEffect(ProtoOutputStream proto, long fieldId, RampSegment segment) {
final long token = proto.start(fieldId);
proto.write(RampSegmentProto.DURATION, segment.getDuration());
proto.write(RampSegmentProto.START_AMPLITUDE, segment.getStartAmplitude());
proto.write(RampSegmentProto.END_AMPLITUDE, segment.getEndAmplitude());
proto.write(RampSegmentProto.START_FREQUENCY, segment.getStartFrequency());
proto.write(RampSegmentProto.END_FREQUENCY, segment.getEndFrequency());
proto.end(token);
}

View File

@@ -75,8 +75,8 @@ final class FakeVibratorControllerProvider {
@Override
public void on(long milliseconds, long vibrationId) {
mEffectSegments.add(
new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, (int) milliseconds));
mEffectSegments.add(new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE,
/* frequency= */ 0, (int) milliseconds));
applyLatency();
scheduleListener(milliseconds, vibrationId);
}

View File

@@ -921,7 +921,7 @@ public class VibrationThreadTest {
}
private VibrationEffectSegment expectedOneShot(long millis) {
return new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, (int) millis);
return new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE, /* frequency= */ 0, (int) millis);
}
private VibrationEffectSegment expectedPrebaked(int effectId) {