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) {