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
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index aba4ce7..bd45aec 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1665,6 +1665,7 @@
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 @@
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 @@
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);
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index 95b5e85..c78bf8c 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -21,6 +21,7 @@
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.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 @@
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 @@
* @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
@@ -771,6 +791,42 @@
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.
*
* Similar to {@link #addPrimitive(int, float, int)}, but with no delay and a
@@ -829,6 +885,21 @@
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 @@
}
}
+ /**
+ * 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>() {
diff --git a/core/java/android/os/vibrator/RampSegment.java b/core/java/android/os/vibrator/RampSegment.java
new file mode 100644
index 0000000..aad87c5
--- /dev/null
+++ b/core/java/android/os/vibrator/RampSegment.java
@@ -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];
+ }
+ };
+}
diff --git a/core/java/android/os/vibrator/StepSegment.java b/core/java/android/os/vibrator/StepSegment.java
index 61a5d6c..11209e0 100644
--- a/core/java/android/os/vibrator/StepSegment.java
+++ b/core/java/android/os/vibrator/StepSegment.java
@@ -27,23 +27,25 @@
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 @@
}
StepSegment other = (StepSegment) o;
return Float.compare(mAmplitude, other.mAmplitude) == 0
+ && Float.compare(mFrequency, other.mFrequency) == 0
&& mDuration == other.mDuration;
}
@@ -61,6 +64,10 @@
return mAmplitude;
}
+ public float getFrequency() {
+ return mFrequency;
+ }
+
@Override
public long getDuration() {
return mDuration;
@@ -92,7 +99,8 @@
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 @@
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 @@
@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 void writeToParcel(@NonNull Parcel out, int flags) {
out.writeInt(PARCEL_TOKEN_STEP);
out.writeFloat(mAmplitude);
+ out.writeFloat(mFrequency);
out.writeInt(mDuration);
}
diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java
index 3dc9e12..5b42845 100644
--- a/core/java/android/os/vibrator/VibrationEffectSegment.java
+++ b/core/java/android/os/vibrator/VibrationEffectSegment.java
@@ -31,7 +31,8 @@
* <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 @@
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 @@
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:
diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto
index 16c3ab0..7b97524 100644
--- a/core/proto/android/server/vibrator/vibratormanagerservice.proto
+++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto
@@ -25,6 +25,16 @@
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 @@
optional PrebakedSegmentProto prebaked = 1;
optional PrimitiveSegmentProto primitive = 2;
optional StepSegmentProto step = 3;
+ optional RampSegmentProto ramp = 4;
}
// A com.android.os.VibrationEffect object.
diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java
index 242adab..009665f 100644
--- a/core/tests/coretests/src/android/os/VibrationEffectTest.java
+++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java
@@ -118,7 +118,16 @@
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 @@
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,
@@ -152,6 +175,16 @@
.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)
.compose()
.validate());
@@ -185,6 +218,12 @@
.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 @@
.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 @@
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 @@
return context;
}
-}
\ No newline at end of file
+}
diff --git a/core/tests/coretests/src/android/os/vibrator/RampSegmentTest.java b/core/tests/coretests/src/android/os/vibrator/RampSegmentTest.java
new file mode 100644
index 0000000..174b4a7
--- /dev/null
+++ b/core/tests/coretests/src/android/os/vibrator/RampSegmentTest.java
@@ -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);
+ }
+}
diff --git a/core/tests/coretests/src/android/os/vibrator/StepSegmentTest.java b/core/tests/coretests/src/android/os/vibrator/StepSegmentTest.java
index 188b6c1..79529b8 100644
--- a/core/tests/coretests/src/android/os/vibrator/StepSegmentTest.java
+++ b/core/tests/coretests/src/android/os/vibrator/StepSegmentTest.java
@@ -38,16 +38,18 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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(),
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 1e897ea..cd84058 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -24,6 +24,7 @@
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 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 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);
}
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
index a959a5e..1e3c344 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -75,8 +75,8 @@
@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);
}
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 739a1a3..37e0ec2 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -921,7 +921,7 @@
}
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) {